Skip to content
loclizr

Every loclizr build runs all fifty-nine rules: no separate lint command, no flag that turns them off. Each has a permanent code and a name that is its severity key. Defaults: 35 error, 19 warn, 3 off.

Checks read the emitter’s own lowered representation, so a rule never disagrees with generated code.

  • Most rules run per message.
  • LZ3xxx rules run once every catalog is resolved, the first moment de.json can be compared with en.json.
  • The two LZ5xxx gates run last, against what is on disk.

The scope decides whether a tree is written at all.

Scope Effect
never Reported; output is written. Twelve missing German strings still write messages.js, fall back, print twelve LZ3001 and exit 1.
always Blocks emission: an identifier collision would declare one function twice.
if source Fatal only for the source catalog. An unreadable de.json still emits; an unreadable en.json does not.
message Drops that source message from the emitter and the record; blocks nothing. Callers hit TS2339: Property 'cart_greeting' does not exist at an import * as m call site, TypeError: m.cart_greeting is not a function at render in JavaScript.
loclizr.config.ts
import { defineConfig } from 'loclizr'
export default defineConfig({
severity: {
'ambiguous-source': 'error',
'missing-translation': 'warn',
'unused-message': 'error',
},
})

severity is applied once, at the end: off drops the rule, except a diagnostic that blocks output, which prints as a warning; any other level rewrites its diagnostics. No analysis reads it, so re-levelling never changes what was emitted.

LZ1001 config-invalid, LZ1007 outdir-unsafe and LZ5001 output-unwritable are off the dial. Turning them down would let the tool quietly do what it never should, such as write outside the project root and exit 0. Naming one in severity is itself LZ1001.

Code Meaning
0 Clean, or warnings only and at or under --max-warnings.
1 At least one error, or warnings over --max-warnings.
2 LZ1001, LZ1007 or LZ5001, or a usage error before any rule ran.

--max-warnings has no cap by default, so warnings alone never exit 1. See CLI exit codes for usage errors and --no-fail.

