Reference
Catalog checks
Fifty-nine rules by code, with default severity, fatal scope, trigger and fix.
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.
When rules run
Section titled “When rules run”Checks read the emitter’s own lowered representation, so a rule never disagrees with generated code.
- Most rules run per message.
LZ3xxxrules run once every catalog is resolved, the first momentde.jsoncan be compared withen.json.- The two
LZ5xxxgates run last, against what is on disk.
Fatal scope
Section titled “Fatal scope”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. |
Levelling a rule
Section titled “Levelling a rule”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.
Exit codes
Section titled “Exit codes”| 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.
LZ1xxx: configuration and catalogs
Section titled “LZ1xxx: configuration and catalogs”| 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
localesunset, 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 earlierrecordpath (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.
LZ2xxx: message syntax
Section titled “LZ2xxx: message syntax”| 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.
LZ3xxx: the cross-locale contract
Section titled “LZ3xxx: the cross-locale contract”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'.
LZ4xxx: identifiers and emit
Section titled “LZ4xxx: identifiers and emit”| 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,_formatsor_root; - group members named
__proto__; Objectas a group id or as the id of a grouped message, since the groups module callsObject.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' }.
LZ5xxx: output, record and scan
Section titled “LZ5xxx: output, record and scan”| 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.