Translate

Check translations before they ship

Runs on your machine, needs no key and sends nothing anywhere. It finds lost variables like {name}, changed numbers, missing keys and empty lines. Errors end the run with exit code 1, so CI can stop the merge; an empty line is a warning and fails the run only with --strict.

Run
npx kaeris check --source en.json --langs de

No Python needed: npx runs the npm package on Node 18 or newer. With Python, pip install kaeris gives you the same kaeris check.

de.json
{
  "greeting": "Hallo!",
  "delete": "50 Dateien löschen",
  "save": "",
  "terms": "Lies unsere <b>AGB</b>"
}
Outputexit code 1
✗ de: 1 placeholder mismatch(es):
    greeting: missing ['{name}'], added -
✗ de: 3 translation fault(s):
    greeting: lost placeholder {name}
    delete: number 50 appears in the translation but not the source
    delete: number 5 from the source is missing or changed in the translation
! de: 1 warning(s):
    save: empty translation — the source has text
✗ 1 placeholder mismatch, 3 translation faults

Source: {"greeting": "Hello, {name}!", "delete": "Delete 5 files", "save": "Save changes", …}

FlagMeaning
--strictAlso fail on warnings, such as an empty translation, and on extra keys that are not in the source.
--pattern '{lang}/common.json'Where the translations live. Default: where translate writes them, next to the source (de.json, app_de.arb, values-de/strings.xml).
--jsonMachine-readable result instead of the report.
--sarif kaeris.sarifAlso writes a SARIF 2.1.0 report for GitHub, see below.

Source file: any of the 13 formats (JSON, YAML, .strings, .xcstrings, Markdown/MDX, .po, ARB, Android XML, CSV, XLIFF, .properties, .resx, Fluent). Exit codes: 0 clean, 1 problems found, 2 wrong usage or a file it cannot read.

Guides by format: JSON · YAML · PO · XLIFF · ARB · .strings · .xcstrings · Fluent · .properties · .resx · CSV · Markdown · Android XML

CI recipes (in English): GitHub Actions · GitLab CI · pre-commit · Make and npm scripts

Glossary: one word per term, per language

--keep leaves a word as it is. When a term must be translated, but always the same way, put it in kaeris-glossary.json next to kaeris.json. translate gives the agreed words to the model, and check fails a translation that uses another word.

kaeris-glossary.json
{"terms": [
  {"term": "workspace", "pos": "noun",
   "note": "the shared project area, not a desk",
   "translations": {"de": "Arbeitsbereich",
                    "fr": "espace de travail",
                    "ru": "рабочее пространство"}},
  {"term": "KAERIS", "keep": true}
]}
Outputexit code 1
→ Glossary: 1 term(s), 1 kept as is
✗ fr: 1 translation fault(s):
    open: glossary: "workspace" should be "espace de travail"
! ru: 1 warning(s):
    leave: glossary: "workspace" should be "рабочее пространство" — not found, not even as an inflected form; check this line
✗ 1 translation fault

Source: {"open": "Open the workspace", "leave": "Leave the workspace"}. French said «bureau», Russian said «рабочей области».

A term counts as a whole word: Pro is not found in “product”. An inflected form of the agreed word passes. Where words change shape or join into compounds (Russian, Polish, German and others) a miss is a warning, not an error, because it can still be correct grammar. A list of words gives accepted variants. It works on every plan, the free demo included.

Findings on the line, inside the pull request

With --sarif every finding becomes a note on the exact line of the language file, next to your reviewer's comments.

.github/workflows/i18n-check.yml
- uses: kaeris-io/kaeris-action@v1
  with:
    mode: check
    source: locales/en.json
    languages: de,fr,es
    sarif-file: kaeris.sarif

- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: kaeris.sarif
locales/de.json · Files changed
  "greeting": "Hallo, {name}!",
- "delete": "5 Dateien löschen",+ "delete": "50 Dateien löschen",KAERIS i18n · delete: number 5 from the source is missing or changed in the translation  "save": "Änderungen speichern"

The report is written even on a clean run, because upload-sarif fails when the file is missing. Notes on the line are free on public repositories; private ones need GitHub Advanced Security. Without it the check still works: the job fails and the log lists every finding.