Skip to content
loclizr
File Owner Written by
locales/en.json developers hand, or the TMS pulling source strings
locales/en.meta.json developers hand
loclizr.config.ts developers hand
locales/de.json and every other target catalog localisation the TMS, or a translator’s editor
locales/loclizr.context.json the compiler loclizr build, committed with the string change
src/loclizr/ the compiler loclizr build, gitignored

The compiler never writes a catalog, so a TMS that opens pull requests against locales/ needs no integration.

Transcripts come from a copy of examples/vite-react (26 messages, 3 locales): check ran without the generated tree, as in CI; build ran with it.

A developer edits cart.greeting in en.json and pushes without rebuilding:

npx loclizr check
error LZ5003 record-stale locales/loclizr.context.json
`locales/loclizr.context.json` no longer matches the catalogs: the contract this build derived differs from the committed record.
fix run `loclizr build` and commit the record with the string change.
26 messages, 3 locales (source en), 1 error, 0 warnings
  1. npx loclizr build rewrites the record and warns once:

    npx loclizr build
    warn LZ5007 record-rewritten locales/loclizr.context.json
    `locales/loclizr.context.json` was rewritten: the committed record's contract differs from the one these catalogs produce.
    fix commit the rewritten record with the string change, so the context lands in the same pull request.
    wrote src/loclizr (2 files) and locales/loclizr.context.json
    26 messages, 3 locales (source en), 0 errors, 1 warning
  2. check is clean; the reviewer sees source text, arguments and description in one diff.

With the generated tree on disk, check also prints LZ5002 output-stale per changed file. CI, with the tree gitignored, never does.

A translator edits cart.greeting in de.json. The contract did not move, so the gate passes without a rebuild:

npx loclizr check
26 messages, 3 locales (source en), 0 errors, 0 warnings

The record’s per-locale translations array is outside the comparison; the next build on main rewrites it, so the translation pull request never commits it.

Filling a missing key clears its error on its own, no rebuild. Before and after restoring cart.greeting in de.json:

npx loclizr check, de.json missing cart.greeting
error LZ3001 missing-translation locales/de.json de cart.greeting
The de catalog has no value for this key, so this message renders en text.
fix add "cart.greeting" to locales/de.json
26 messages, 3 locales (source en), 1 error, 0 warnings
fell back to source text: de 1
npx loclizr check, after the translation lands
26 messages, 3 locales (source en), 0 errors, 0 warnings

LZ3 rules check a translation against the source. One de.json edit dropped the placeholder from cart.greeting and the tag from terms.accept:

npx loclizr check
error LZ3004 arg-missing locales/de.json:24:18 de cart.greeting
The en text uses {name} and the de translation does not.
fix add {name} to "cart.greeting" in locales/de.json
error LZ3010 markup-mismatch locales/de.json:38:16 de terms.accept
The en text uses <link> and the de translation does not.
fix wrap the matching words of "terms.accept" in <link> in locales/de.json
26 messages, 3 locales (source en), 2 errors, 0 warnings
Edit to a target catalog Raises
placeholder or tag dropped, added or retyped; select branch the source lacks LZ3004, LZ3005, LZ3006, LZ3009, LZ3010, at error
select branch of the source left out; plural branch the locale never selects, or one it needs and lacks LZ3008, LZ3013, LZ3007, at warn
key deleted or set to null LZ3001 for it and every locale inheriting through it: nav.home nulled in de.json reports de and de-AT
key set to "" LZ3002, plus LZ3001 for every locale inheriting through it
key the source lacks LZ3003 at warn; no message generated, exit 0
direction control left open, or closed with nothing open LZ3014 at warn

Checks lists each rule with its printed fix.

Deleting a key from en.json changes the contract: LZ5003 until the record is rebuilt and committed. Target catalogs still holding the key warn LZ3003:

npx loclizr check, nav.home deleted from en.json
error LZ5003 record-stale locales/loclizr.context.json
`locales/loclizr.context.json` no longer matches the catalogs: the contract this build derived differs from the committed record.
fix run `loclizr build` and commit the record with the string change.
warn LZ3003 extra-translation locales/de.json:20:14 de nav.home
"nav.home" is in the de catalog and not in en, so no message is generated for it.
fix add "nav.home" to the en catalog, or delete it from locales/de.json
25 messages, 3 locales (source en), 1 error, 1 warning

Setting the key to null in en.json drops it the same way and adds LZ1010 catalog-shape-invalid at error: the source locale has nothing to fall back to.

LZ3001 missing-translation defaults to error, so every pull request between a string and its translations fails the gate. To warn instead:

loclizr.config.ts
import { defineConfig } from 'loclizr'
export default defineConfig({
severity: { 'missing-translation': 'warn' },
})
npx loclizr check, nav.home missing from de.json
warn LZ3001 missing-translation locales/de-AT.json de-AT nav.home
The de-AT catalog has no value for this key, so this message renders en text.
fix add "nav.home" to locales/de-AT.json
warn LZ3001 missing-translation locales/de.json de nav.home
The de catalog has no value for this key, so this message renders en text.
fix add "nav.home" to locales/de.json
26 messages, 3 locales (source en), 0 errors, 2 warnings
fell back to source text: de 1, de-AT 1
Where Run Exit on that tree
pull requests npx loclizr check 0, with missing keys still printed and counted
main, scheduled before a release npx loclizr check --max-warnings 0 1, so nothing ships untranslated unnoticed

Severity is per rule, not per locale, so staging one new locale relaxes it for all of them. Delete the line once the catalogs catch up.

What the TMS exports for an untranslated key decides the rule. Set it once:

Untranslated key exported as Build
absent, or null LZ3001 missing-translation, the right diagnostic
"" LZ3002 blank-translation at error, plus LZ3001 for every locale inheriting through it
the source text copied in nothing; the record marks the locale translated

Configure the last row away. No rule compares a translation to the source for equality, and the record cannot tell a deliberate Home from a copied one.

No TMS reads loclizr.context.json natively. Descriptions and placeholder notes reach translators through the vendor’s context field, filled by your own script from locales/en.meta.json:

scripts/tms-context.mjs
import { readFileSync, writeFileSync } from 'node:fs'
const meta = JSON.parse(readFileSync('locales/en.meta.json', 'utf8'))
const context = {}
for (const [key, entry] of Object.entries(meta)) {
const notes = Object.entries(entry.placeholders ?? {}).map(([name, note]) => `{${name}}: ${note}`)
context[key] = [entry.description, ...notes].filter(Boolean).join(' ')
}
writeFileSync('tms-context.json', JSON.stringify(context, null, 2) + '\n')
tms-context.json
{
"cart.items": "Badge under the cart icon on every page. {count}: Number of line items, not total quantity."
}

One string per key, the shape most context fields take; upload it through the vendor’s API or import.

For pull-request readers, the context record carries the same description and notes, plus argument types, plural shape, and each call site’s file and enclosing scope.

A sidecar entry whose key is not in en.json is LZ1015 meta-orphan, and a placeholder note for an argument the message does not take is LZ1022 meta-placeholder-orphan, so a rename the sidecar missed is caught in the same build. Catalogs covers the sidecar.