Reference
CLI
Three commands, eight options, two reporters, three exit codes.
loclizr <command> [options]
Commands init write loclizr.config.ts and a seed catalog, then print the package.json scripts and the CI step build read the catalogs, check them, write the generated tree and the context record check everything build does with no writes, plus the output and record gates
Options --cwd <dir> directory to run in, default the current one --config <path> config file to load, or the one init writes --reporter human|json diagnostic format, default human --max-warnings <n> exit 1 above this many warnings, --max-warnings=-1 for no cap, which is also the default --quiet print errors only, plus a summary line when warnings were dropped --no-fail build only: report everything, exit 0 once output was written -v, --version print the installed loclizr version -h, --help print thisRun it as npx loclizr or from a package.json script. One help text serves every command
(loclizr build --help prints it too); loclizr --version (or -v) prints the installed version.
Samples are runs against examples/vite-react (en.json, en.meta.json, de.json, a sparse
de-AT.json), whose config sets groups: { errors: 'errors' } and turns group-args-heterogeneous off (hence no LZ4006). Error samples name the edit behind them.
Commands
Section titled “Commands”loclizr init
Section titled “loclizr init”wrote loclizr.config.tswrote locales/en.json
Install loclizr, which loclizr.config.ts and the generated code import:
npm i loclizr
Add these scripts to package.json:
"predev": "loclizr build --no-fail", "prebuild": "loclizr build", "pretypecheck": "loclizr build"
Add this step to CI, before any step that runs loclizr build:
# loclizr check compares the committed context record against this tree. # loclizr build would rewrite that record in the CI workspace and pass. - run: npx loclizr check- Create-only: an existing file is kept, and the output says so.
package.jsonis never edited. - Takes
--cwdand--config.--no-failis a usage error.
init first reports catalogs on disk (found 3 locales at locales/{locale}.json: de, de-AT, en):
| On disk | Config gets |
|---|---|
| No source catalog, or an empty one | severity: { 'ambiguous-source': 'error' }, a hard gate from the first commit |
| Existing catalogs | the same line, commented out |
A generated tree (messages.js with the header, outside node_modules, dist, build and outDir) |
augmentLocale: false, since two trees augmenting the registry in one program is TS2717 |
Another layout, such as public/locales/{locale}/{ns}.json or lang/{locale}.json |
that catalogs pattern; meta and record in the layout’s base directory when that is not locales (lang/{sourceLocale}.meta.json, lang/loclizr.context.json; public/locales/ for the {ns} layout); locales with the list it found when the pattern has {ns}; and no seed file, which would otherwise hide the real catalogs |
Catalogs under a directory catalogs cannot spell, such as app/(marketing)/locales/ |
the default pattern, the gate commented out and no seed file; the output names the directory and says to move it |
import { defineConfig } from 'loclizr'
export default defineConfig({ sourceLocale: 'en', catalogs: 'locales/{locale}.json', outDir: 'src/loclizr', severity: { 'ambiguous-source': 'error' },})import { defineConfig } from 'loclizr'
export default defineConfig({ sourceLocale: 'en', catalogs: 'locales/{locale}.json', outDir: 'src/loclizr', // Turn this on once locales/en.meta.json describes your keys. It // reports keys that share source text where at least one of them has no // description, so on a catalog imported without descriptions it fires on // every shared string at once. // severity: { 'ambiguous-source': 'error' },})import { defineConfig } from 'loclizr'
export default defineConfig({ locales: ['de', 'en'], sourceLocale: 'en', catalogs: 'public/locales/{locale}/{ns}.json', meta: 'public/locales/{sourceLocale}.meta.json', record: 'public/locales/loclizr.context.json', outDir: 'src/loclizr', // Turn this on once public/locales/en.meta.json describes your keys. It // reports keys that share source text where at least one of them has no // description, so on a catalog imported without descriptions it fires on // every shared string at once. // severity: { 'ambiguous-source': 'error' },})The seed catalog writes its plural as ICU ({count, plural, one {...} other {...}}), not
_one / _other, so the default catalogFormat: 'auto' keeps reading it as ICU beside an
imported i18next file. The config never sets catalogFormat.
loclizr build
Section titled “loclizr build”wrote src/loclizr (21 files) and locales/loclizr.context.jsoncommit locales/loclizr.context.json; `loclizr check` compares it26 messages, 3 locales (source en), 0 errors, 0 warningswrote src/loclizr (20 files)26 messages, 3 locales (source en), 0 errors, 0 warningsThe 21st file is the self-ignoring .gitignore, written only when outDir does not exist at the
start of the run. The example tracks that file and its committed record already matches, so a
clone’s first build writes 20 files and leaves the record alone, with no commit line.
Runs every rule, scans sources for usage, writes the tree under outDir and the record at
record: locales/loclizr.context.json by default whatever catalogs is. init points it beside
catalogs that live elsewhere.
- Write-if-changed. An unchanged rerun prints no
wroteline. A run that a fatal diagnostic blocked printsnothing generated: 1 fatal (LZ4002)in its place. - Prune. Header-carrying files under
outDirthis emit did not produce are deleted, so a renamed top-level key leaves nomessages/old.js. Files without the header are kept and raiseLZ1021 outdir-foreign-file. - Non-fatal errors still emit. A missing German string still writes
messages.jsfrom the fallback chain, printsLZ3001and exits 1. - Fatal diagnostics write nothing: identifier collision, no catalogs, invalid config. A config
that cannot run (
LZ1001,LZ1007) exits 2 with no summary. Every other fatal run that stops before the write step exits 1, names the fatal codes in anothing generatedline, and prints nofell back to source textline, since no tree was rendered. LZ5001 output-unwritablefails at the write step. The tree was rendered, so the run keeps its summary and itsfell back to source textline, prints nonothing generatedline, and exits 2.
error LZ3001 missing-translation locales/de-AT.json de-AT subscribe
The de-AT catalog has no value for this key, so this message renders en text.
fix add "subscribe" to locales/de-AT.json
error LZ3001 missing-translation locales/de.json de subscribe
The de catalog has no value for this key, so this message renders en text.
fix add "subscribe" to locales/de.json
error LZ4002 identifier-reserved locales/en.json:2:17 subscribe
The key "subscribe" produces the reserved identifier "subscribe".
fix The generated barrel already exports that name, and a star export loses to it silently. Map the key in loclizr.config.ts: identifiers: { 'subscribe': 'subscribeMessage' }
nothing generated: 1 fatal (LZ4002)27 messages, 3 locales (source en), 3 errors, 0 warningsThe nothing generated line prints under --quiet too, and when the rule was turned down to
warn or off: the rule still blocks the tree.
loclizr check
Section titled “loclizr check”The CI command: build with no writes, plus two gates. Here cart.greeting changed in en.json
after the last build:
error LZ5002 output-stale src/loclizr/messages/cart.d.ts
`src/loclizr/messages/cart.d.ts` differs from what this build would write.
fix the generated tree on disk is stale; run `loclizr build`.
error LZ5002 output-stale src/loclizr/messages/cart.js
`src/loclizr/messages/cart.js` differs from what this build would write.
fix the generated tree on disk is stale; run `loclizr build`.
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), 3 errors, 0 warnings| Gate | Fires when |
|---|---|
LZ5002 output-stale |
a generated file on disk differs from this build’s output, or is a header-carrying orphan; a gitignored outDir absent in CI raises nothing |
LZ5003 record-stale |
the committed record differs from the one these catalogs produce, or was never committed |
--no-fail is refused:
loclizr: --no-fail is not accepted by `loclizr check`, which is the gateOptions
Section titled “Options”Global, after the command. --max-warnings 0 and --max-warnings=0 are equivalent.
| Option | Default | Notes |
|---|---|---|
--cwd <dir> |
current directory | Project root: config lookup and the base of every path. Missing or not a directory: usage error. |
--config <path> |
first of loclizr.config.ts, .mts, .js, .mjs; none means defaults |
Relative to --cwd. Names no file: LZ1001 config-invalid, exit 2. |
--reporter human|json |
human |
Anything else: usage error. |
--max-warnings <n> |
no cap (also any negative n) |
Exit 1 above n warnings. Non-integer: usage error. |
--quiet |
off | Errors only; the counts line stays when warnings were hidden, and a nothing generated line always stays. No effect on JSON. |
--no-fail |
off | build only; below. |
-v, --version |
The installed version, exit 0, with or without a command. | |
-h, --help |
Usage block, exit 0, with or without a command. |
Colour only when stdout is a TTY and NO_COLOR is unset.
--no-fail
Section titled “--no-fail”The dev loop’s flag. Only the exit code changes; diagnostics, the record rewrite and LZ5007 stay.
Both runs: cart.greeting deleted from 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
wrote src/loclizr (1 file) and locales/loclizr.context.jsoncommit locales/loclizr.context.json; `loclizr check` compares it26 messages, 3 locales (source en), 1 error, 0 warningsfell back to source text: de 1Exit code 1.
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
wrote src/loclizr (1 file) and locales/loclizr.context.jsoncommit locales/loclizr.context.json; `loclizr check` compares it26 messages, 3 locales (source en), 1 error, 0 warningsfell back to source text: de 1Exit code 0.
| Run | Exit |
|---|---|
| Reached the write step, even with zero bytes changed | 0 instead of 1 |
| Wrote no output (identifier collision, missing source catalog) | still 1: vite dev would fail on the missing module anyway |
| Could not run | still 2 |
Only predev carries it, never the gate. A 2000-key, 10-locale catalog builds in about half a
second on a laptop, so each loop is a full rebuild.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | Clean, or warnings at or under --max-warnings. Also any build --no-fail that reached the write step. |
| 1 | At least one error, warnings over --max-warnings, or a fatal diagnostic that blocked the tree at any severity. |
| 2 | The tool could not run. |
Exit 2 has exactly these causes:
LZ1001 config-invalid,LZ1007 outdir-unsafe,LZ5001 output-unwritable, the three rulesseveritycannot re-level.- A usage error: unknown command or option,
--no-failoncheckorinit, a bad--reporter, a non-integer--max-warnings, a missing--cwd. initfailing to write one of its two files.- An unexpected throw, printed as
loclizr: <message>on stderr.
error LZ1001 config-invalid loclizr.config.ts
`severity` cannot re-level `config-invalid`.
fix LZ1001 decides whether the tool can run at all, so turning it down would let a broken build exit 0.Twenty-one of the fifty-nine rules default to warn, so the no-cap default decides whether a freshly
imported i18next catalog exits 0 or 1 on its first build.
Reporters
Section titled “Reporters”Both sort diagnostics the same way: errors first, then code, file, locale, key, offset. Paths are POSIX and project-relative, so the order matches between a laptop and CI.
error LZ3012 ambiguous-source locales/en.json:5:14
3 keys share the source text "Cart" and one has no description. A translator sees one string with no way to tell the meanings apart.
app.cart locales/en.json:5:14 "Heading over the cart panel. A section title, not a link." footer.cart locales/en.json:41:14 no description nav.cart locales/en.json:21:14 "Top navigation link that opens the cart page."
fix add descriptions in locales/en.meta.json: "footer.cart": { "description": "" } or turn the rule down in loclizr.config.ts: severity: { 'ambiguous-source': 'warn' }| Part | Content |
|---|---|
| Header | Severity, code, rule, then whichever of file:line:column, locale and key apply. |
| Related | A three-column table, when the rule has related locations. |
| Repeats | Diagnostics that share severity, code, key, message and hint print once: the header ends with the key and the number of distinct files, such as 3 files, and one row per diagnostic gives its locale and file:line:column. A diagnostic with related locations always prints on its own. The JSON output and the counts keep one entry per diagnostic. |
fix |
The rule’s hint, line breaks kept. |
| Summary | One line of counts. On build, preceded by the wrote line and a commit reminder (unless LZ5007 already says so), and followed by fell back to source text: de 2, de-AT 1 when any message resolved all the way to source text. A run that a fatal diagnostic blocked prints nothing generated: 1 fatal (LZ4002) in place of the wrote line and no fallback line; with nothing analyzed, the counts name no source locale (0 messages, 0 locales, 1 error, 0 warnings). |
Terminal control characters print as <U+009B>, and a newline in a key becomes one space.
{ "schema": 1, "diagnostics": [], "summary": { "errors": 0, "warnings": 0, "messages": 26, "locales": 3, "fellBack": [] }}After deleting nav.home and cart.greeting from de.json, diagnostics elided:
"summary": { "errors": 3, "warnings": 0, "messages": 26, "locales": 3, "fellBack": [ { "locale": "de", "count": 2 }, { "locale": "de-AT", "count": 1 } ] }}One two-space indented object on stdout. --quiet never thins it, so the counts always match the
array. Every diagnostic carries every field, null where it does not apply:
| Field | Type | Meaning |
|---|---|---|
code |
string |
Stable code, LZ3001. |
rule |
string |
Rule name, the key severity accepts. |
severity |
"warn" or "error" |
After the config’s severity is applied. |
fatal |
boolean |
Whether it blocked emission. |
message |
string |
The body; may contain newlines. |
hint |
string or null |
The fix text. |
file |
string or null |
POSIX, project-relative. |
locale |
string or null |
|
key |
string or null |
Flat catalog key. |
span |
object or null |
{ line, column, offset, length }, 1-based line and column. null for a missing translation. |
related |
array | file, locale, key, span, message per entry: the human reporter’s table rows. |
summary.fellBack:{ "locale", "count" }per locale with any message resolved to source text.- A run halted by an invalid config reports
"messages": 0and"locales": 0. - DEL and C1 controls are escaped as
\u007fthrough\u009f: a parser gets the catalog’s bytes back, a tailing terminal cannot be told to move the cursor.
The scripts init prints
Section titled “The scripts init prints”-
Install
Section titled “Install”npm i loclizr, as a regular dependency: the generated tree imports the locale store at run time. -
Add the scripts
Section titled “Add the scripts”package.json "predev": "loclizr build --no-fail","prebuild": "loclizr build","pretypecheck": "loclizr build"predevbuilds the tree before everydevrun;--no-failkeeps one untranslated key from stopping the dev server.prebuildandpretypechecktake the real exit code. -
Add
Section titled “Add check to CI”checkto CI.github/workflows/ci.yml # loclizr check compares the committed context record against this tree.# loclizr build would rewrite that record in the CI workspace and pass.- run: npx loclizr checkPut it before any step that runs
build. A pipeline running onlypnpm buildrewrites the record and passes, letting a stale record merge.
There is no prepare: it runs on npm ci and would rewrite the record before check compared
it. The cost: a fresh clone shows editor errors under ./loclizr/messages until the first dev,
build or typecheck. See Continuous integration.