translint
A missing translation is a UI bug. A renamed placeholder is a crash.
translint checks your locale files against a base file and flags exactly that: missing keys, stale extra keys, empty values, values that still look untranslated, and placeholder tokens that don't match between the base string and the translation - the one that actually takes an app down. One Python file, standard library only, zero dependencies.
Run it as a CLI by hand or in a pre-commit hook, as a GitHub Action that gates CI, or as an agent skill so Claude Code (or any agent using the open Agent Skills standard) checks its own i18n changes before handing a PR back to you.
Install
pip install git+https://github.com/munzzyy/translint
Or skip installing it, since it's one file with no dependencies:
curl -LO https://raw.githubusercontent.com/munzzyy/translint/main/translint.py
python translint.py --help
Usage
translint locales/ # scan a directory, base=en
translint locales/ --base fr # use fr.json as the reference instead
translint locales/en.json locales/de.json # check specific files
translint locales/ --json # machine-readable report
translint locales/ --strict # also fail on extra keys + untranslated
translint locales/ --allow-identical brand.name # suppress one heuristic false positive
--base defaults to en - the locale name (filename stem) every
other discovered file gets checked against. Exit code is 0 when everything's
clean, 1 when translint found something to fix, 2 if a path
couldn't be read or parsed at all - a bad-JSON error and a real lint finding never look the
same to a script.
See it catch something
This is real output, not a mockup. examples/locales/ in the repo ships one
base file (en.json) and two translations (fr.json,
de.json) with a handful of real, intentionally planted problems. Below is
translint run against them, copied verbatim - red categories fail the run by default, amber
ones are reported but only fail under --strict.
$translint examples/locales --base en
de (examples\locales\de.json):
empty values (1):
- checkout.success
possibly untranslated (1, heuristic):
- brand.name
fr (examples\locales\fr.json):
missing keys (1):
- nav.settings
extra keys (1):
- checkout.oldStep
placeholder mismatches (1):
- checkout.confirm: base has ['{amount}', '{last4}'], locale has ['{last4}', '{total}']
possibly untranslated (2, heuristic):
- brand.name
- errors.networkDown
$echo $?
1
translint exits 1 whenever there's something to fix. Exit 0
means every locale is clean; exit 2 means a path couldn't even be read or
parsed. Try it yourself against these same files: translint examples/locales --base en.
What it checks
-
Missing keys
A key that exists in the base locale but never made it into a translation.
-
Placeholder mismatches
{amount}became{total}- the bug that renders fine in every test that doesn't pass real interpolation data, and throws the moment one does. -
Empty values
A key that exists but was never filled in.
-
Extra keys
Left over from a rename. Reported, but doesn't fail the run unless you pass
--strict. -
Possibly untranslated
A heuristic: the value still reads byte-identical to the base after stripping placeholders, punctuation, and numbers. Allowlist real matches with
--allow-identicalor--do-not-translate. -
Three file formats
JSON (nested or flat dot-namespaced keys), gettext
.po/.pot, and Java.properties, auto-detected by extension. -
Five placeholder styles
{name},{{name}},%s/%d/%1$s,%(name)s, and${name}/$name, diffed as a multiset so a doubled placeholder doesn't slip through. -
Scriptable output
--jsonprints machine-readable results. Exit codes are just as script-friendly: 0 clean, 1 findings, 2 couldn't read or parse. -
Runs three ways
CLI by hand or in a pre-commit hook, a GitHub Action that gates CI, or a Claude Code / Agent Skills skill, so a coding agent checks its own i18n changes before handing back a PR.
What it doesn't do
- No YAML locale files. Parsing YAML safely needs a dependency, and translint is stdlib-only on purpose.
- The untranslated-value check is a heuristic, not a hard rule. It flags what looks untranslated; it doesn't prove anything.
- It doesn't translate anything, and it doesn't validate translation quality - just structure: keys, placeholders, non-empty-ness.
- Non-recursive directory scan. Point it at a directory and it checks the files directly inside, not subdirectories.