One source file, every language in sync. A CLI that fills missing translation keys,
re-translates edited ones and keeps ICU plurals valid, for JSON, ARB and YAML i18n projects.
curl -sSL langsync.nevil.dev | bashThe script installs with pipx and falls back to pip --user. Run the same
line again to update. To do it by hand:
pipx install git+https://github.com/dracu-lah/langsync-cli.gitRun langsync in your project. With no config yet, it finds your locale files (messages/,
locales/, public/locales/, src/i18n/, lib/l10n/ and the like) and offers to write a
langsync.json for you. Or write one by hand:
{ "source": "messages/en.json", "dir": "messages" }langsync --add fr,de,jaEvery key in en.json now exists in fr.json, de.json and ja.json, translated. Run langsync
again after editing the source and only the new and changed keys go out.
- Only what changed. A snapshot of every source string tells new keys from edited ones. Translations you did not touch stay as they are.
- ICU plurals stay valid. Only the words inside
plural,selectandselectordinalbranches are translated. Each rebuilt message is parsed before it is written, and a broken one is rejected. - Repairs broken strings. A target with broken ICU, often left by an older tool, is re-translated in place on the next run.
- Batched and parallel. Keys go out in batches and locales run side by side, with backoff when the API rate limits.
- Placeholders untouched.
{name},{{name}},%s,%{name},:name,$t(key),<b>and<0>tags, URLs and whitelisted brand names come back exactly as they went in. A translation that drops one is rejected, not written. Arrays and nested objects go element by element. - Placeholder checks. Existing translations whose variables or tags no longer match the source
are reported, so a renamed
{nmae}doesn't ship. - Your files stay yours. Indentation, line endings, BOM and key order are kept, unchanged files
are not rewritten, and every write is atomic.
"key_order": "source"mirrors the source order. - Any layout. One file per locale (
messages/fr.json), one folder per locale with namespaces (public/locales/fr/common.json), Flutter ARB (app_fr.arb) or YAML. - Errors with the fix. Every error and warning ends with the command that fixes it, ready to paste.
langsync # sync every locale file in "dir"
langsync --dry-run # show what would change, write nothing
langsync --add ja,pt-BR # create these locales and translate them
langsync -l fr-FR,de-DE # only these locales
langsync --update-changed # re-translate edited keys only, skip missing ones
langsync --rewrite # re-translate everything (replaces existing translations)
langsync --prune # remove keys the source no longer has
langsync --check # exit 1 on drift, for CI
langsync --stats # coverage per locale
langsync --json # machine-readable report on stdout
langsync -p deepl # use another provider for this run
langsync -v # print every key as it is processedA sync that leaves keys untranslated exits 1, so scripts notice. Re-running picks those keys up;
each locale tracks its own progress, so syncing only -l fr never hides an edit from de.
--check writes nothing and exits 1 when any locale has missing, changed or orphan keys, so a pull
request that edits en.json without syncing fails:
- run: pipx install git+https://github.com/dracu-lah/langsync-cli.git
- run: langsync --checkLangSync reads langsync.json or .langsync.json from the current directory or the nearest parent
(up to the git root), then ~/.langsync.json. Paths in it are relative to the file. Flags override it.
{
"source": "messages/en-GB.json",
"dir": "messages",
"locales": ["fr", "de", "pt-BR"],
"whitelist": ["MyBrand", "ProMode"],
"glossary": { "fr": { "Dashboard": "Tableau de bord" } },
"exclude": ["meta.*", "*.url"],
"key_order": "source"
}| Key | What it does |
|---|---|
source, dir |
The source file and the folder of <locale>.json files (.arb, .yaml work too). |
pattern, source_locale |
Instead of source/dir: a path with {locale} and optionally {ns}, e.g. "public/locales/{locale}/{ns}.json" with "source_locale": "en". |
locales |
Locales that should exist. Missing files are created on the next run. |
whitelist |
Terms that are never translated (case-sensitive). |
glossary |
Forced translations per language or locale. |
exclude |
Glob patterns of keys that are copied as-is, never translated. |
key_order |
keep (default), source or alpha. |
provider |
google (default, no key), deepl, libre, microsoft or mymemory. |
api_key_env, provider_url |
Env var holding the key (defaults: DEEPL_API_KEY, LIBRETRANSLATE_API_KEY, MICROSOFT_TRANSLATOR_KEY), and the LibreTranslate server URL. |
source_lang, language_map |
Override the source language, or map a locale to the provider's code ({"nb-NO": "no"}). |
batch_size, max_parallel_locales, max_workers_per_locale, delay_between_requests, retry_count |
Throughput and rate-limit tuning. |
The source language comes from the source file name (en-GB.json is English); name it something else
and set source_lang. YAML files need PyYAML: pipx inject langsync pyyaml.
The snapshot lives in <dir>/.langsync-state.json (for a pattern, in the folder above the locale
folders). Commit it, so the next person's run knows which keys you already translated. On a first run
inside a git repository, LangSync builds it from history.
pipenv install
pipenv run pip install -e .
pipenv run pytestCONTRIBUTING.md has the ground rules. The website lives in site/ and deploys to Cloudflare Pages.
- Bugs and ideas: Issues. Security reports follow SECURITY.md.
- Chip in: LangSync is free and MIT licensed. If it saves you time, sponsor it on GitHub or buy me a coffee. A star helps the next person find it.
MIT © dracu-lah. Translations go through Google Translate (or the provider you pick) via deep-translator.



