Skip to content

Commit 3aaeed6

Browse files
feat(i18n): cleanup unused translation keys (#1155)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent ea436c3 commit 3aaeed6

57 files changed

Lines changed: 520 additions & 2379 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ This focus helps guide our project decisions as a community and what we choose t
4343
- [RTL Support](#rtl-support)
4444
- [Localization (i18n)](#localization-i18n)
4545
- [Approach](#approach)
46+
- [i18n commands](#i18n-commands)
4647
- [Adding a new locale](#adding-a-new-locale)
4748
- [Update translation](#update-translation)
4849
- [Adding translations](#adding-translations)
@@ -378,6 +379,17 @@ npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization.
378379
- We use the `no_prefix` strategy (no `/en-US/` or `/fr-FR/` in URLs)
379380
- Locale preference is stored in cookies and respected on subsequent visits
380381

382+
### i18n commands
383+
384+
The following scripts help manage translation files. `en.json` is the reference locale.
385+
386+
| Command | Description |
387+
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
388+
| `pnpm i18n:check [locale]` | Compares `en.json` with other locale files. Shows missing and extra keys. Optionally filter output by locale (e.g. `pnpm i18n:check ja-JP`). |
389+
| `pnpm i18n:check:fix [locale]` | Same as check, but adds missing keys to other locales with English placeholders. |
390+
| `pnpm i18n:report` | Audits translation keys against code usage in `.vue` and `.ts` files. Reports missing keys (used in code but not in locale), unused keys (in locale but not in code), and dynamic keys. |
391+
| `pnpm i18n:report:fix` | Removes unused keys from `en.json` and all other locale files. |
392+
381393
### Adding a new locale
382394

383395
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.
@@ -421,25 +433,7 @@ Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essential
421433
We track the current progress of translations with [Lunaria](https://lunaria.dev/) on this site: https://i18n.npmx.dev/
422434
If you see any outdated translations in your language, feel free to update the keys to match the English version.
423435

424-
In order to make sure you have everything up-to-date, you can run:
425-
426-
```bash
427-
pnpm i18n:check <country-code>
428-
```
429-
430-
For example to check if all Japanese translation keys are up-to-date, run:
431-
432-
```bash
433-
pnpm i18n:check ja-JP
434-
```
435-
436-
To automatically add missing keys with English placeholders, use `--fix`:
437-
438-
```bash
439-
pnpm i18n:check:fix fr-FR
440-
```
441-
442-
This will add missing keys with `"EN TEXT TO REPLACE: {english text}"` as placeholder values, making it easier to see what needs translation.
436+
Use `pnpm i18n:check` and `pnpm i18n:check:fix` to verify and fix your locale (see [i18n commands](#i18n-commands) above for details).
443437

444438
#### Country variants (advanced)
445439

@@ -527,6 +521,32 @@ See how `es`, `es-ES`, and `es-419` are configured in [config/i18n.ts](./config/
527521
- Use `common.*` for shared strings (loading, retry, close, etc.)
528522
- Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*`
529523
- Do not use dashes (`-`) in translation keys; always use underscore (`_`): e.g., `privacy_policy` instead of `privacy-policy`
524+
- **Always use static string literals as translation keys.** Our i18n scripts (`pnpm i18n:report`) rely on static analysis to detect unused and missing keys. Dynamic keys cannot be analyzed and will be flagged as errors.
525+
526+
**Bad:**
527+
528+
```vue
529+
<!-- Template literal -->
530+
<p>{{ $t(`package.tabs.${tab}`) }}</p>
531+
532+
<!-- Variable -->
533+
<p>{{ $t(myKey) }}</p>
534+
```
535+
536+
**Good:**
537+
538+
```typescript
539+
const { t } = useI18n()
540+
541+
const tabLabels = computed(() => ({
542+
readme: t('package.tabs.readme'),
543+
versions: t('package.tabs.versions'),
544+
}))
545+
```
546+
547+
```vue
548+
<p>{{ tabLabels[tab] }}</p>
549+
```
530550

531551
### Using i18n-ally (recommended)
532552

app/components/ColumnPicker.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ onKeyDown(
4141
const toggleableColumns = computed(() => props.columns.filter(col => col.id !== 'name'))
4242
4343
// Map column IDs to i18n keys
44-
const columnLabelKey = computed(() => ({
44+
const columnLabels = computed(() => ({
4545
name: $t('filters.columns.name'),
4646
version: $t('filters.columns.version'),
4747
description: $t('filters.columns.description'),
@@ -57,7 +57,7 @@ const columnLabelKey = computed(() => ({
5757
}))
5858
5959
function getColumnLabel(id: ColumnId): string {
60-
const key = columnLabelKey.value[id]
60+
const key = columnLabels.value[id]
6161
return key ?? id
6262
}
6363

app/components/Filter/Panel.vue

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const emit = defineEmits<{
2727
'toggleKeyword': [keyword: string]
2828
}>()
2929
30+
const { t } = useI18n()
31+
3032
const isExpanded = shallowRef(false)
3133
const showAllKeywords = shallowRef(false)
3234
@@ -55,62 +57,77 @@ const hasMoreKeywords = computed(() => {
5557
})
5658
5759
// i18n mappings for filter options
58-
const scopeLabelKeys = {
59-
name: 'filters.scope_name',
60-
description: 'filters.scope_description',
61-
keywords: 'filters.scope_keywords',
62-
all: 'filters.scope_all',
63-
} as const
64-
65-
const scopeDescriptionKeys = {
66-
name: 'filters.scope_name_description',
67-
description: 'filters.scope_description_description',
68-
keywords: 'filters.scope_keywords_description',
69-
all: 'filters.scope_all_description',
70-
} as const
71-
72-
const downloadRangeLabelKeys = {
73-
'any': 'filters.download_range.any',
74-
'lt100': 'filters.download_range.lt100',
75-
'100-1k': 'filters.download_range.100_1k',
76-
'1k-10k': 'filters.download_range.1k_10k',
77-
'10k-100k': 'filters.download_range.10k_100k',
78-
'gt100k': 'filters.download_range.gt100k',
79-
} as const
80-
81-
const updatedWithinLabelKeys = {
82-
any: 'filters.updated.any',
83-
week: 'filters.updated.week',
84-
month: 'filters.updated.month',
85-
quarter: 'filters.updated.quarter',
86-
year: 'filters.updated.year',
87-
} as const
88-
89-
const securityLabelKeys = {
90-
all: 'filters.security_options.all',
91-
secure: 'filters.security_options.secure',
92-
warnings: 'filters.security_options.insecure',
93-
} as const
60+
const scopeLabelKeys = computed(
61+
() =>
62+
({
63+
name: t('filters.scope_name'),
64+
description: t('filters.scope_description'),
65+
keywords: t('filters.scope_keywords'),
66+
all: t('filters.scope_all'),
67+
}) as const,
68+
)
69+
70+
const scopeDescriptionKeys = computed(
71+
() =>
72+
({
73+
name: t('filters.scope_name_description'),
74+
description: t('filters.scope_description_description'),
75+
keywords: t('filters.scope_keywords_description'),
76+
all: t('filters.scope_all_description'),
77+
}) as const,
78+
)
79+
80+
const downloadRangeLabelKeys = computed(
81+
() =>
82+
({
83+
'any': t('filters.download_range.any'),
84+
'lt100': t('filters.download_range.lt100'),
85+
'100-1k': t('filters.download_range.100_1k'),
86+
'1k-10k': t('filters.download_range.1k_10k'),
87+
'10k-100k': t('filters.download_range.10k_100k'),
88+
'gt100k': t('filters.download_range.gt100k'),
89+
}) as const,
90+
)
91+
92+
const updatedWithinLabelKeys = computed(
93+
() =>
94+
({
95+
any: t('filters.updated.any'),
96+
week: t('filters.updated.week'),
97+
month: t('filters.updated.month'),
98+
quarter: t('filters.updated.quarter'),
99+
year: t('filters.updated.year'),
100+
}) as const,
101+
)
102+
103+
const securityLabelKeys = computed(
104+
() =>
105+
({
106+
all: t('filters.security_options.all'),
107+
secure: t('filters.security_options.secure'),
108+
warnings: t('filters.security_options.insecure'),
109+
}) as const,
110+
)
94111
95112
// Type-safe accessor functions
96113
function getScopeLabelKey(value: SearchScope): string {
97-
return scopeLabelKeys[value]
114+
return scopeLabelKeys.value[value]
98115
}
99116
100117
function getScopeDescriptionKey(value: SearchScope): string {
101-
return scopeDescriptionKeys[value]
118+
return scopeDescriptionKeys.value[value]
102119
}
103120
104121
function getDownloadRangeLabelKey(value: DownloadRange): string {
105-
return downloadRangeLabelKeys[value]
122+
return downloadRangeLabelKeys.value[value]
106123
}
107124
108125
function getUpdatedWithinLabelKey(value: UpdatedWithin): string {
109-
return updatedWithinLabelKeys[value]
126+
return updatedWithinLabelKeys.value[value]
110127
}
111128
112129
function getSecurityLabelKey(value: SecurityFilter): string {
113-
return securityLabelKeys[value]
130+
return securityLabelKeys.value[value]
114131
}
115132
116133
function handleTextInput(event: Event) {
@@ -215,10 +232,10 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
215232
: 'text-fg-muted hover:text-fg'
216233
"
217234
:aria-pressed="filters.searchScope === scope"
218-
:title="$t(getScopeDescriptionKey(scope))"
235+
:title="getScopeDescriptionKey(scope)"
219236
@click="emit('update:searchScope', scope)"
220237
>
221-
{{ $t(getScopeLabelKey(scope)) }}
238+
{{ getScopeLabelKey(scope) }}
222239
</button>
223240
</div>
224241
</div>
@@ -251,7 +268,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
251268
@update:modelValue="emit('update:downloadRange', $event as DownloadRange)"
252269
name="range"
253270
>
254-
{{ $t(getDownloadRangeLabelKey(range.value)) }}
271+
{{ getDownloadRangeLabelKey(range.value) }}
255272
</TagRadioButton>
256273
</div>
257274
</fieldset>
@@ -274,7 +291,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
274291
name="updatedWithin"
275292
@update:modelValue="emit('update:updatedWithin', $event as UpdatedWithin)"
276293
>
277-
{{ $t(getUpdatedWithinLabelKey(option.value)) }}
294+
{{ getUpdatedWithinLabelKey(option.value) }}
278295
</TagRadioButton>
279296
</div>
280297
</fieldset>
@@ -296,7 +313,7 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
296313
:value="security"
297314
name="security"
298315
>
299-
{{ $t(getSecurityLabelKey(security)) }}
316+
{{ getSecurityLabelKey(security) }}
300317
</TagRadioButton>
301318
</div>
302319
</fieldset>

app/components/Package/ListToolbar.vue

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const props = defineProps<{
3232
searchContext?: boolean
3333
}>()
3434
35+
const { t } = useI18n()
36+
3537
const sortOption = defineModel<SortOption>('sortOption', { required: true })
3638
const viewMode = defineModel<ViewMode>('viewMode', { required: true })
3739
const paginationMode = defineModel<PaginationMode>('paginationMode', { required: true })
@@ -85,22 +87,22 @@ function handleToggleDirection() {
8587
}
8688
8789
// Map sort key to i18n key
88-
const sortKeyLabelKeys: Record<SortKey, string> = {
89-
'relevance': 'filters.sort.relevance',
90-
'downloads-week': 'filters.sort.downloads_week',
91-
'downloads-day': 'filters.sort.downloads_day',
92-
'downloads-month': 'filters.sort.downloads_month',
93-
'downloads-year': 'filters.sort.downloads_year',
94-
'updated': 'filters.sort.published',
95-
'name': 'filters.sort.name',
96-
'quality': 'filters.sort.quality',
97-
'popularity': 'filters.sort.popularity',
98-
'maintenance': 'filters.sort.maintenance',
99-
'score': 'filters.sort.score',
100-
}
90+
const sortKeyLabelKeys = computed<Record<SortKey, string>>(() => ({
91+
'relevance': t('filters.sort.relevance'),
92+
'downloads-week': t('filters.sort.downloads_week'),
93+
'downloads-day': t('filters.sort.downloads_day'),
94+
'downloads-month': t('filters.sort.downloads_month'),
95+
'downloads-year': t('filters.sort.downloads_year'),
96+
'updated': t('filters.sort.published'),
97+
'name': t('filters.sort.name'),
98+
'quality': t('filters.sort.quality'),
99+
'popularity': t('filters.sort.popularity'),
100+
'maintenance': t('filters.sort.maintenance'),
101+
'score': t('filters.sort.score'),
102+
}))
101103
102104
function getSortKeyLabelKey(key: SortKey): string {
103-
return sortKeyLabelKeys[key]
105+
return sortKeyLabelKeys.value[key]
104106
}
105107
</script>
106108

@@ -169,7 +171,7 @@ function getSortKeyLabelKey(key: SortKey): string {
169171
:value="keyConfig.key"
170172
:disabled="keyConfig.disabled"
171173
>
172-
{{ $t(getSortKeyLabelKey(keyConfig.key)) }}
174+
{{ getSortKeyLabelKey(keyConfig.key) }}
173175
</option>
174176
</select>
175177
<div

0 commit comments

Comments
 (0)