Guides
Translation workflow
Who owns which file, what each pull request shape raises, and how a lagging locale is handled.
| 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 string change
Section titled “A string change”A developer edits cart.greeting in en.json and pushes without rebuilding:
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-
Rebuild
Section titled “Rebuild”npx loclizr buildrewrites 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.json26 messages, 3 locales (source en), 0 errors, 1 warning -
Commit the record with the string
Section titled “Commit the record with the string”checkis 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 translation change
Section titled “A translation change”A translator edits cart.greeting in de.json. The contract did not move, so the gate passes
without a rebuild:
26 messages, 3 locales (source en), 0 errors, 0 warningsThe 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:
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 warningsfell back to source text: de 126 messages, 3 locales (source en), 0 errors, 0 warningsWhat a target edit can raise
Section titled “What a target edit can raise”LZ3 rules check a translation against the source. One de.json edit dropped the placeholder
from cart.greeting and the tag from terms.accept:
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.
A deleted source key
Section titled “A deleted source key”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:
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 warningSetting 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.
While translations lag
Section titled “While translations lag”LZ3001 missing-translation defaults to error, so every pull request between a string and its
translations fails the gate. To warn instead:
import { defineConfig } from 'loclizr'
export default defineConfig({ severity: { 'missing-translation': 'warn' },})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 warningsfell 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.
Rules for the vendor export
Section titled “Rules for the vendor export”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.
Descriptions
Section titled “Descriptions”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:
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'){ "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.