Checking locale files

check reads your locale files and reports what's wrong with them: keys a target locale is missing, values left empty, placeholders lost in translation and keys a target still has after the source dropped them.

It's free and open source and runs on your machine. No account, no API key, no localhero.json and nothing uploaded. It works the same on a repository you just cloned as on your own.

npx @localheroai/cli check --source en --path locales

Already using Localhero.ai to translate your pull requests? Then you probably don't need check. The GitHub Action translates new and changed keys on each pull request and commits them to the branch. Every pull request is ready to ship with its translations in place.

What it compares

A source locale file and a target locale file side by side, with the four comparisons between them labelled: a key absent from the target is missing, a value carrying fewer placeholders than the source is a placeholder mismatch, an empty string is an empty value and a key only the target has is an orphan

Finding What it means
Missing key The source has the key, a target locale doesn't. The string renders in your source language or shows the key itself, depending on your i18n library.
Empty value The key exists in the target with an empty string. Easy to miss: the key is there and nothing errors.
Placeholder mismatch The target's placeholders don't match the source's. "Order {id} confirmed" translated as "Order bekräftad" drops the order number from the sentence; an extra placeholder the source doesn't have is reported too.
Orphan The target has a key the source no longer does. Usually left over from a key that was renamed or removed. Reported, never fails a run.
Missing plural form A plural key lacks a form the target language needs, such as few and many in Polish. The i18n library falls back to other and renders fluent but wrong text.
Plural shape mismatch The source key has plural forms, the target has a single string. Not reported for languages that only have other, such as Japanese.
Structure mismatch The source has a string where the target has nested keys.
Conflicting value The same key has different values in two files of one locale. Rails merges a locale's files and whichever loads last wins.
Duplicate key A YAML file defines the same key twice. Only the last value is used.
Identical value The target string is byte-identical to the source. Sometimes correct, sometimes a copy-paste that never got translated.

The placeholder check tends to find the bugs nobody has reported. A missing key is visible the moment someone loads the page in that language; a dropped {amount} renders a grammatical sentence with a fact removed from it.

Reading the report

Locale        Keys   Missing  Placeholders  Orphans  Complete
de            6      2        0             0        67%
sv            6      0        1             0        100%

2 locale(s), 6 keys, 83% complete, 1 placeholder mismatches, 0 orphans

== de ==

Missing keys (2):
  cart.checkout
  order.total

== sv ==

Placeholder mismatches (1):
  order.confirmed: "Order {id} confirmed" -> "Order bekräftad" [missing {id}]

sv is 100% complete and still has a problem. Completeness counts the keys that exist, not whether their contents survived translation.

Each category is capped so a badly out-of-date locale doesn't bury the rest. Pass --all to print every finding.

Supported formats

JSON, YAML and gettext .po/.pot, which covers Rails, Django, Phoenix, React and Lingui projects. Placeholders are recognised across the common syntaxes and compared by kind: %{name} (Rails), {{name}} (i18next), {name} (ICU, including the argument of an ICU plural or select block), %(name)s (Python), %<name>s and printf's %s/%1$s. Kinds are not interchangeable: a {{count}} translated as {count} is reported as a mismatch.

Finding your files

With a localhero.json in the repository, check uses the paths and locales from it and needs no flags:

npx @localheroai/cli check

Without one, point it at a folder:

npx @localheroai/cli check --source en --path config/locales
Flag Use
--source <locale> Source locale. Defaults to the one in localhero.json, otherwise detected.
--locales <codes> Comma-separated targets to check. Defaults to every other locale found.
--path <dir> Locale folder to scan when there is no localhero.json.
--pattern <glob> File pattern inside --path. Defaults to **/*.{json,yml,yaml,po,pot}.
--all Print every finding instead of capping each category.

Use it in GitHub Actions

Add check as a step in any workflow. It needs no secrets or API key. On a pull request it annotates the diff on the lines the findings belong to:

# .github/workflows/i18n-check.yml
on: pull_request

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npx @localheroai/cli check --source en --path locales

On a pull request check compares against the base branch and reports only what the branch introduced. An existing backlog of missing keys won't fail everyone's work. The default shallow checkout is enough: check fetches the base commit itself. If that fails, it checks every key, reports the findings without failing the run and suggests fetch-depth: 0.

Use --full to check every key regardless. Use --changed-only to force the comparison outside a pull request.

Exit codes and --fail-on

check exits 0 when it finds nothing to fail on and 1 when it does. What counts is set by --fail-on:

Mode Fails on
missing Missing keys and empty values.
placeholders Placeholder mismatches only.
any Missing keys, empty values, placeholder mismatches, plural and structure problems, conflicting values and duplicate keys.
none No findings fail the run.

The default is any new problem when comparing against a base branch. Otherwise it fails on missing keys.

Orphans never fail a run, whatever the mode. Neither do identical values or placeholder hints. A key only the target has is as often an unloaded file or a framework's bundled translations as a real leftover. check reports it and leaves the call to you.

Even with none, check exits 1 when it can't run at all, for example when it finds no translation files.

On an existing codebase, run --fail-on none for a week to see the size of the backlog without blocking anyone. Then --fail-on missing on pull requests stops it growing.

Machine-readable output

--json writes the full report to stdout, with every category listed per locale. Each finding carries the key and the file it was found in; a missing key also carries the target file it is missing from:

npx @localheroai/cli check --json > i18n-report.json
{
  "sourceLocale": "en",
  "keyCount": 6,
  "locales": [
    {
      "locale": "de",
      "keyCount": 6,
      "missing": [
        { "key": "cart.checkout", "path": "locales/en.json", "targetPath": "locales/de.json" }
      ],
      "empty": [],
      "placeholderMismatches": []
    }
  ]
}

Handy for a dashboard or a scheduled job that tracks completeness over time.

Checking a repository you don't own

Because check needs no account and uploads nothing, it works on any clone:

git clone https://github.com/someone/their-app
cd their-app
npx @localheroai/cli check --source en --path config/locales

Open-source projects with community translations tend to collect exactly these findings. A report with file paths and key names makes a good issue.

Fixing what it finds

check only reports. To fill in missing keys, Localhero.ai's GitHub Action translates them on the pull request that introduced them and commits them to the same branch. The check that failed passes on the next run.

Last updated

Ready to try it?

Get setup in a couple of minutes. No credit card required.