Skip to content

Commit 894ee96

Browse files
authored
Merge branch 'main' into feat-production-setup
2 parents 183e810 + 7cf66b2 commit 894ee96

25 files changed

Lines changed: 3244 additions & 140 deletions

CONTRIBUTING.md

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -256,31 +256,46 @@ The [config/i18n.ts](./config/i18n.ts) configuration file will be used to regist
256256
- `locales` object will be used to link the supported locales (country and single one)
257257
- `buildLocales` function will build the target locales
258258

259-
To register a new locale:
259+
To add a new locale:
260260

261-
- 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
262-
- 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))
261+
1. Create a new JSON file in [`i18n/locales/`](./i18n/locales) with the locale code as the filename (e.g., `uk-UA.json`, `de-DE.json`)
262+
2. Copy [`en.json`](./i18n/locales/en.json) and translate the strings
263+
3. Add the locale to the `locales` array in [config/i18n.ts](./config/i18n.ts):
263264

264-
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.
265+
```typescript
266+
{
267+
code: 'uk-UA', // Must match the filename (without .json)
268+
file: 'uk-UA.json',
269+
name: 'Українська', // Native name of the language
270+
},
271+
```
265272

266-
To add a new locale:
273+
4. Copy your translation file to `lunaria/files/` for translation tracking:
274+
275+
```bash
276+
cp i18n/locales/uk-UA.json lunaria/files/uk-UA.json
277+
```
267278

268-
1. Add a new file at [locales](./i18n/locales) folder with the language code as the filename.
269-
2. Copy [en](./i18n/locales/en.json) and translate the strings
270-
3. Add the language to the `locales` array in [config/i18n.ts](./config/i18n.ts), below `en` and `ar`:
271-
- 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)
272-
- Add all country variants in [country variants object](./config/i18n.ts)
273-
- Add all country variants files with empty `messages` object: `{}`
274-
- Translate the strings in the generic language file
275-
- 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.
276-
- If the generic language already exists:
277-
- If the translation doesn't differ from the generic language, then add the corresponding translations in the corresponding file
278-
- 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
279-
4. If the language is `right-to-left`, add `dir` option with `rtl` value, for example, for [ar](./config/i18n.ts)
280-
5. If the language requires special pluralization rules, add `pluralRule` callback option, for example, for [ar](./config/i18n.ts)
279+
> [!IMPORTANT]
280+
> This file must be committed. Lunaria uses git history to track translation progress, so the build will fail if this file is missing.
281+
282+
5. If the language is `right-to-left`, add `dir: 'rtl'` (see `ar-EG` in config for example)
283+
6. If the language requires special pluralization rules, add a `pluralRule` callback (see `ar-EG` or `ru-RU` in config for examples)
281284

282285
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info.
283286

287+
#### Country variants (advanced)
288+
289+
Most languages only need a single locale file. Country variants are only needed when you want to support regional differences (e.g., `es-ES` for Spain vs `es-419` for Latin America).
290+
291+
If you need country variants:
292+
293+
1. Create a base language file (e.g., `es.json`) with all translations
294+
2. Create country variant files (e.g., `es-ES.json`, `es-419.json`) with only the differing translations
295+
3. Register the base language in `locales` and add variants to `countryLocaleVariants`
296+
297+
See how `es`, `es-ES`, and `es-419` are configured in [config/i18n.ts](./config/i18n.ts) for a complete example.
298+
284299
### Adding translations
285300

286301
1. Add your translation key to `i18n/locales/en.json` first (American English is the source of truth)

app/components/PackageDeprecatedTree.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const isExpanded = shallowRef(false)
1515
const showAll = shallowRef(false)
1616
1717
const hasDeprecated = computed(
18-
() => analysisData.value && analysisData.value.deprecatedPackages.length > 0,
18+
() => analysisData.value?.deprecatedPackages && analysisData.value.deprecatedPackages.length > 0,
1919
)
2020
2121
// Banner color - purple for deprecated

app/components/PackageDownloadAnalytics.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ const config = computed(() => {
463463
return {
464464
theme: isDarkMode.value ? 'dark' : 'default',
465465
chart: {
466-
height: isMobile.value ? 850 : 600,
466+
height: isMobile.value ? 950 : 600,
467467
userOptions: {
468468
buttons: {
469469
pdf: false,
@@ -508,15 +508,17 @@ const config = computed(() => {
508508
grid: {
509509
stroke: colors.value.border,
510510
labels: {
511+
fontSize: isMobile.value ? 24 : 16,
511512
axis: {
512513
yLabel: $t('package.downloads.y_axis_label', {
513514
granularity: $t(`package.downloads.granularity_${selectedGranularity.value}`),
514515
}),
515516
xLabel: packageName,
516517
yLabelOffsetX: 12,
517-
fontSize: 24,
518+
fontSize: isMobile.value ? 32 : 24,
518519
},
519520
xAxisLabels: {
521+
show: !isMobile.value,
520522
values: chartData.value?.dates,
521523
showOnlyAtModulo: true,
522524
modulo: 12,
@@ -554,7 +556,7 @@ const config = computed(() => {
554556
},
555557
},
556558
zoom: {
557-
maxWidth: 500,
559+
maxWidth: isMobile.value ? 350 : 500,
558560
customFormat:
559561
displayedGranularity.value !== 'weekly'
560562
? undefined

app/components/PackageReplacement.vue

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,27 @@ const props = defineProps<{
55
replacement: ModuleReplacement
66
}>()
77
8-
const { t } = useI18n()
9-
10-
const message = computed(() => {
8+
const message = computed<[string, { replacement?: string; nodeVersion?: string }]>(() => {
119
switch (props.replacement.type) {
1210
case 'native':
13-
return t('package.replacement.native', {
14-
replacement: props.replacement.replacement,
15-
nodeVersion: props.replacement.nodeVersion,
16-
})
11+
return [
12+
'package.replacement.native',
13+
{
14+
replacement: props.replacement.replacement,
15+
nodeVersion: props.replacement.nodeVersion,
16+
},
17+
]
1718
case 'simple':
18-
return t('package.replacement.simple', {
19-
replacement: props.replacement.replacement,
20-
})
19+
return [
20+
'package.replacement.simple',
21+
{
22+
replacement: props.replacement.replacement,
23+
},
24+
]
2125
case 'documented':
22-
return t('package.replacement.documented')
26+
return ['package.replacement.documented', {}]
2327
case 'none':
24-
return t('package.replacement.none')
28+
return ['package.replacement.none', {}]
2529
}
2630
})
2731
@@ -45,7 +49,25 @@ const docPath = computed(() => {
4549
{{ $t('package.replacement.title') }}
4650
</h2>
4751
<p class="text-sm m-0">
48-
{{ message }}
52+
<i18n-t :keypath="message[0]" scope="global">
53+
<template #replacement>
54+
{{ message[1].replacement ?? '' }}
55+
</template>
56+
<template #nodeVersion>
57+
{{ message[1].nodeVersion ?? '' }}
58+
</template>
59+
<template #community>
60+
<a
61+
href="https://e18e.dev/docs/replacements/"
62+
target="_blank"
63+
rel="noopener noreferrer"
64+
class="inline-flex items-center gap-1 ms-1 underline underline-offset-4 decoration-amber-600/60 dark:decoration-amber-400/50 hover:decoration-fg transition-colors"
65+
>
66+
{{ $t('package.replacement.community') }}
67+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
68+
</a>
69+
</template>
70+
</i18n-t>
4971
<a
5072
v-if="mdnUrl"
5173
:href="mdnUrl"

app/components/PackageVersions.vue

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,14 @@ const visibleTagRows = computed(() => {
109109
const rows = isPackageDeprecated.value
110110
? allTagRows.value
111111
: allTagRows.value.filter(row => !row.primaryVersion.deprecated)
112-
return rows.slice(0, MAX_VISIBLE_TAGS)
112+
const first = rows.slice(0, MAX_VISIBLE_TAGS)
113+
const latestTagRow = rows.find(row => row.tag === 'latest')
114+
// Ensure 'latest' tag is always included (at the end) if not already present
115+
if (latestTagRow && !first.includes(latestTagRow)) {
116+
first.pop()
117+
first.push(latestTagRow)
118+
}
119+
return first
113120
})
114121
115122
// Hidden tag rows (all other tags) - shown in "Other versions"
@@ -322,7 +329,10 @@ function getTagVersions(tag: string): VersionDisplay[] {
322329
<div class="space-y-0.5 min-w-0">
323330
<!-- Dist-tag rows (limited to MAX_VISIBLE_TAGS) -->
324331
<div v-for="row in visibleTagRows" :key="row.id">
325-
<div class="flex items-center gap-2">
332+
<div
333+
class="flex items-center gap-2 pe-2"
334+
:class="row.tag === 'latest' ? 'bg-bg-subtle rounded-lg' : ''"
335+
>
326336
<!-- Expand button (only if there are more versions to show) -->
327337
<button
328338
v-if="getTagVersions(row.tag).length > 1 || !hasLoadedAll"
@@ -354,65 +364,67 @@ function getTagVersions(tag: string): VersionDisplay[] {
354364
<span v-else class="w-4" />
355365

356366
<!-- Version info -->
357-
<div class="flex-1 py-1.5 min-w-0">
358-
<div class="flex items-center justify-between gap-2">
359-
<NuxtLink
360-
:to="versionRoute(row.primaryVersion.version)"
361-
class="font-mono text-sm transition-colors duration-200 truncate inline-flex items-center gap-1"
362-
:class="
363-
row.primaryVersion.deprecated
364-
? 'text-red-400 hover:text-red-300'
365-
: 'text-fg-muted hover:text-fg'
366-
"
367-
:title="
368-
row.primaryVersion.deprecated
369-
? $t('package.versions.deprecated_title', {
370-
version: row.primaryVersion.version,
371-
})
372-
: row.primaryVersion.version
373-
"
374-
>
367+
<div class="flex-1 py-1.5 min-w-0 flex gap-2 justify-between items-center">
368+
<div>
369+
<div>
370+
<NuxtLink
371+
:to="versionRoute(row.primaryVersion.version)"
372+
class="font-mono text-sm transition-colors duration-200 truncate inline-flex items-center gap-1"
373+
:class="
374+
row.primaryVersion.deprecated
375+
? 'text-red-400 hover:text-red-300'
376+
: 'text-fg-muted hover:text-fg'
377+
"
378+
:title="
379+
row.primaryVersion.deprecated
380+
? $t('package.versions.deprecated_title', {
381+
version: row.primaryVersion.version,
382+
})
383+
: row.primaryVersion.version
384+
"
385+
>
386+
<span
387+
v-if="row.primaryVersion.deprecated"
388+
class="i-carbon-warning-hex w-3.5 h-3.5 shrink-0"
389+
aria-hidden="true"
390+
/>
391+
{{ row.primaryVersion.version }}
392+
</NuxtLink>
393+
</div>
394+
<div v-if="row.tags.length" class="flex items-center gap-1 mt-0.5 flex-wrap">
375395
<span
376-
v-if="row.primaryVersion.deprecated"
377-
class="i-carbon-warning-hex w-3.5 h-3.5 shrink-0"
378-
aria-hidden="true"
379-
/>
380-
{{ row.primaryVersion.version }}
381-
</NuxtLink>
382-
<div class="flex items-center gap-2 shrink-0">
383-
<DateTime
384-
v-if="row.primaryVersion.time"
385-
:datetime="row.primaryVersion.time"
386-
year="numeric"
387-
month="short"
388-
day="numeric"
389-
class="text-xs text-fg-subtle"
390-
/>
391-
<ProvenanceBadge
392-
v-if="row.primaryVersion.hasProvenance"
393-
:package-name="packageName"
394-
:version="row.primaryVersion.version"
395-
compact
396-
/>
396+
v-for="tag in row.tags"
397+
:key="tag"
398+
class="text-[9px] font-semibold text-fg-subtle uppercase tracking-wide truncate max-w-[150px]"
399+
:title="tag"
400+
>
401+
{{ tag }}
402+
</span>
397403
</div>
398404
</div>
399-
<div v-if="row.tags.length" class="flex items-center gap-1 mt-0.5 flex-wrap">
400-
<span
401-
v-for="tag in row.tags"
402-
:key="tag"
403-
class="text-[9px] font-semibold text-fg-subtle uppercase tracking-wide truncate max-w-[150px]"
404-
:title="tag"
405-
>
406-
{{ tag }}
407-
</span>
405+
<div class="flex items-center gap-2 shrink-0">
406+
<DateTime
407+
v-if="row.primaryVersion.time"
408+
:datetime="row.primaryVersion.time"
409+
year="numeric"
410+
month="short"
411+
day="numeric"
412+
class="text-xs text-fg-subtle"
413+
/>
414+
<ProvenanceBadge
415+
v-if="row.primaryVersion.hasProvenance"
416+
:package-name="packageName"
417+
:version="row.primaryVersion.version"
418+
compact
419+
/>
408420
</div>
409421
</div>
410422
</div>
411423

412424
<!-- Expanded versions -->
413425
<div
414426
v-if="expandedTags.has(row.tag) && getTagVersions(row.tag).length > 1"
415-
class="ms-4 ps-2 border-is border-border space-y-0.5"
427+
class="ms-4 ps-2 border-is border-border space-y-0.5 pe-2"
416428
>
417429
<div v-for="v in getTagVersions(row.tag).slice(1)" :key="v.version" class="py-1">
418430
<div class="flex items-center justify-between gap-2">
@@ -542,7 +554,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
542554
/>
543555
{{ row.primaryVersion.version }}
544556
</NuxtLink>
545-
<div class="flex items-center gap-2 shrink-0">
557+
<div class="flex items-center gap-2 shrink-0 pe-2">
546558
<DateTime
547559
v-if="row.primaryVersion.time"
548560
:datetime="row.primaryVersion.time"
@@ -618,7 +630,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
618630
{{ group.versions[0]?.version }}
619631
</NuxtLink>
620632
</div>
621-
<div class="flex items-center gap-2 shrink-0">
633+
<div class="flex items-center gap-2 shrink-0 pe-2">
622634
<DateTime
623635
v-if="group.versions[0]?.time"
624636
:datetime="group.versions[0]?.time"

app/pages/docs/[...path].vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,16 @@ const showEmptyState = computed(() => docsData.value?.status !== 'ok')
364364
@apply text-badge-orange text-sm;
365365
}
366366
367-
.docs-content .docs-deprecated p {
368-
@apply text-badge-orange text-sm mt-2 mb-0;
367+
.docs-content .docs-deprecated-message {
368+
@apply text-badge-orange text-sm mt-2;
369+
}
370+
371+
.docs-content .docs-deprecated-message code {
372+
@apply bg-badge-orange/20 text-badge-orange;
373+
}
374+
375+
.docs-content .docs-deprecated-message .docs-link {
376+
@apply text-badge-orange;
369377
}
370378
371379
/* Parameters, Returns, Examples, See Also sections */

app/pages/settings.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ const { locale, locales, setLocale } = useI18n()
55
const colorMode = useColorMode()
66
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
77
8-
const availableLocales = computed(() =>
9-
locales.value.map(l => (typeof l === 'string' ? { code: l, name: l } : l)),
10-
)
11-
128
// Escape to go back (but not when focused on form elements)
139
onKeyStroke('Escape', e => {
1410
const target = e.target as HTMLElement
@@ -211,7 +207,7 @@ defineOgImageComponent('Default', {
211207
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"
212208
@change="setLocale(($event.target as HTMLSelectElement).value as typeof locale)"
213209
>
214-
<option v-for="loc in availableLocales" :key="loc.code" :value="loc.code">
210+
<option v-for="loc in locales" :key="loc.code" :value="loc.code" :lang="loc.code">
215211
{{ loc.name }}
216212
</option>
217213
</select>

0 commit comments

Comments
 (0)