i18n Tooling
Scripts for managing translation keys in ValGuide.
Type Safety
Translation keys are type-checked at compile time via packages/core/i18n/global.d.ts:
import type en from './messages/en.json'
type Messages = typeof en
declare module 'use-intl' {
interface AppConfig {
Messages: Messages
}
}This provides:
- Autocomplete for translation keys in your editor
- Compile-time errors when using non-existent keys
- Refactoring support when renaming keys
Run val type-check to catch missing keys before runtime.
Commands
| Command | Description |
|---|---|
pnpm i18n:check | Validate locale files are in sync |
pnpm i18n:unused | Find unused translation keys |
pnpm i18n:prune --dry-run | Preview keys that would be removed |
pnpm i18n:prune | Remove unused keys from all locales |
Validating Locale Files
pnpm i18n:checkValidates that all locale files are in sync:
- Missing keys: Reports keys in
en.jsonthat are missing from other locales (de.json,rm.json) - Extra keys: Warns about keys in other locales that don't exist in
en.json - Duplicate keys: Detects duplicate keys within the same file (which JSON silently overwrites)
Run this in CI to ensure translations stay consistent across locales.
Finding Unused Keys
pnpm i18n:unusedScans the codebase using AST parsing (ts-morph) and reports translation keys that aren't used anywhere.
Removing Unused Keys
pnpm i18n:prune --dry-run # Preview changes
pnpm i18n:prune # Remove unused keysRemoves unused keys from all locale files (en.json, de.json, rm.json). Always run with --dry-run first to review what will be deleted.
How It Works
The scripts use ts-morph for AST parsing to accurately detect key usage:
- Finds all files using
useTranslationsorgetTranslations - Tracks namespace bindings:
const t = useTranslations('namespace') - Detects key usage:
t('key'),t.rich('key'), etc. - Handles ternary expressions in both namespaces and keys
- Supports
// i18n-used-keyscomments for dynamic keys
Detected Patterns
// ✅ Simple namespace + key
const t = useTranslations('tours')
t('title') // → guides.title
// ✅ Ternary namespace
const t = useTranslations(isLogin ? 'login' : 'signup')
t('welcome') // → login.welcome, signup.welcome
// ✅ Ternary key
t(isActive ? 'active' : 'inactive') // → both keys detected
// ✅ Method variants
t.rich('richKey', { ... })
t.markup('markupKey')Marking Dynamic Keys as Used
For keys that can't be statically detected (variables, template literals), add a comment:
// i18n-used-keys: guide.previewCard.draft, guide.previewCard.published
const t = useTranslations('tour.previewCard')
// This variable-based usage would otherwise be missed
{t(status)} // status is 'draft' | 'published'The comment must use fully-qualified keys (including namespace).
Limitations
- Template literals:
t(\prefix.${variable}`)` won't be detected - Variable keys:
t(someVariable)won't be detected (usei18n-used-keyscomment) - Indirect references: Keys passed through multiple variables won't be traced
Always review the --dry-run output before pruning.
Best Practices
- Run
i18n:checkin CI to catch missing translations early - Run
i18n:unusedperiodically to find dead keys - Always use
--dry-runfirst before pruning - Add
i18n-used-keyscomments when using dynamic keys - Run
val type-checkafter pruning to verify no used keys were removed