i18n Sync
Detect and fix drift between locale files so every language has the same key set.
When to use
- "some translations are missing"
- "add a new language / locale"
- "check my locale files are in sync"
- after a PR adds strings to only the base locale
Supported formats
| Format | Extensions | Notes |
|---|---|---|
| JSON | .json |
nested or flat/dotted keys |
| YAML | .yml .yaml |
incl. Rails-style files rooted at the locale code |
| gettext | .po .pot |
msgctxt, plurals, and multi-line strings preserved |
Layouts handled automatically: locales/en.json, locales/en/common.json, and
locales/fr/LC_MESSAGES/messages.po (a .pot template is treated as the base).
YAML requires PyYAML (pip install pyyaml). JSON and PO need nothing beyond stdlib.
Running the bundled script
scripts/sync_keys.py ships next to this SKILL.md. Your shell's working
directory is the user's project, not this skill's directory, so always
invoke it by absolute path — a bare scripts/sync_keys.py will not resolve:
SYNC="$(find ~/.claude ~/.config/claude -path '*i18n-sync*' -name sync_keys.py 2>/dev/null | head -1)"
python3 "$SYNC" <locale-dir> --base en
Set SYNC once, then reuse it for every invocation below.
If python3 is unavailable
The script is the reliable path, but it must not be a hard requirement. Check first, and only fall back if it is genuinely absent:
command -v python3 || echo "no python3"
If there is no python3, do the comparison by reading the files directly:
- Read the base locale file and each target locale file for the same
namespace (
en/common.jsonpairs withfr/common.json, never withfr/errors.json). - Flatten both to dotted key paths (
nav.home), then diff the key sets — reportmissing,extra, anduntranslatedexactly as the script does. - Check placeholders per key: every
{{name}}/%{name}/%s/%(name)s/<0>in the base string must appear in the translation. - Apply fixes with targeted edits — add only the absent keys, and never rewrite a line that already holds a translation.
Then tell the user plainly:
Python 3 isn't installed, so I compared the files by reading them. Install Python 3 for a faster and more reliable check on large locale sets.
Say it because it is true: reading by hand does not scale. Past a few hundred keys, or across many namespaces, quietly missing some is a real failure mode — if the files are large, report that the comparison may be incomplete rather than implying it was exhaustive.
Workflow
1. Discover the setup
Find the locale files before assuming anything:
find . -type d \( -name node_modules -o -name .git -o -name dist \) -prune -o \
\( -path '*locale*' -o -path '*i18n*' -o -path '*lang*' -o -path '*translation*' \) \
-type f \( -name '*.json' -o -name '*.yml' -o -name '*.yaml' -o -name '*.po' \) -print \
| head -50
Confirm with the user if ambiguous:
- Locale directory (e.g.
src/locales/,config/locales/) - Base locale: usually
en. It is the source of truth.
2. Diff the key sets
Run the bundled script — do not eyeball the files (if python3 is missing,
use the fallback above instead):
python3 "$SYNC" <locale-dir> --base en
Exit code is 1 when drift exists, so it works as a CI check. It reports per locale:
- missing — keys in base but not in this locale
- extra — keys here but not in base (usually stale)
- untranslated — empty, or identical to the base string
- placeholder mismatch — the translation lost or altered a
{{name}}/%{name}/%s/<0>placeholder. These are runtime bugs, not style issues: a missing interpolation renders a literal placeholder or crashes. - plural needs review — plural entries a human must complete
- orphan file — a locale file with no counterpart in the base locale. Reported, never auto-deleted: removing a whole file is the user's call.
3. Report before changing anything
Show the user a summary table (locale, missing, extra, untranslated). Never bulk-edit locale files without showing this first — translations are human-reviewed content, and overwriting a real translation is not recoverable from the file itself.
4. Apply the fix the user chooses
python3 "$SYNC" <locale-dir> --base en --fix-missing [--prune-extra]
--fix-missinginserts missing keys, preserving key order, indent width and trailing newline. Note: JSON is re-serialised one key per line, so a file written with compact inline objects ("nav": { "a": 1, "b": 2 }) comes back expanded. Content is unchanged; mention it if the user cares about the diff.--prune-extraremoves stale keys. Ask first — the key may still be referenced by code that only that locale reaches.- For a new language: copy the base file, keep all keys, and report which keys need human translation.
Per-format behaviour worth knowing:
- JSON/YAML — a missing key is filled with the base-language text as a visible placeholder, so the UI stays readable. Report these as untranslated.
- gettext — a missing entry is added with an empty
msgstr. That is the correct idiom: gettext falls back to themsgidautomatically, and an emptymsgstris what marks the string as untranslated to translator tooling. Never pre-fill amsgstrwith English — it makes untranslated strings indistinguishable from finished ones. - YAML — PyYAML cannot preserve comments. The script refuses to rewrite a
commented YAML file unless
--allow-comment-lossis passed. Prefer editing those by hand; translator comments carry context that is expensive to recreate.
5. Verify
Re-run the script; all locales should report 0 missing and 0 placeholder
mismatch. Then run the project's own i18n lint/test if one exists (check
package.json scripts, or i18n-tasks health on Rails). For gettext, validate
with msgfmt --check-format -o /dev/null <file>.po if gettext tools are present.
Rules
- Never machine-translate unless the user explicitly asks. Filling a key is not the same as translating it — always report what still needs a human.
- Preserve interpolation exactly:
{{name}},{count},%{name},%s,%(name)s,<0>. Flag any mismatch as an error, not a warning. - Preserve what matters: key ordering, indent width, trailing newline, and the locale root key in Rails YAML. Existing translations are never overwritten — only absent keys are added.
- Plural forms are language-specific — 1 form in Japanese, 2 in English, 3 in
Russian, 6 in Arabic. The script reads
npluralsfrom each PO file's own header. Never copy English's_one/_othershape into another language; flag those keys for human input instead. - RTL and length: do not "fix" a translation that looks wrong for being longer, shorter, or right-to-left. Only key structure is machine-checkable.