Skip to content

Commit 8227417

Browse files
Merge branch 'main' into fix/package-manager-tab-mismatch
2 parents 7297e46 + 99cc5a8 commit 8227417

14 files changed

Lines changed: 626 additions & 51 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ test-results/
3636

3737
# Lighthouse
3838
.lighthouseci
39+
40+
# generated files
41+
shared/types/lexicons

CONTRIBUTING.md

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -208,20 +208,70 @@ const props = defineProps<{
208208

209209
Ideally, extract utilities into separate files so they can be unit tested. 🙏
210210

211+
## RTL Support
212+
213+
We support `right-to-left` languages, we need to make sure that the UI is working correctly in both directions.
214+
215+
Simple approach used by most websites of relying on direction set in HTML element does not work because direction for various items, such as timeline, does not always match direction set in HTML.
216+
217+
We've added some `UnoCSS` utilities styles to help you with that:
218+
219+
- Do not use `left/right` padding and margin: for example `pl-1`. Use `padding-inline-start/end` instead. So `pl-1` should be `ps-1`, `pr-1` should be `pe-1`. The same rules apply to margin.
220+
- Do not use `rtl-` classes, such as `rtl-left-0`.
221+
- For icons that should be rotated for RTL, add `class="rtl-flip"`. This can only be used for icons outside of elements with `dir="auto"`.
222+
- For absolute positioned elements, don't use `left/right`: for example `left-0`. Use `inset-inline-start/end` instead. `UnoCSS` shortcuts are `inset-is` for `inset-inline-start` and `inset-ie` for `inset-inline-end`. Example: `left-0` should be replaced with `inset-is-0`.
223+
- If you need to change the border radius for an entire left or right side, use `border-inline-start/end`. `UnoCSS` shortcuts are `rounded-is` for left side, `rounded-ie` for right side. Example: `rounded-l-5` should be replaced with `rounded-ie-5`.
224+
- If you need to change the border radius for one corner, use `border-start-end-radius` and similar rules. `UnoCSS` shortcuts are `rounded` + top/bottom as either `-bs` (top) or `-be` (bottom) + left/right as either `-is` (left) or `-ie` (right). Example: `rounded-tl-0` should be replaced with `rounded-bs-is-0`.
225+
211226
## Localization (i18n)
212227

213228
npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization. We aim to make the UI accessible to users in their preferred language.
214229

215230
### Approach
216231

217232
- All user-facing strings should use translation keys via `$t()` in templates and script
218-
- Translation files live in `i18n/locales/` (e.g., `en.json`)
219-
- We use the `no_prefix` strategy (no `/en/` or `/fr/` in URLs)
233+
- Translation files live in `i18n/locales/` (e.g., `en-US.json`)
234+
- We use the `no_prefix` strategy (no `/en-US/` or `/fr-FR/` in URLs)
220235
- Locale preference is stored in cookies and respected on subsequent visits
221236

237+
### Adding a new locale
238+
239+
We are using localization using country variants (ISO-6391) via [multiple translation files](https://i18n.nuxtjs.org/docs/guide/lazy-load-translations#multiple-files-lazy-loading) to avoid repeating every key per country.
240+
241+
The [config/i18n.ts](./config/i18n.ts) configuration file will be used to register the new locale:
242+
243+
- `countryLocaleVariants` object will be used to register the country variants
244+
- `locales` object will be used to link the supported locales (country and single one)
245+
- `buildLocales` function will build the target locales
246+
247+
To register a new locale:
248+
249+
- for a single country, your JSON file should include the language and the country in the name (for example, `pl-PL.json`) and register the info at `locales` object
250+
- for multiple country variants, you need to add the default language JSON file (for example for Spanish, `es.json`) and one of the country variants (for example for Spanish for Spain, `es-ES.json`); register the language at `countryLocaleVariants` object adding the country variants with the JSON country file and register the language at `locales` object using the language JSON file (check how we register `es`, `es-ES` and `es-419` in [config/i18n.ts](./config/i18n.ts))
251+
252+
The country file should contain will contain only the translations that differ from the language JSON file, Vue I18n will merge the messages for us.
253+
254+
To add a new locale:
255+
256+
1. Add a new file at [locales](./i18n/locales) folder with the language code as the filename.
257+
2. Copy [en](./i18n/locales/en.json) and translate the strings
258+
3. Add the language to the `locales` array in [config/i18n.ts](./config/i18n.ts), below `en` and `ar`:
259+
- If your language has multiple country variants, add the generic one for language only (only if there are a lot of common entries, you can always add it as a new one)
260+
- Add all country variants in [country variants object](./config/i18n.ts)
261+
- Add all country variants files with empty `messages` object: `{}`
262+
- Translate the strings in the generic language file
263+
- Later, when anyone wants to add the corresponding translations for the country variant, just override any entry in the corresponding file: you can see an example with `es` variants.
264+
- If the generic language already exists:
265+
- If the translation doesn't differ from the generic language, then add the corresponding translations in the corresponding file
266+
- If the translation differs from the generic language, then add the corresponding translations in the corresponding file and remove it from the country variants entry
267+
4. If the language is `right-to-left`, add `dir` option with `rtl` value, for example, for [ar](./config/i18n.ts)
268+
5. If the language requires special pluralization rules, add `pluralRule` callback option, for example, for [ar](./config/i18n.ts)
269+
270+
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info.
271+
222272
### Adding translations
223273

224-
1. Add your translation key to `i18n/locales/en.json` first (English is the source of truth)
274+
1. Add your translation key to `i18n/locales/en.json` first (American English is the source of truth)
225275
2. Use the key in your component:
226276

227277
```vue
@@ -267,22 +317,6 @@ We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName
267317

268318
The extension is included in our workspace recommendations, so VSCode should prompt you to install it.
269319

270-
### Adding a new locale
271-
272-
1. Create a new JSON file in `i18n/locales/` (e.g., `fr.json`)
273-
2. Add the locale to `nuxt.config.ts`:
274-
275-
```typescript
276-
i18n: {
277-
locales: [
278-
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
279-
{ code: 'fr', language: 'fr-FR', name: 'Francais', file: 'fr.json' },
280-
],
281-
}
282-
```
283-
284-
3. Translate all keys from `en.json`
285-
286320
### Formatting with locale
287321

288322
When formatting numbers or dates that should respect the user's locale, pass the locale:

app/assets/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ html {
8080
}
8181
}
8282

83+
html[dir='rtl'] .rtl-flip {
84+
transform: scale(-1, 1);
85+
}
86+
8387
body {
8488
margin: 0;
8589
background-color: var(--bg);

app/components/AppHeader.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ const debouncedNavigate = debounce(async () => {
3030
name: 'search',
3131
query: query ? { q: query } : undefined,
3232
})
33-
searchQuery.value = ''
33+
// allow time for the navigation to occur before resetting searchQuery
34+
setTimeout(() => (searchQuery.value = ''), 1000)
3435
}, 100)
3536
3637
async function handleSearchInput() {

app/pages/index.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const debouncedNavigate = debounce(() => {
1414
}, 250)
1515
1616
function handleSearch() {
17-
debouncedNavigate()
17+
// If input is empty, navigate immediately (no need to debounce)
18+
return searchQuery.value.trim() ? debouncedNavigate() : router.push('/search')
1819
}
1920
2021
useSeoMeta({

app/pages/search.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const resultCount = computed(() => visibleResults.value?.objects.length ?? 0)
5656
// Track if page just loaded (for hiding "Searching..." during view transition)
5757
const hasInteracted = ref(false)
5858
onMounted(() => {
59+
// Focus search onMount
60+
isSearchFocused.value = true
5961
// Small delay to let view transition complete
6062
setTimeout(() => {
6163
hasInteracted.value = true

app/pages/settings.vue

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ defineOgImageComponent('Default', {
203203
</div>
204204
</section>
205205

206-
<!-- LANGUAGE Section -->
207206
<section>
208207
<h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4">
209208
{{ $t('settings.sections.language') }}
@@ -214,16 +213,22 @@ defineOgImageComponent('Default', {
214213
<label for="language-select" class="block text-sm text-fg font-medium">
215214
{{ $t('settings.language') }}
216215
</label>
217-
<select
218-
id="language-select"
219-
:value="locale"
220-
class="w-full sm:w-auto min-w-48 bg-bg border border-border rounded-md px-3 py-2 text-sm text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 cursor-pointer"
221-
@change="setLocale(($event.target as HTMLSelectElement).value as typeof locale)"
222-
>
223-
<option v-for="loc in availableLocales" :key="loc.code" :value="loc.code">
224-
{{ loc.name }}
225-
</option>
226-
</select>
216+
217+
<ClientOnly>
218+
<select
219+
id="language-select"
220+
:value="locale"
221+
class="w-full sm:w-auto min-w-48 bg-bg border border-border rounded-md px-3 py-2 text-sm text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 cursor-pointer"
222+
@change="setLocale(($event.target as HTMLSelectElement).value as typeof locale)"
223+
>
224+
<option v-for="loc in availableLocales" :key="loc.code" :value="loc.code">
225+
{{ loc.name }}
226+
</option>
227+
</select>
228+
<template #fallback>
229+
<span class="skeleton block w-48 h-8" />
230+
</template>
231+
</ClientOnly>
227232
</div>
228233

229234
<!-- Translation helper for non-source locales -->

knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"$schema": "https://unpkg.com/knip@5/schema.json",
33
"includeEntryExports": true,
44
"ignoreExportsUsedInFile": true,
5-
"ignoreBinaries": ["simple-git-hooks", "lint-staged"],
5+
"ignoreBinaries": ["simple-git-hooks", "lint-staged", "lex"],
6+
"ignore": ["shared/types/lexicons/**"],
67
"workspaces": {
78
".": {
89
"entry": [

lexicons.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": 1,
3+
"lexicons": ["site.standard.document"],
4+
"resolutions": {
5+
"com.atproto.repo.strongRef": {
6+
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.strongRef",
7+
"cid": "bafyreifrkdbnkvfjujntdaeigolnrjj3srrs53tfixjhmacclps72qlov4"
8+
},
9+
"site.standard.document": {
10+
"uri": "at://did:plc:re3ebnp5v7ffagz6rb6xfei4/com.atproto.lexicon.schema/site.standard.document",
11+
"cid": "bafyreigdukg62hmel4jbdvghdsoaphslhrdktmahzdyokomuka6yejetwa"
12+
}
13+
}
14+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"id": "com.atproto.repo.strongRef",
3+
"defs": {
4+
"main": {
5+
"type": "object",
6+
"required": ["uri", "cid"],
7+
"properties": {
8+
"cid": {
9+
"type": "string",
10+
"format": "cid"
11+
},
12+
"uri": {
13+
"type": "string",
14+
"format": "at-uri"
15+
}
16+
}
17+
}
18+
},
19+
"$type": "com.atproto.lexicon.schema",
20+
"lexicon": 1,
21+
"description": "A URI with a content-hash fingerprint."
22+
}

0 commit comments

Comments
 (0)