---
name: i18n-localization
description: Use when adding UI text, adding or shipping a language, reviewing translation files, or building i18n into an app. Checks missing keys, placeholders and CLDR plural forms with locale_sync.py, finds hardcoded strings with hardcoded_strings.py, and builds a pseudo-locale with pseudo_localize.py.
argument-hint: "[locales dir | source dir | release check]"
license: MIT
metadata:
version: 1.0.0
build_pattern: "Three deterministic stdlib scripts (translation-file checker with the CLDR 49 plural table embedded, hardcoded-string scanner for React/Vue/Svelte/HTML, pseudo-locale generator) with blocking exit codes; rules cited to Unicode CLDR and UTS #35, ICU MessageFormat, ECMA-402/MDN Intl, i18next, Android and gettext docs, W3C Internationalization and WCAG 3.1"
distinct_from: "a11y-audit (WCAG in source; this checks the translation layer and lang/dir only as they affect i18n); email-template-builder (localizes one email template); marketing localization and transcreation skills (copy, not code)"
---
# i18n Localization
> **Portability:** 3 stdlib Python scripts. No network, no translation service.
> They check and generate files; they never translate text or call an API.
A release with a new language usually breaks in the same places: keys added in
English and never synced, a placeholder renamed by a translator, Russian or
Arabic files that copied English's two plural forms, UI text that never went
through `t()`, and dates formatted in the server's locale. This skill makes each
of those a check that fails before the release, not a bug report after it.
## Workflow
```bash
# 1 - Translation files: keys, placeholders, ICU syntax, CLDR plural forms
python scripts/locale_sync.py locales/ --source-locale en
# 2 - Code: user-facing text that bypasses t(), concatenation, locale-less formatting
python scripts/hardcoded_strings.py src/
# 3 - Layout: generate a pseudo-locale and click through the app in it
python scripts/pseudo_localize.py locales/en/common.json -o locales/en-XA/common.json
python scripts/pseudo_localize.py locales/en/common.json --bidi -o locales/ar-XB/common.json
```
**Step 1** reads JSON/ARB (i18next or ICU), `.properties`, Apple `.strings`,
Android `strings.xml` and gettext `.po`, pairs each target with the source file of
the same namespace, and blocks on missing or empty keys, changed placeholders,
ICU syntax errors, plural/select without `other`, a `zero` branch that can never
match, and plural categories the target locale needs (ru `few`/`many`, ar six
forms), using the embedded CLDR 49 table. Untranslated values, orphan keys,
fuzzy entries and markup drift are warnings.
**Step 2** blocks on literal text between tags and literal user-facing
attributes (`placeholder`, `alt`, `aria-label`, `title`...), and warns on
literals rendered from `{cond ? 'Saving...' : ...}`, `t('a') + x` concatenation,
`toLocaleString()` with no locale, `'$' + amount` and hardcoded date patterns.
It is a text heuristic: read each finding. Use `--warn-only` to adopt it on a
legacy codebase, `i18n-ignore` on a line, and `.i18n-allow` for brand names.
**Step 3** accents, pads (by translation-growth rates) and brackets every string
while keeping placeholders and ICU syntax intact. Plain English on screen is a
hardcoded string; a cut-off `[[...` is truncation; `[[a]] [[b]]` is concatenation.
## Hard rules
- **Never concatenate translated fragments.** One message per sentence, with named placeholders and a plural branch.
- **Plural forms follow CLDR for the target locale**, not English. Never copy `_one`/`_other` into ru, pl, uk, cs, ar, he files and call them done.
- **`=0`, not `zero`,** for an empty-count message in ICU.
- **Pass the locale to every formatter.** Currency comes from the data, not the UI locale.
- **Placeholders are code.** A translator may move them, never rename or drop them; use positional `%1$s` where order can change.
- **Do not machine-translate to make the checker pass.** A missing translation falls back to the source language; a wrong one ships silently. Mark gaps for a translator instead.
## Scripts
| Script | Role | Exit codes |
|---|---|---|
| `scripts/locale_sync.py` | Translation files vs source locale: keys, placeholders, ICU, CLDR plurals, gettext | 0 · 2 warnings · 3 bad input · 4 blocked |
| `scripts/hardcoded_strings.py` | React/Vue/Svelte/HTML: untranslated text, concatenation, locale-less formatting | 0 · 2 · 3 · 4 |
| `scripts/pseudo_localize.py` | `en-XA` (or `--bidi` `ar-XB`) pseudo-locale from JSON/ARB/.properties | 0 · 3 |
All support `--help`, `--sample` and `--output json`.
## References and assets
- [`references/plurals_and_messages.md`](references/plurals_and_messages.md): CLDR categories per locale, ICU rules, library key shapes, placeholders (8 sources)
- [`references/string_routing_and_formatting.md`](references/string_routing_and_formatting.md): what to route through `t()`, Intl formatting, BCP 47 and fallback (7)
- [`references/layout_rtl_pseudo.md`](references/layout_rtl_pseudo.md): text expansion, pseudo-locales, RTL, release checklist (7)
- [`assets/i18n-gate.yml`](assets/i18n-gate.yml): CI job example
- [`assets/i18n-allow.example`](assets/i18n-allow.example): allowlist format