A deterministic CLI that lints your i18n/l10n translation files against a reference locale β missing keys, placeholder mismatches, incomplete plural forms (CLDR-aware), HTML tag drift and untranslated leftovers β with a coverage score, AβF grade and JSON/Markdown reports.
i18nlint is a zero-config command-line linter that compares all of your
locale files (en.json, ko.json, pl.json, β¦) against a reference locale and
reports every structural problem that breaks a translated UI β runs 100%
locally, no API key, no server.
Translation files are where localized apps quietly break:
- A missing key renders a blank label or falls back to English.
- A renamed variable β
{name}β{nom}β means the value is never interpolated at runtime; users see a literal{nom}. - Polish has four plural forms (
one/few/many/other); ship only two and the grammar is wrong for whole ranges of numbers. Arabic has six. - A dropped
<a>tag breaks a link; an injected tag can break layout. - A value left identical to English is an untranslated leftover.
These are mechanical, high-stakes, and easy to miss in review across 20 files.
They're also exactly the kind of cross-file, deterministic check an LLM gets
subtly wrong on large inputs β you want a repeatable tool you can gate a
deploy on. That's i18nlint.
- π Missing & orphan keys vs a reference locale.
- π§© Placeholder mismatch across
{x},{{x}},%s/%d,%(x)s,:x. - π’ CLDR-aware plural completeness β knows Polish needs
one/few/many/other, Korean needs onlyother, Arabic needs six. - π·οΈ HTML tag drift β flags lost or unexpected markup in translations.
- π³ Empty values and untranslated (same-as-source) detection.
- π Coverage score + AβF grade per locale and overall.
- π JSON & Markdown export, colored console output, CI gate exit codes.
- βοΈ Config file to set the reference, ignore keys, tune severities.
- π§± Works with nested (i18next/react-intl) or flat JSON. Zero network calls.
# run without installing
npx @didrod2539/i18nlint scan ./locales
# or install
npm install -g @didrod2539/i18nlint # global CLI (provides `i18nlint`)
npm install -D @didrod2539/i18nlint # project dev-dependency (for CI)Node β₯ 18. ESM + CJS + TypeScript types.
i18nlint scan ./localesfr 56/100 (F) 75% coverage Β· 6/8 keys Β· locales/fr.json
β Missing key "cta"
β Add "cta" to fr.
β Empty value for "cart.empty"
β Provide a translation for "cart.empty", or remove the key.
β Incomplete plural in "cart.items" β missing `many`
β Add the many plural branch(es) for fr.
βΉ "app.logout" looks untranslated (same as en)
pl 69/100 (D) 100% coverage Β· 8/8 keys Β· locales/pl.json
β Orphan key "legacy.old" not in reference
β Placeholder mismatch in "cart.total" (missing `amount`, unexpected `kwota`)
β Incomplete plural in "cart.items" β missing `few`, `many`
Overall 75/100 (C) ref en, 92% avg coverage, 4 error(s), 2 warning(s), 1 info
i18nlint scan [...paths] # lint locale files / directories
i18nlint report <input.json> # re-render a saved JSON report as Markdown
i18nlint init # scaffold i18nlint.config.json
i18nlint --help
i18nlint --versionscan options:
| Option | Description |
|---|---|
--config <file> |
Path to a config file (otherwise auto-detected) |
--reference <locale> |
Reference locale code (default: auto / en) |
--json <file> |
Write a JSON report |
--md <file> |
Write a Markdown report |
--min-coverage <n> |
Exit non-zero if avg coverage < n (CI gate) |
--max-errors <n> |
Exit non-zero if total errors > n (CI gate) |
--quiet |
Hide info-level issues in the console |
Locale codes are taken from file names (en.json β en, pt-BR.json β
pt-BR). Point scan at a directory and it finds every *.json recursively.
A full report for the bundled sample locales lives in
examples/sample-report.md and
examples/sample-report.json.
πΈ Screenshot / demo GIF placeholder:
./docs/screenshot.pngβ record the terminal runningnpx @didrod2539/i18nlint scan examples/locales.
Create i18nlint.config.json (or run i18nlint init):
{
"reference": "en",
"untranslated": "warning",
"minCoverage": 90,
"ignoreKeys": ["legacy.*"],
"allowUntranslated": ["app.title"],
"disableRules": [],
"ruleSeverity": { "extra-keys": "error" }
}| Field | Meaning |
|---|---|
reference |
Reference locale code, or null to auto-detect (en, else most keys) |
untranslated |
Severity for same-as-source values: "off", "info", "warning", "error" |
minCoverage |
CI gate threshold (overridable with --min-coverage) |
ignoreKeys |
Keys to skip β exact, or trailing-* prefix wildcard |
ignoreLocales |
Locale codes to skip |
allowUntranslated |
Keys allowed to equal the source (brand names, etc.) |
disableRules |
Rule ids to turn off entirely |
ruleSeverity |
Override severity per rule id |
Rule ids: missing-keys, extra-keys, empty-value, placeholder-mismatch,
plural-incomplete, html-mismatch, untranslated.
- Block broken translations in CI. Add
i18nlint scan ./locales --min-coverage 95 --max-errors 0to your pipeline. A PR that drops a key or renames a{variable}fails before it merges. - Onboard a new language safely. Drop in
pl.json, runi18nlint scanand instantly see the missing keys, the Polish plural forms you still owe, and any placeholders you mistyped. - Audit a translation vendor's delivery. Run
i18nlint scan ./delivery --md audit.mdand hand back a precise, per-key Markdown report instead of eyeballing diffs.
import { lint, loadLocales, toMarkdown } from "@didrod2539/i18nlint";
const report = lint(loadLocales(["./locales"]), { config });
console.log(report.summary.coverage, report.summary.grade);
await fs.writeFile("report.md", toMarkdown(report));- YAML and
.properties(Java/Android) locale formats. - Namespaced / directory-per-locale layouts (
locales/en/common.json). - Source-code scan to detect keys used in code but missing from locales.
- ICU
select/selectordinaldeep validation. --fixto scaffold missing keys and plural branches.- A GitHub Action wrapper that comments coverage on PRs.
Does it send my files anywhere?
No. i18nlint runs entirely on your machine β no API key, no telemetry, no
uploads. It makes zero network calls.
Which i18n libraries does it work with?
Any that store messages as JSON: i18next, react-intl/FormatJS, vue-i18n, LinguiJS,
Polyglot, and plain JSON. It understands ICU plural and the common placeholder
styles. (YAML/.properties are on the roadmap.)
How does it know Polish needs four plural forms?
It ships a curated subset of the Unicode CLDR cardinal plural rules mapping
each language to its required categories. Unknown languages default to
one/other; see src/plural.ts.
Won't the plural/HTML checks have false positives?
The plural scan is a deterministic regex over ICU branches and the HTML check
compares tag-name sets β both documented and conservative. Anything you disagree
with can be silenced via disableRules, ignoreKeys, or ruleSeverity.
Is the "coverage score" official?
No β it's a transparent metric (translated keys Γ· reference keys, minus
correctness penalties) so you can track and gate it. The math lives in
src/score.ts.
Contributions welcome! Each check is a small, self-contained rule in
src/rules/. See CONTRIBUTING.md and the
Code of Conduct.
git clone https://github.com/didrod205/i18nlint.git
cd i18nlint
npm install
npm test
npm run build
node dist/cli.js scan examples/localesMIT Β© i18nlint contributors
i18nlint is free, MIT-licensed, and built in spare time. If it caught a bug before your users did, please consider supporting it:
- β Star this repo β free, and it helps others find it.
- π Sponsor via Lemon Squeezy β one-time or recurring.
Where your support goes: YAML/.properties support, namespaced layouts,
source-code key scanning, ICU select validation, a --fix mode, a PR-commenting
GitHub Action, and fast issue responses.