Code Rule Default Fatal Fires when Fix
LZ1001 config-invalid error always, exit 2 The config threw, has no default export, or is missing at --config; a field failed validation or is not a config field; sourceLocale is not in locales; outDir holds the catalogs, meta or record path, compared without case; the catalogs pattern matches the meta or record path; severity names an unknown rule or a fixed one. Paste the printed line for the field, such as locales: ['en', 'de'], or rename an unknown field to the one the hint names. After moving outDir, delete the .gitignore and generated files the old one holds.
LZ1002 locale-tag-invalid error always Intl.getCanonicalLocales rejects a tag in locales or sourceLocale. Correct the tag: no formatter can be built for it.
LZ1003 no-catalogs-found error always The catalogs pattern matched no file. Write locales/en.json, or point catalogs at your layout, such as 'public/locales/{locale}/{ns}.json'.
LZ1004 source-catalog-missing error always No catalog for the source locale, or no en catalog and several locales to choose from. Paste the printed config as loclizr.config.ts, or copy its locales and sourceLocale into the config you have: one line per discovered locale, with its file.
LZ1005 catalog-missing error never A declared locale has no catalog file. Every message falls back to the source. Write the catalog, or drop the locale.
LZ1006 catalog-undeclared warn never A catalog file that is not a declared locale. Cases below the table. Add the locale to locales, or move the file off the pattern.
LZ1007 outdir-unsafe error always, exit 2 outDir resolves outside the project root, or to the root itself. Name a directory inside the project, such as 'src/loclizr'. The build prunes everything under it that the emit did not produce.
LZ1008 catalog-unreadable error if source A catalog or the meta sidecar is unreadable. The sidecar is never fatal. Check the path in catalogs and the file permissions. For the sidecar, check the path in meta, or set meta: false to switch descriptions off.
LZ1009 catalog-json-syntax error if source Not valid JSON. The span points at the offending character. Plain JSON: double-quoted keys, no trailing comma, no comments. Save as UTF-8 without a byte order mark.
LZ1010 catalog-shape-invalid error never A non-object root; an array or other non-string leaf; a formatjs extract record (a string defaultMessage with only extract fields beside it); a sidecar entry, description or placeholder note of the wrong shape. A null leaf in a target catalog is a missing translation, not this; in the source catalog it is this. Quote the value, or write null for an untranslated target unit. Give a null source leaf its text, or delete the key from every catalog. Give each array item its own key: arrays need format(), deferred past v0.1. For formatjs compile --ast output, compile again without --ast. Run formatjs compile on an extract file and carry its descriptions into the meta sidecar (Migrating from FormatJS).
LZ1011 duplicate-key error never One flat key from a nested and a dotted form, a duplicate JSON key, two namespace files of one locale, or a bare X beside the folded plural of X_*. JSON.parse keeps the last: a translation vanishes with no diff. Delete or rename one of the two.
LZ1012 i18next-nesting-unsupported error never An i18next value contains $t(. Inline the nested text, or split the sentence into two messages.
LZ1013 i18next-format-unsupported error never An inline formatter, {{val, fmt}}. It renders the raw value. Convert the file to ICU and write {val, number, yourStyle} with the style in formats.number, or {val, date, yourStyle} with the style in formats.dateTime for a datetime formatter.
LZ1014 plural-suffix-orphan warn never A CLDR-suffixed key with no _other sibling, or X_plural beside a bare X (i18next JSON v3). Both stay ordinary keys. Add X_other, rename a key that was never a plural, or run i18next’s JSON v3 to v4 converter.
LZ1015 meta-orphan warn never A sidecar entry for a key the source catalog lacks. Address entries by the post-fold, post-prefix key every diagnostic prints.
LZ1016 i18next-markup-literal warn never Tag-shaped text in an i18next value was escaped to literal text, as i18next rendered it. Once every tag in the value lowers (its name starts with an ASCII letter, it carries no attributes, it is closed), set i18nextMarkup: 'tags' to lower the tags to markup arguments. Until then the warning names the tag that cannot lower: name a numbered tag and move attributes to the call site.
LZ1017 i18next-context-detected warn never X_male or X_female beside a bare X, once picked by t('X', { context }); each is now its own message with no selector. Other X_word keys are ordinary snake_case and raise nothing. Convert the file to ICU, then use the printed {context, select, ...} message.
LZ1018 locale-base-missing warn never A declared region tag whose base tag is not declared. Add the base, such as 'de', or expect Accept-Language: de to fall back to the source.
LZ1019 icu-data-incomplete warn never A startup probe finds small ICU (Intl.PluralRules('ru') under four categories), which silently answers unknown locales with English data; or a declared locale has no plural data and Intl answered for the host locale instead. Run a Node with full ICU, or set NODE_ICU_DATA to a full data file. Until then LZ3007 and LZ3013 are off for the run (or that locale): no claim beats a wrong one.
LZ1020 icu-in-i18next-file warn never A file read as i18next holds a single-brace run shaped like a typed ICU argument, which renders as literal text. Convert the file to ICU, or set severity: { 'icu-in-i18next-file': 'off' } if literal text was meant. The hint names the {{ or suffixed key that made 'auto' pick i18next.
LZ1021 outdir-foreign-file warn never A file under outDir lacks the generated header, or is a symbolic link at a path the build emits, so it was neither overwritten nor pruned; or a headered orphan could not be deleted. Move it outside outDir, or delete it if it is generated output whose header was stripped.
LZ1022 meta-placeholder-orphan warn never A sidecar placeholders key that names no argument of its message, so the note never reaches the record. The message lists the arguments the message does take. Rename the key to the argument it describes, or delete it.

LZ1006 fires for:

  • a catalog of an undeclared locale;
  • a basename that is not a locale tag (skipped);
  • a file under the catalog directory off the pattern;
  • with locales unset, a five to eight letter name with no BCP 47 subtag;
  • a file under the pattern that parses to an object with schema: 1, a context record left at an earlier record path (skipped).

For a name that is not a tag, rename it to a BCP 47 tag, or move it out of the catalog pattern. For a leftover record, delete it: the build writes the record at the configured record path.

Code Rule Default Fatal Fires when Fix
LZ2001 icu-syntax error message The ICU parser rejected the value, other than for a missing other branch, or a select has two options equal under NFC. By parser error, below the table.
LZ2002 icu-style-unknown error never A named style neither built in nor in formats. Define it, such as formats.number.compact, or use an ICU skeleton. ICU carries no currency code: define formats.number.currency, or write ::currency/USD.
LZ2003 icu-skeleton-invalid error never A :: skeleton the parser cannot resolve or Intl cannot build, one whose stems all resolve to nothing, a per-measure-unit/ with no unit to divide, or a date or time skeleton holding J or C, which Intl has no hour option for. Falls back to the bare number, date or time format. Fix the skeleton, or use a named style from formats. When the parser gives a reason, the message quotes it, such as `D/F/g` (day) patterns are not supported, use `d` instead for ::D. Stems that resolve to nothing are misspelled, like ::currrency/USD, or not supported by loclizr, like ::latin. For J or C, write ::jm for the locale’s hour with its day period, or ::Hm for a 24-hour clock.
LZ2004 plural-other-missing error message A plural or selectordinal with no other branch. Add other {...} as the last branch.
LZ2005 select-other-missing error message A select with no other branch. Add other {...} as the last branch.
LZ2006 plural-category-unknown error never A branch keyword that is neither a CLDR category nor =N. Use zero, one, two, few, many or other, or an exact branch such as =0.
LZ2007 arg-name-invalid error never An argument name that is not a JavaScript identifier after NFC normalization, or is __proto__. Rename the argument.
LZ2008 pound-literal warn never A literal # inside a plural body, rendering the character rather than the count. Inside a nested select, move the # out or write {count, number}. For a quoted '#', drop the quotes. In an i18next file, write {{count}}.
LZ2009 arg-type-conflict-local error message One name at two irreconcilable types in one value, {x, number} of {x, date}. Use one type, or split it into two arguments.

LZ2001 fixes:

Parser error Fix
Unclosed { Add the matching }.
Unclosed tag Tag-shaped text lowers to markup in an ICU file. Close the tag, or quote it as '<br>' to keep it literal.
Malformed name, {user.name} Names take letters, digits and _. Write {name}, not {{name}}; rename an argument carrying a dot, a dash or a $.
Two select options equal under NFC Delete one branch: the options differ only in how an accented letter is encoded.
Anything else Look for an unbalanced brace, an unclosed ', or a < read as a tag.

In a file read as i18next, the span names the catalog entry, not the character, and the hint carries the parser’s error.

These rules compare a translation against the source locale. Hints speak the syntax of the file they point at: {name} and few {...} in an ICU catalog, {{name}} and an item_few key in an i18next one.

Code Rule Default Fatal Fires when Fix
LZ3001 missing-translation error never A locale has no value (null counts) and fell through to the source. Inheriting from a declared ancestor (de-AT from de) is not this. Add the key to the target catalog.
LZ3002 blank-translation error never The same, with an empty or whitespace-only value. Not raised where the source value is blank too. Write a value.
LZ3003 extra-translation warn never A target key the source catalog lacks. No message is generated for it. Add it to the source catalog, or delete it from the target.
LZ3004 arg-missing error never A source argument the translation does not use. Add the argument, such as {name}, to the translation.
LZ3005 arg-extra error never The translation uses an argument or tag the source lacks. That locale renders source text. Fix the spelling, remove the tag, or add it to the source.
LZ3006 arg-type-conflict error never One argument typed differently in two locales. One call site cannot carry both. Use one type in every locale.
LZ3007 plural-category-incomplete warn never The locale’s plural rules select a category the message has no branch for. Add the branches, such as few {...} many {...} in ru.
LZ3008 select-option-missing warn never A target lacks a branch for a source option, so that value renders other. Add the branch to the target.
LZ3009 select-option-extra error never A target names a branch the source lacks. The generated type never lets a call site pass it. Add it to the source select, or delete it from the target.
LZ3010 markup-mismatch error never A translation’s set of tags differs from the source. Nesting arity is not compared. Wrap the matching words in the tag, such as <link>.
LZ3011 date-without-timezone off never A date or time argument whose resolved options carry no timeZone, once per message against the source, only while formats.timeZone is unset. Set formats.timeZone, or leave the rule off where the viewer’s own zone is wanted.
LZ3012 ambiguous-source warn never Keys share byte-identical source text and any one lacks a description. One diagnostic per text, listing every colliding key. Paste the printed meta entries, or the printed severity line.
LZ3013 plural-category-unreachable warn never A keyword branch the locale’s plural rules never select. Use an exact branch (de never selects zero: write =0), or delete it.
LZ3014 bidi-control-unpaired warn never A value opens an embedding, override or isolate (U+202A to U+202E, U+2066 to U+2069) and never closes it, or closes one with nothing open. A pair balances inside one plural or select branch. U+200E and U+200F are marks and never fire it. Close it with the printed control (U+202C, or U+2069 for an isolate), or delete it.

LZ3012 is the rule an imported catalog hits first

Section titled “LZ3012 is the rule an imported catalog hits first”

“Open” can be a verb or an adjective, and a translator handed the bare string cannot tell. The rule fires when any key in the colliding set lacks a description. Here the example app gained an undescribed footer.cart:

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' }

It defaults to warn: at error it fires on every app-sized catalog (“Save”, “Cancel”), and an imported i18next catalog has no descriptions. loclizr init writes the error line for a greenfield project and comments it out for a retrofit. At warn, the hint offers make this a hard gate with 'error'.

Code Rule Default Fatal Fires when Fix
LZ4001 identifier-collision error always Two keys mangle to one identifier; two top-level segments name modules a case-insensitive filesystem cannot tell apart; two groups share an export id or type base; a group id equals the id of a grouped message; two keys become one member of a group. Never auto-numbered: a counter shifts when a key is inserted. Rename a key or group, or map it: identifiers: { 'nav_home': 'nav_home2' }.
LZ4002 identifier-reserved error always A reserved name, listed below the table. Rename the key or group, or map the key in identifiers.
LZ4003 confusable-key error never Two keys differ but fold to one skeleton after NFKC, removing invisible joiners (U+200C, U+200D, U+2060, U+FEFF), and a Cyrillic and Greek to Latin fold. Pairwise, so a wholly Cyrillic key alone never fires. Retype the key carrying the homoglyph in Latin, or delete the invisible character.
LZ4004 group-empty error never A group prefix matched no keys. Check the prefix: it matches on a dot boundary, so err captures err.forbidden, never errforbidden.
LZ4005 nondeterministic-output error always A second emit over a reversed program produced different bytes. A compiler bug: report it with the printed path and the catalogs.
LZ4006 group-args-heterogeneous warn never Group members differ in arguments, so every dynamic call must pass the union. Related rows list each member’s. Split the group by argument shape, or give the odd members bare {x} arguments.
LZ4007 identifier-orphan warn never An identifiers entry names no source key, top-level key segment or group, so it renames nothing. The hint names the closest one. Correct the entry’s key, or remove the entry.

LZ4002 reserves:

  • identifiers in the $ namespace, and __proto__, constructor, prototype, cookie, locales, sourceLocale, getLocale, setLocale, subscribe;
  • top-level segments naming _locale, _formats or _root;
  • group members named __proto__;
  • Object as a group id or as the id of a grouped message, since the groups module calls Object.freeze.

__proto__, constructor and prototype are built-in properties of every JavaScript object. The generated barrel already exports names such as locales, and a star export loses to it silently. Map the key, such as identifiers: { 'locales': 'localesMessage' }.

Code Rule Default Fatal Fires when Fix
LZ5001 output-unwritable error always, exit 2 A generated file or the record could not be written, a write path escapes the project root through a symlink, or the record path holds a file that is not a record. Make the path writable and check nothing else holds it open. For a file that is not a record, point record at a path of its own.
LZ5002 output-stale error never check only. A generated file on disk differs from what build would write, or is a headered orphan this emit did not produce. Only files that exist. Run loclizr build, which prunes orphans; commit the result if the tree has no self-ignoring .gitignore.
LZ5003 record-stale error never check only. The committed context record’s contract differs from what these catalogs produce, or the record does not exist. Run loclizr build and commit the record with the string change.
LZ5004 scan-found-nothing warn never The scan read files but found no import of the generated tree. Check scan.include. Usage is found through an import such as import * as m from './loclizr/messages'.
LZ5005 unused-message off never No scanned file references the message. Suppressed while the scan found no generated import, so a wrong glob costs one warning, not four hundred. Delete the key, or widen scan.include to reach the call site.
LZ5006 missing-description off never A message with arguments or markup and no description; a blank one counts as none. Paste the printed entry into the meta sidecar, or set meta to switch the sidecar on.
LZ5007 record-rewritten warn never build only. The record was rewritten because the committed contract differed. Commit the rewritten record with the string change.

LZ5007 is the LZ5003 comparison on build, for a pipeline that only runs pnpm build. Both ignore every message’s usage and translations, so a renamed component or a translation batch passes. The staleness gate lists what is compared.