Skip to content

didrod205/i18nlint

🌍 i18nlint

Catch broken translations before your users do — locally, no API key.

npm version CI node license

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.


One-line summary

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.

Why this project exists

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.

Key features

  • 🔑 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 only other, 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.

Install

# 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.

Quick start

i18nlint scan ./locales
fr  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

CLI usage

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 --version

scan 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.jsonen, pt-BR.jsonpt-BR). Point scan at a directory and it finds every *.json recursively.

Example result

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 running npx @didrod2539/i18nlint scan examples/locales.

Configuration

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.

Real-world use cases

  1. Block broken translations in CI. Add i18nlint scan ./locales --min-coverage 95 --max-errors 0 to your pipeline. A PR that drops a key or renames a {variable} fails before it merges.
  2. Onboard a new language safely. Drop in pl.json, run i18nlint scan and instantly see the missing keys, the Polish plural forms you still owe, and any placeholders you mistyped.
  3. Audit a translation vendor's delivery. Run i18nlint scan ./delivery --md audit.md and hand back a precise, per-key Markdown report instead of eyeballing diffs.

Programmatic API

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));

Roadmap

  • 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 / selectordinal deep validation.
  • --fix to scaffold missing keys and plural branches.
  • A GitHub Action wrapper that comments coverage on PRs.

FAQ

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.

Contributing

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/locales

License

MIT © i18nlint contributors

💖 Sponsor

i18nlint is free, MIT-licensed, and built in spare time. If it caught a bug before your users did, please consider supporting it:

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.

About

Deterministic CLI that lints i18n/l10n translation files: missing keys, placeholder mismatches, CLDR-aware plural completeness, HTML tag drift, untranslated strings. JSON/Markdown reports, no API key, no server.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors