Systematic approach to internationalizing applications. Covers two scenarios: adding
multilingual support from scratch and auditing existing i18n for gaps. Built from real
production pain - the hardest part of i18n is not translation but finding every string
that needs it, and making sure translations read naturally in context rather than as
mechanical word-by-word output.
When to use
Adding multilingual support to an existing single-language app
Auditing a codebase for untranslated hardcoded strings
Checking an already-internationalized app for completeness or quality gaps
Setting up locale catalogs, providers, and translation infrastructure
Generating machine translations for new or changed source strings
Validating catalog completeness across locales
Adding new languages to an already-internationalized app
Translating standalone documents, README files, or prose - just ask the LLM directly
Reviewing code quality or style issues - use code-review or anti-slop
Building AI/LLM features that produce multilingual output at runtime - use ai-ml
Setting up backend API endpoints for locale handling - use backend-api
Writing tests for i18n behavior - use testing (though this skill includes validation)
AI Self-Check
AI tools consistently produce the same i18n mistakes. Before returning any generated i18n
code, catalogs, or translations, verify against this list:
Native orthography in catalogs: translated strings use proper Unicode characters
for the target language (umlauts, accents, cedillas, CJK characters, etc.). ASCII-only
rules from global or project config (CLAUDE.md, AGENTS.md, .cursorrules, etc.) apply to
source code and prose, NOT to locale catalogs. Writing "hinzugefuegt" (ASCII ae/oe/ue
substitution) instead of proper umlauts is a bug. Locale files are the one place where
native script is mandatory.
Every string category covered: checked all categories in the String Categories
table below, not just visible text. Toast notifications, validation messages, aria-labels,
placeholders, title attributes, alt text, loading states, conditional fragments, and error
messages are the most commonly missed
Source catalog is the type authority: types for message keys derive from the source
locale (usually English), not from a union of all locales
Placeholders preserved: every {0}, {name}, {{var}}, %s, %d in source
strings appears identically in translated strings - same count, same order, same syntax
Brand names protected: product names, service names, and proper nouns are preserved
exactly in all locales
No partial extraction: if auditing a file, every user-facing string in that file
is extracted - not just the obvious ones. Check JSX text content, attribute values, template
literals, and string arguments to UI functions
Fallback chain exists: missing keys fall back to the source locale, then to the
key itself - never to an empty string or a crash
Validation script created: a script or test exists that compares all locale catalogs
against the source for missing keys, extra keys, and empty values
Keys use dot notation: keys follow a consistent namespace.context.label pattern.
Keys describe what the text is for, not what it says
Voice consistency: translations within each language use the same register (formal
or informal) throughout. German "du" vs "Sie", French "tu" vs "vous", Spanish "tu" vs
"usted" - pick one per language and stick with it across the entire catalog
Context-aware translation: translations read naturally in the app's domain, not as
mechanical word-by-word output. UI labels, error messages, and toast notifications should
sound like a native speaker wrote them for that specific app
No library API hallucination: if using a library (i18next, next-intl, vue-i18n),
verify import paths, hook names, and configuration options against current docs
RTL/bidirectional text handled: layout direction set in HTML lang/dir attributes,
no LTR-only CSS assumptions
Catalog files parseable: JSON/YAML validates without syntax errors, no trailing
commas or unquoted keys
Locale detection complete: browser navigator.language, Accept-Language header,
or user preference stored and respected
Current source checked: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
Hidden state identified: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
Verification is real: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
Routing overlap checked: overlapping skills, trigger terms, and "When NOT to use" boundaries are checked before returning guidance
Spec claims verified: claims about tool behavior, output contracts, or repo conventions are checked against current docs, scripts, or skill files
Locale data checked: ICU message syntax, plural categories, and CLDR assumptions match the target locales
Fallback behavior tested: missing keys, pseudo-locales, RTL, and long strings are exercised
Performance
Load locale bundles per route or language instead of shipping every catalog to every user.
Cache compiled ICU messages where the framework supports it.
Detect hardcoded strings with static scans before manual review.
Best Practices
Keep source strings stable and meaningful; do not use English copy as an implicit key if text changes often.
Use translators' notes for placeholders, gender, tone, and domain-specific terms.
Never concatenate translated fragments where grammar can change by language.
Workflow
Entry points (always read the AI Self-Check above first, regardless of entry point):
Adding i18n from scratch? Start at Step 1.
Already have i18n, checking completeness? Start at Step 1 (assess), then Step 3 (audit).
Just need translations for new keys? Jump to Step 4.
Just validating catalogs? Jump to Step 5.
Step 1: Assess current state
Before touching code, understand what exists:
Check for existing i18n setup - look for i18n libraries in package.json (or
equivalent), locale/translation directories, i18n config files, and translation function
usage (t(), $t(), useTranslations, FormattedMessage, etc.)
Identify the framework - React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, or
vanilla JS/TS. This determines the provider pattern and available libraries.
Locate catalog files - find where locale/translation files live. Common locations:
src/locales/, src/i18n/messages/, public/locales/, or inline <i18n> blocks.
Note the format (JSON, YAML, TS objects) and identify the source locale.
Count the scope - estimate how many files contain user-facing strings. Adapt
the file extension to your framework. The JSX-text regex alone undercounts by a
lot (misses attributes, toasts, errors) - combine with attribute and toast patterns:
Check for partial i18n - the worst state is partially translated: some strings use
t(), others are hardcoded. Map which areas are done and which are not.
If i18n exists, check quality - run the validation patterns from Step 5 to find
missing keys, inconsistent voice, or stale translations.
Step 2: Set up i18n infrastructure
If no i18n system exists, set one up. If one exists, skip to Step 3.
Architecture decisions (decide these first, not mid-implementation):
Decision
Options
Guidance
Catalog format
JSON, YAML, TS objects
TS objects give type safety without tooling. JSON works with most libraries
Key structure
flat, nested, dot-notation
Dot-notation (auth.signIn) balances readability and grep-ability
Interpolation
positional {0}, named {name}, ICU
Named for readability. Positional is simpler for machine translation
Pluralization
separate keys, ICU MessageFormat
Separate keys work for 2-form languages (English, German). Use ICU for Slavic and Arabic: Russian and Polish each have 4 CLDR plural categories (one/few/many/other), Arabic has 6. Picking "singular/plural" keys up front will force a rewrite later
Locale detection
browser, URL path, cookie, header
Browser detection for first visit, persisted preference after
Fallback
source locale, then key
Always. Never return empty or crash on missing key
Voice register
formal, informal
Decide per target language upfront. Document the choice
Source catalog - English (or source language) with all keys
Provider/context - framework-appropriate state for active locale
Translation function - t(key) lookup with fallback chain
Locale persistence - local storage for pre-auth, database for authenticated users
Validation script - compares all catalogs against source (see Step 5)
Language switcher - a visible UI element for changing locale. Place it where users
can find it without digging through settings (e.g., below login form when unauthenticated,
in the app bar when authenticated). Show native language names (Deutsch, not German).
RTL awareness: if any target locale uses right-to-left script (Arabic, Hebrew, Persian,
Urdu), the UI needs dir="rtl" support, CSS logical properties (margin-inline-start
instead of margin-left), and bidirectional text handling. RTL is a layout concern beyond
catalog setup - plan for it in the infrastructure, not as an afterthought.
Quick setup for React (react-i18next) - the most common case:
// 1. Install: npm install react-i18next i18next
// 2. src/i18n.ts - init once, import before rendering
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import en from './locales/en.json'
i18n.use(initReactI18next).init({ lng: 'en', fallbackLng: 'en',
resources: { en: { translation: en } } })
export default i18n
// 3. src/main.tsx - import side-effect before <App />
import './i18n'
// 4. In any component
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
return <button>{t('auth.signIn')}</button>
Quick setup for Vue (vue-i18n):
// 1. Install: npm install vue-i18n
// 2. src/i18n.ts
import { createI18n } from 'vue-i18n'
import en from './locales/en.json'
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en', messages: { en } })
// 3. In any component
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// <button>{{ t('auth.signIn') }}</button>
For Next.js use next-intl; for others see references/audit-patterns.md.
Read references/audit-patterns.md for framework-specific patterns on where strings hide.
Step 3: Audit and extract strings
This is where i18n projects fail. Strings hide in places that are easy to overlook.
The rule: work file by file, not category by category. Open a file, extract ALL strings
from ALL categories before moving on. The "one more pass" loop (toast this time, then
aria-labels, then placeholders...) is the #1 i18n time sink.
Audit process per file:
Read the file completely.
Walk through the String Categories table below. For each category, check whether that
file has any instances.
Add keys to the source catalog. Use dot-notation: namespace.context.label.
Replace hardcoded strings with t() calls (or the project's equivalent).
Convert template literals: `${name} connected` becomes
t('service.connected').replace('{0}', name) or the library's interpolation syntax.
String categories quick reference:
Category
Example
Why it gets missed
Toast/notification
toast.success('Saved')
In event handlers, not JSX
Validation error
setError('Name is required')
Buried in form logic
Placeholder
placeholder="Search..."
Attribute, not text content
defaultValue
defaultValue="Search..."
Functionally same as placeholder, different attr
aria-label
aria-label="Close menu"
Not visible on screen
title attribute
title="Click to expand"
Tooltip, invisible by default
alt text
alt="User avatar"
Image fallback, often ignored
Button label
<button>Submit</button>
Obvious but still missed in edge components
Loading state
'Loading...'
Short, feels like a constant
Conditional fragment
'(not configured)'
Not a full sentence
Confirm dialog
confirm('Delete this?')
Browser API, not component
Error boundary
'Something went wrong'
Rare path
Empty state
'No results found'
Only visible when data is absent
Select/option label
<option>Choose one</option>
Inside form elements
Table header
<th>Status</th>
Structural, feels permanent
Document title
document.title = 'Settings'
Not in the component tree
Server response text
{ error: 'Invalid email' }
Lives in API layer, not frontend
See references/audit-patterns.md for false positives to skip (console.log, CSS classes,
data attributes, route patterns, etc.).
Read references/audit-patterns.md for grep commands that catch each category.
Step 4: Generate translations
After the source catalog is complete and all strings use t() calls:
Translation quality matters more than speed. Machine translations that read like
mechanical word-by-word output are worse than no translation - they make the app feel
broken in every language. Read references/translation-quality.md for the full approach.
Key principles:
Provide app context in the translation prompt. Include the app's domain, what it does,
and who uses it. "A music discovery app" produces different translations than "an enterprise
billing system."
Specify voice register per language. Decide formal vs informal for each target language
before translating. Document this decision.
Protect brand names - maintain a list of terms that must not be translated (product
names, service names, technical identifiers).
Translate in batches, validate between batches. One locale at a time. Don't translate
all 15 locales then discover a systematic error.
Validate every batch before committing (see Step 5).
Use the full prompt template from references/translation-quality.md - it includes
app context, voice register, protected terms, preservation rules, and explicit "do not"
constraints. Do not improvise a shorter prompt.
Step 5: Validate catalogs
Validation is the safety net. Set it up early, run it often.
Completeness checks:
Check
What it catches
Missing keys
Keys in source but not in target
Extra keys
Stale translations for removed source keys
Empty values
Key exists but value is blank
Placeholder mismatch
Source has {0} but translation doesn't
Protected term mutation
Brand name was translated when it shouldn't be
Line break mismatch
Formatting differs from source
Validation script pattern:
const sourceKeys = Object.keys(sourceCatalog)
for (const locale of supportedLocales) {
const catalog = getCatalog(locale)
const missing = sourceKeys.filter(k => !(k in catalog))
const extra = Object.keys(catalog).filter(k => !sourceKeys.includes(k))
const empty = sourceKeys.filter(k => catalog[k]?.trim() === '')
// Fail if any issues
}
For placeholder and protected term validation, see references/translation-quality.md.
When to run:
After every translation generation
In CI (pre-merge) - prevents drift from day one
Before releases
Quality audit for existing i18n:
When checking an already-internationalized app, go beyond completeness:
Run the validation script for missing/extra/empty keys
Spot-check 10-20 keys across each locale for voice consistency (formal vs informal mixing)
Check interpolated strings render correctly with real data
Verify Intl formatting uses the locale variable, not hardcoded 'en-US':
Re-run the audit grep patterns from Step 3 to catch newly hardcoded strings
Step 6: Ongoing maintenance
Once i18n is set up, the workflow for new features is:
Add source-language strings to the source catalog
Use t() in new code - never hardcoded strings
Run the validation script to see which locales need updates
Generate translations for the new keys
Validate and commit
Preventing drift:
Add validation to CI so PRs with missing translations fail
Periodically re-audit with grep patterns to catch regressions
When reviewing PRs, check new UI text uses t(), not literals
Reference Files
references/audit-patterns.md - grep commands for finding hardcoded strings in React, Vue,
Svelte, Angular, and vanilla JS/TS. Organized by string category. Use during Step 3.
references/translation-quality.md - context-aware translation prompting, voice consistency
rules, protected term handling, and validation script implementations. Use during Steps 4-5.
Output Contract
See skills/_shared/output-contract.md for the full contract.
Skill name: LOCALIZE
Deliverable bucket:audits
Mode: conditional. When invoked to audit, review, or improve existing i18n (hardcoded-string audit, catalog completeness check, translation quality review), emit the full contract - boxed inline header, body summary inline plus per-finding detail in the deliverable file, boxed conclusion, conclusion table - and write the deliverable to docs/local/audits/localize/<YYYY-MM-DD>-<slug>.md. When invoked to set up i18n from scratch, generate translations for new keys, or answer a question, respond freely without the contract.
Severity scale:P0 | P1 | P2 | P3 | info (see shared contract; only used in audit/review mode).
Related Skills
testing - write tests for i18n behavior (locale switching, fallbacks, formatting).
This skill guides what to build; testing guides how to verify it.
code-review - catches hardcoded strings during review. This skill catches them
systematically via audit.
backend-api - if the API serves user-facing text, locale resolution and content
negotiation belong in the API layer. This skill handles the broader i18n setup.
ai-ml - when AI-generated text needs to respond in the user's language at runtime,
the AI locale context is an i18n concern. The response quality is an ai-ml concern.
Rules
Audit file by file, not category by category. Extract ALL strings from a file before
moving to the next. The "one more pass" loop is the #1 i18n time sink.
Source locale is the type authority. All message key types derive from the source
catalog. Other locales conform to it, not the other way around.
Validate before committing translations. Never commit machine-translated catalogs
without running placeholder and completeness checks.
Never return empty strings for missing keys. The fallback chain must end at the
source locale value or the key itself - never empty, null, or a crash.
Preserve brand names exactly. Product names, service names, and proper nouns must
match the source. Maintain a protected terms list per project.
Maintain voice consistency per language. Pick formal or informal register for each
target language and enforce it across the entire catalog. Mixing registers makes the
app feel incoherent.
Translate for the app's context, not word-by-word. UI strings should read like a
native speaker wrote them for this specific application.
Add validation to CI early. A script that fails on missing keys prevents drift from
day one. Don't defer this.
Don't translate what shouldn't be translated. Code identifiers, CSS classes, data
attributes, technical log messages, and developer-facing strings stay in the source
language.
Use native orthography in locale catalogs. Translated strings must use proper
Unicode characters for the target language - umlauts, accents, cedillas, CJK characters,
full-width punctuation, etc. ASCII-only rules from global or project config apply to
source code and prose, not to translation output. Writing "hinzugefuegt" instead of
proper German umlauts, or "nino" instead of Spanish n-with-tilde, is a translation
bug, not a style choice.
1---2name: localize-23description: · Audit app i18n/l10n: hardcoded strings, locale catalogs, translations, fallback gaps. Triggers: 'i18n', 'internationalization', 'localization', 'locale', 'hardcoded strings', 'next-intl'.4license: MIT5---67# Localize: App Internationalization Workflow
89**Target versions (May 2026):** react-i18next 17.x, vue-i18n 11.x, next-intl 4.x, i18next 26.x
1011Systematic approach to internationalizing applications. Covers two scenarios: adding
12multilingual support from scratch and auditing existing i18n for gaps. Built from real
13production pain - the hardest part of i18n is not translation but finding every string
14that needs it, and making sure translations read naturally in context rather than as
15mechanical word-by-word output.
1617## When to use
1819- Adding multilingual support to an existing single-language app
20- Auditing a codebase for untranslated hardcoded strings
21- Checking an already-internationalized app for completeness or quality gaps
22- Setting up locale catalogs, providers, and translation infrastructure
23- Generating machine translations for new or changed source strings
24- Validating catalog completeness across locales
25- Adding new languages to an already-internationalized app
26- Reviewing translation quality (voice consistency, domain accuracy, placeholder integrity)
2728## When NOT to use
2930- Translating standalone documents, README files, or prose - just ask the LLM directly
31- Reviewing code quality or style issues - use **code-review** or **anti-slop**
32- Building AI/LLM features that produce multilingual output at runtime - use **ai-ml**
33- Setting up backend API endpoints for locale handling - use **backend-api**
34- Writing tests for i18n behavior - use **testing** (though this skill includes validation)
3536---
3738## AI Self-Check
3940AI tools consistently produce the same i18n mistakes. **Before returning any generated i18n
41code, catalogs, or translations, verify against this list:**
4243- [ ] **Native orthography in catalogs**: translated strings use proper Unicode characters
44 for the target language (umlauts, accents, cedillas, CJK characters, etc.). ASCII-only
45 rules from global or project config (CLAUDE.md, AGENTS.md, .cursorrules, etc.) apply to
46 source code and prose, NOT to locale catalogs. Writing "hinzugefuegt" (ASCII ae/oe/ue
47 substitution) instead of proper umlauts is a bug. Locale files are the one place where
48 native script is mandatory.
49- [ ] **Every string category covered**: checked all categories in the String Categories
50 table below, not just visible text. Toast notifications, validation messages, aria-labels,
51 placeholders, title attributes, alt text, loading states, conditional fragments, and error
52 messages are the most commonly missed
53- [ ] **Source catalog is the type authority**: types for message keys derive from the source
54 locale (usually English), not from a union of all locales
55- [ ] **Placeholders preserved**: every `{0}`, `{name}`, `{{var}}`, `%s`, `%d` in source
56 strings appears identically in translated strings - same count, same order, same syntax
57- [ ] **Brand names protected**: product names, service names, and proper nouns are preserved
58 exactly in all locales
59- [ ] **No partial extraction**: if auditing a file, every user-facing string in that file
60 is extracted - not just the obvious ones. Check JSX text content, attribute values, template
61 literals, and string arguments to UI functions
62- [ ] **Fallback chain exists**: missing keys fall back to the source locale, then to the
63 key itself - never to an empty string or a crash
64- [ ] **Validation script created**: a script or test exists that compares all locale catalogs
65 against the source for missing keys, extra keys, and empty values
66- [ ] **Keys use dot notation**: keys follow a consistent `namespace.context.label` pattern.
67 Keys describe what the text is for, not what it says
68- [ ] **Voice consistency**: translations within each language use the same register (formal
69 or informal) throughout. German "du" vs "Sie", French "tu" vs "vous", Spanish "tu" vs
70 "usted" - pick one per language and stick with it across the entire catalog
71- [ ] **Context-aware translation**: translations read naturally in the app's domain, not as
72 mechanical word-by-word output. UI labels, error messages, and toast notifications should
73 sound like a native speaker wrote them for that specific app
74- [ ] **No library API hallucination**: if using a library (i18next, next-intl, vue-i18n),
75 verify import paths, hook names, and configuration options against current docs
76- [ ] **RTL/bidirectional text handled**: layout direction set in HTML lang/dir attributes,
77 no LTR-only CSS assumptions
78- [ ] **Catalog files parseable**: JSON/YAML validates without syntax errors, no trailing
79 commas or unquoted keys
80- [ ] **Locale detection complete**: browser navigator.language, Accept-Language header,
81 or user preference stored and respected
82- [ ] **Current source checked**: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
83- [ ] **Hidden state identified**: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
84- [ ] **Verification is real**: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
85- [ ] **Routing overlap checked**: overlapping skills, trigger terms, and "When NOT to use" boundaries are checked before returning guidance
86- [ ] **Spec claims verified**: claims about tool behavior, output contracts, or repo conventions are checked against current docs, scripts, or skill files
87- [ ] **Locale data checked**: ICU message syntax, plural categories, and CLDR assumptions match the target locales
88- [ ] **Fallback behavior tested**: missing keys, pseudo-locales, RTL, and long strings are exercised
8990---
9192## Performance
9394- Load locale bundles per route or language instead of shipping every catalog to every user.
95- Cache compiled ICU messages where the framework supports it.
96- Detect hardcoded strings with static scans before manual review.
979899---
100101## Best Practices
102103- Keep source strings stable and meaningful; do not use English copy as an implicit key if text changes often.
104- Use translators' notes for placeholders, gender, tone, and domain-specific terms.
105- Never concatenate translated fragments where grammar can change by language.
106107108## Workflow
109110**Entry points** (always read the AI Self-Check above first, regardless of entry point):
111- Adding i18n from scratch? Start at Step 1.
112- Already have i18n, checking completeness? Start at Step 1 (assess), then Step 3 (audit).
113- Just need translations for new keys? Jump to Step 4.
114- Just validating catalogs? Jump to Step 5.
115116### Step 1: Assess current state
117118Before touching code, understand what exists:
1191201. **Check for existing i18n setup** - look for i18n libraries in `package.json` (or
121 equivalent), locale/translation directories, i18n config files, and translation function
122 usage (`t()`, `$t()`, `useTranslations`, `FormattedMessage`, etc.)
1232. **Identify the framework** - React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, or
124 vanilla JS/TS. This determines the provider pattern and available libraries.
1253. **Locate catalog files** - find where locale/translation files live. Common locations:
126 `src/locales/`, `src/i18n/messages/`, `public/locales/`, or inline `<i18n>` blocks.
127 Note the format (JSON, YAML, TS objects) and identify the source locale.
1284. **Count the scope** - estimate how many files contain user-facing strings. Adapt
129 the file extension to your framework. The JSX-text regex alone undercounts by a
130 lot (misses attributes, toasts, errors) - combine with attribute and toast patterns:
131 ```bash
132 # React/JSX: text content + attribute values (placeholder, title, alt, aria-label)
133 grep -rlE '>[A-Z][a-z]|(placeholder|title|alt|aria-label)="[A-Z]' \
134 --include='*.tsx' --include='*.jsx' src/ | wc -l
135 # Toast/validation strings hide in logic (not JSX). Check separately.
136 grep -rlE 'toast\.(success|error|info|warn)|setError\(' \
137 --include='*.ts' --include='*.tsx' src/ | wc -l
138 # Vue: *.vue | Svelte: *.svelte | Angular: *.html + *.ts (see references/audit-patterns.md)
139 ```
1405. **Check for partial i18n** - the worst state is partially translated: some strings use
141 `t()`, others are hardcoded. Map which areas are done and which are not.
1426. **If i18n exists, check quality** - run the validation patterns from Step 5 to find
143 missing keys, inconsistent voice, or stale translations.
144145### Step 2: Set up i18n infrastructure
146147If no i18n system exists, set one up. If one exists, skip to Step 3.
148149**Architecture decisions** (decide these first, not mid-implementation):
150151| Decision | Options | Guidance |
152|----------|---------|----------|
153| Catalog format | JSON, YAML, TS objects | TS objects give type safety without tooling. JSON works with most libraries |
154| Key structure | flat, nested, dot-notation | Dot-notation (`auth.signIn`) balances readability and grep-ability |
155| Interpolation | positional `{0}`, named `{name}`, ICU | Named for readability. Positional is simpler for machine translation |
156| Pluralization | separate keys, ICU MessageFormat | Separate keys work for 2-form languages (English, German). Use ICU for Slavic and Arabic: Russian and Polish each have 4 CLDR plural categories (one/few/many/other), Arabic has 6. Picking "singular/plural" keys up front will force a rewrite later |
157| Locale detection | browser, URL path, cookie, header | Browser detection for first visit, persisted preference after |
158| Fallback | source locale, then key | Always. Never return empty or crash on missing key |
159| Voice register | formal, informal | Decide per target language upfront. Document the choice |
160161**Components to create:**
1621631. **Locale registry** - supported locales, aliases, normalization, native display names
1642. **Source catalog** - English (or source language) with all keys
1653. **Provider/context** - framework-appropriate state for active locale
1664. **Translation function** - `t(key)` lookup with fallback chain
1675. **Locale persistence** - local storage for pre-auth, database for authenticated users
1686. **Validation script** - compares all catalogs against source (see Step 5)
1697. **Language switcher** - a visible UI element for changing locale. Place it where users
170 can find it without digging through settings (e.g., below login form when unauthenticated,
171 in the app bar when authenticated). Show native language names (Deutsch, not German).
172173**RTL awareness:** if any target locale uses right-to-left script (Arabic, Hebrew, Persian,
174Urdu), the UI needs `dir="rtl"` support, CSS logical properties (`margin-inline-start`
175instead of `margin-left`), and bidirectional text handling. RTL is a layout concern beyond
176catalog setup - plan for it in the infrastructure, not as an afterthought.
177178**Quick setup for React (react-i18next)** - the most common case:
179180```tsx
181// 1. Install: npm install react-i18next i18next
182// 2. src/i18n.ts - init once, import before rendering
183import i18n from 'i18next'
184import { initReactI18next } from 'react-i18next'
185import en from './locales/en.json'
186i18n.use(initReactI18next).init({ lng: 'en', fallbackLng: 'en',
187 resources: { en: { translation: en } } })
188export default i18n
189190// 3. src/main.tsx - import side-effect before <App />
191import './i18n'
192193// 4. In any component
194import { useTranslation } from 'react-i18next'
195const { t } = useTranslation()
196return <button>{t('auth.signIn')}</button>
197```
198199**Quick setup for Vue (vue-i18n):**
200201```ts
202// 1. Install: npm install vue-i18n
203// 2. src/i18n.ts
204import { createI18n } from 'vue-i18n'
205import en from './locales/en.json'
206export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en', messages: { en } })
207208// 3. In any component
209import { useI18n } from 'vue-i18n'
210const { t } = useI18n()
211// <button>{{ t('auth.signIn') }}</button>
212```
213214For Next.js use `next-intl`; for others see `references/audit-patterns.md`.
215216Read `references/audit-patterns.md` for framework-specific patterns on where strings hide.
217218### Step 3: Audit and extract strings
219220This is where i18n projects fail. Strings hide in places that are easy to overlook.
221222**The rule: work file by file, not category by category.** Open a file, extract ALL strings
223from ALL categories before moving on. The "one more pass" loop (toast this time, then
224aria-labels, then placeholders...) is the #1 i18n time sink.
225226**Audit process per file:**
2272281. Read the file completely.
2292. Walk through the String Categories table below. For each category, check whether that
230 file has any instances.
2313. Add keys to the source catalog. Use dot-notation: `namespace.context.label`.
2324. Replace hardcoded strings with `t()` calls (or the project's equivalent).
2335. Convert template literals: `` `${name} connected` `` becomes
234 `t('service.connected').replace('{0}', name)` or the library's interpolation syntax.
235236**String categories quick reference:**
237238| Category | Example | Why it gets missed |
239|----------|---------|-------------------|
240| Toast/notification | `toast.success('Saved')` | In event handlers, not JSX |
241| Validation error | `setError('Name is required')` | Buried in form logic |
242| Placeholder | `placeholder="Search..."` | Attribute, not text content |
243| defaultValue | `defaultValue="Search..."` | Functionally same as placeholder, different attr |
244| aria-label | `aria-label="Close menu"` | Not visible on screen |
245| title attribute | `title="Click to expand"` | Tooltip, invisible by default |
246| alt text | `alt="User avatar"` | Image fallback, often ignored |
247| Button label | `<button>Submit</button>` | Obvious but still missed in edge components |
248| Loading state | `'Loading...'` | Short, feels like a constant |
249| Conditional fragment | `'(not configured)'` | Not a full sentence |
250| Confirm dialog | `confirm('Delete this?')` | Browser API, not component |
251| Error boundary | `'Something went wrong'` | Rare path |
252| Empty state | `'No results found'` | Only visible when data is absent |
253| Select/option label | `<option>Choose one</option>` | Inside form elements |
254| Table header | `<th>Status</th>` | Structural, feels permanent |
255| Document title | `document.title = 'Settings'` | Not in the component tree |
256| Server response text | `{ error: 'Invalid email' }` | Lives in API layer, not frontend |
257258See `references/audit-patterns.md` for false positives to skip (console.log, CSS classes,
259data attributes, route patterns, etc.).
260261Read `references/audit-patterns.md` for grep commands that catch each category.
262263### Step 4: Generate translations
264265After the source catalog is complete and all strings use `t()` calls:
266267**Translation quality matters more than speed.** Machine translations that read like
268mechanical word-by-word output are worse than no translation - they make the app feel
269broken in every language. Read `references/translation-quality.md` for the full approach.
270271**Key principles:**
2722731. **Provide app context** in the translation prompt. Include the app's domain, what it does,
274 and who uses it. "A music discovery app" produces different translations than "an enterprise
275 billing system."
2762. **Specify voice register** per language. Decide formal vs informal for each target language
277 before translating. Document this decision.
2783. **Protect brand names** - maintain a list of terms that must not be translated (product
279 names, service names, technical identifiers).
2804. **Translate in batches, validate between batches.** One locale at a time. Don't translate
281 all 15 locales then discover a systematic error.
2825. **Validate every batch** before committing (see Step 5).
283284Use the full prompt template from `references/translation-quality.md` - it includes
285app context, voice register, protected terms, preservation rules, and explicit "do not"
286constraints. Do not improvise a shorter prompt.
287288### Step 5: Validate catalogs
289290Validation is the safety net. Set it up early, run it often.
291292**Completeness checks:**
293294| Check | What it catches |
295|-------|----------------|
296| Missing keys | Keys in source but not in target |
297| Extra keys | Stale translations for removed source keys |
298| Empty values | Key exists but value is blank |
299| Placeholder mismatch | Source has `{0}` but translation doesn't |
300| Protected term mutation | Brand name was translated when it shouldn't be |
301| Line break mismatch | Formatting differs from source |
302303**Validation script pattern:**
304305```typescript
306const sourceKeys = Object.keys(sourceCatalog)
307for (const locale of supportedLocales) {
308 const catalog = getCatalog(locale)
309 const missing = sourceKeys.filter(k => !(k in catalog))
310 const extra = Object.keys(catalog).filter(k => !sourceKeys.includes(k))
311 const empty = sourceKeys.filter(k => catalog[k]?.trim() === '')
312 // Fail if any issues
313}
314```
315316For placeholder and protected term validation, see `references/translation-quality.md`.
317318**When to run:**
319- After every translation generation
320- In CI (pre-merge) - prevents drift from day one
321- Before releases
322323**Quality audit for existing i18n:**
324325When checking an already-internationalized app, go beyond completeness:
3263271. Run the validation script for missing/extra/empty keys
3282. Spot-check 10-20 keys across each locale for voice consistency (formal vs informal mixing)
3293. Check interpolated strings render correctly with real data
3304. Verify `Intl` formatting uses the locale variable, not hardcoded `'en-US'`:
331 ```bash
332 grep -rn "Intl\.\(DateTimeFormat\|NumberFormat\)" src/ | grep "'en"
333 ```
3345. Re-run the audit grep patterns from Step 3 to catch newly hardcoded strings
335336### Step 6: Ongoing maintenance
337338Once i18n is set up, the workflow for new features is:
3393401. Add source-language strings to the source catalog
3412. Use `t()` in new code - never hardcoded strings
3423. Run the validation script to see which locales need updates
3434. Generate translations for the new keys
3445. Validate and commit
345346**Preventing drift:**
347- Add validation to CI so PRs with missing translations fail
348- Periodically re-audit with grep patterns to catch regressions
349- When reviewing PRs, check new UI text uses `t()`, not literals
350351---
352353## Reference Files
354355- `references/audit-patterns.md` - grep commands for finding hardcoded strings in React, Vue,
356 Svelte, Angular, and vanilla JS/TS. Organized by string category. Use during Step 3.
357- `references/translation-quality.md` - context-aware translation prompting, voice consistency
358 rules, protected term handling, and validation script implementations. Use during Steps 4-5.
359360## Output Contract
361362See `skills/_shared/output-contract.md` for the full contract.
363364- **Skill name:** LOCALIZE
365- **Deliverable bucket:** `audits`
366- **Mode:** conditional. When invoked to **audit, review, or improve** existing i18n (hardcoded-string audit, catalog completeness check, translation quality review), emit the full contract - boxed inline header, body summary inline plus per-finding detail in the deliverable file, boxed conclusion, conclusion table - and write the deliverable to `docs/local/audits/localize/<YYYY-MM-DD>-<slug>.md`. When invoked to **set up i18n from scratch, generate translations for new keys, or answer a question**, respond freely without the contract.
367- **Deliverable path:** `docs/local/audits/localize/<YYYY-MM-DD>-<slug>.md`
368- **Severity scale:** `P0 | P1 | P2 | P3 | info` (see shared contract; only used in audit/review mode).
369370## Related Skills
371372- **testing** - write tests for i18n behavior (locale switching, fallbacks, formatting).
373 This skill guides what to build; testing guides how to verify it.
374- **code-review** - catches hardcoded strings during review. This skill catches them
375 systematically via audit.
376- **backend-api** - if the API serves user-facing text, locale resolution and content
377 negotiation belong in the API layer. This skill handles the broader i18n setup.
378- **ai-ml** - when AI-generated text needs to respond in the user's language at runtime,
379 the AI locale context is an i18n concern. The response quality is an ai-ml concern.
380381## Rules
3823831. **Audit file by file, not category by category.** Extract ALL strings from a file before
384 moving to the next. The "one more pass" loop is the #1 i18n time sink.
3852. **Source locale is the type authority.** All message key types derive from the source
386 catalog. Other locales conform to it, not the other way around.
3873. **Validate before committing translations.** Never commit machine-translated catalogs
388 without running placeholder and completeness checks.
3894. **Never return empty strings for missing keys.** The fallback chain must end at the
390 source locale value or the key itself - never empty, null, or a crash.
3915. **Preserve brand names exactly.** Product names, service names, and proper nouns must
392 match the source. Maintain a protected terms list per project.
3936. **Maintain voice consistency per language.** Pick formal or informal register for each
394 target language and enforce it across the entire catalog. Mixing registers makes the
395 app feel incoherent.
3967. **Translate for the app's context, not word-by-word.** UI strings should read like a
397 native speaker wrote them for this specific application.
3988. **Add validation to CI early.** A script that fails on missing keys prevents drift from
399 day one. Don't defer this.
4009. **Don't translate what shouldn't be translated.** Code identifiers, CSS classes, data
401 attributes, technical log messages, and developer-facing strings stay in the source
402 language.
40310. **Use native orthography in locale catalogs.** Translated strings must use proper
404 Unicode characters for the target language - umlauts, accents, cedillas, CJK characters,
405 full-width punctuation, etc. ASCII-only rules from global or project config apply to
406 source code and prose, not to translation output. Writing "hinzugefuegt" instead of
407 proper German umlauts, or "nino" instead of Spanish n-with-tilde, is a translation
408 bug, not a style choice.
Run npx skillmds add majiayu000/localize-2 in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
· Audit app i18n/l10n: hardcoded strings, locale catalogs, translations, fallback gaps. Triggers: 'i18n', 'internationalization', 'localization', 'locale', 'hardcoded strings', 'next-intl'. It is listed under Security on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: docs only. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free. This skill is licensed under MIT.
majiayu000 (@majiayu000) published this skill. Their other Agent Skills are listed on their SkillMD profile.