Reference
Context record
The schema of locales/loclizr.context.json, and what the gate compares.
loclizr build writes the record to the record path, locales/loclizr.context.json by default
whatever catalogs says; commit it. loclizr init points it beside catalogs that live elsewhere.
Whoever translates next, a person, a model or a TMS, reads it.
It is a pure function of the catalogs, the meta sidecar, the scanned sources and the config, and never reads its own previous value. A fresh and an incremental regenerate agree byte for byte, and two branches editing one string cannot conflict on a sticky flag inside a generated file.
A real file
Section titled “A real file”Three of the example app’s twenty-six messages: a plural, a select and a markup message.
{ "schema": 1, "sourceLocale": "en", "locales": [ "de", "de-AT", "en" ], "messages": [ { "key": "cart.items", "id": "cart_items", "module": "messages/cart.js", "kind": "text", "source": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}", "sourceHash": "a826cf6a40d3293e", "description": "Badge under the cart icon on every page.", "args": [ { "name": "count", "type": "number", "options": null, "note": "Number of line items, not total quantity." } ], "variants": [ { "arg": "count", "kind": "plural", "matches": [ "=0", "one", "other" ] } ], "markup": [], "translations": [ { "locale": "de", "status": "translated", "from": null, "reason": null }, { "locale": "de-AT", "status": "translated", "from": null, "reason": null }, { "locale": "en", "status": "translated", "from": null, "reason": null } ], "usage": [ { "file": "src/App.tsx", "scope": "App" } ] }, { "key": "order.status", "id": "order_status", "module": "messages/order.js", "kind": "text", "source": "{state, select, packing {Being packed} shipped {On its way} delivered {Delivered} other {Processing}}", "sourceHash": "0277ce5c835efb7e", "description": "Chip in the order list. Past tense.", "args": [ { "name": "state", "type": "select", "options": [ "packing", "shipped", "delivered" ], "note": null } ], "variants": [ { "arg": "state", "kind": "select", "matches": [ "packing", "shipped", "delivered", "other" ] } ], "markup": [], "translations": [ { "locale": "de", "status": "translated", "from": null, "reason": null }, { "locale": "de-AT", "status": "inherited", "from": "de", "reason": null }, { "locale": "en", "status": "translated", "from": null, "reason": null } ], "usage": [ { "file": "src/App.tsx", "scope": null }, { "file": "src/App.tsx", "scope": "App" } ] }, { "key": "terms.accept", "id": "terms_accept", "module": "messages/terms.js", "kind": "markup", "source": "Read our <link>terms</link> before you continue.", "sourceHash": "b2a2946cc6836837", "description": "Sits under the checkout button. The link opens the terms page.", "args": [ { "name": "link", "type": "markup", "options": null, "note": null } ], "variants": [], "markup": [ "link" ], "translations": [ { "locale": "de", "status": "translated", "from": null, "reason": null }, { "locale": "de-AT", "status": "inherited", "from": "de", "reason": null }, { "locale": "en", "status": "translated", "from": null, "reason": null } ], "usage": [ { "file": "src/App.tsx", "scope": "App" } ] } ]}Schema
Section titled “Schema”Top level
Section titled “Top level”| Field | Type | Meaning |
|---|---|---|
schema |
1 |
Record format version, the only one there is. |
sourceLocale |
string |
Locale whose catalog defines the message set. |
locales |
string[] |
Every declared locale, sorted by code point. |
messages |
array | One per source-catalog message, sorted by key by code point. |
Per message
Section titled “Per message”| Field | Type | Meaning |
|---|---|---|
key |
string |
Flat catalog key after namespace prefixing and plural folding; what every diagnostic prints. |
id |
string |
Generated identifier, m.cart_items. |
module |
string |
Generated module that exports it, relative to outDir. |
kind |
"text" or "markup" |
Returns a string, or an array of parts. |
source |
string |
Canonical ICU source text, identical for i18next and ICU catalogs with the same meaning. |
sourceHash |
string |
SHA-256 of source as UTF-8 (an unpaired surrogate as its three-byte WTF-8 form, not U+FFFD), first sixteen hex characters. |
description |
string or null |
From the meta sidecar. |
args |
array | Arguments, in first-appearance order. |
variants |
array | Every plural, selectordinal and select, with its branches. |
markup |
string[] |
Tag names, sorted. |
translations |
array | One per declared locale, sorted. |
usage |
array | Where the scan found a reference, deduplicated, sorted by file then scope. |
| Field | Type | Meaning |
|---|---|---|
name |
string |
Argument name. |
type |
"text", "number", "date", "select" or "markup" |
text is a bare {name}, typed string | number at the call site. date covers date and time. markup is a tag handler. |
options |
string[] or null |
For select: the named branches without other, the union the call site is typed as. |
note |
string or null |
Placeholder note from the meta sidecar. |
variants
Section titled “variants”| Field | Type | Meaning |
|---|---|---|
arg |
string |
Selector argument. |
kind |
"plural", "selectordinal" or "select" |
The construct. |
matches |
string[] |
Plural: exact =N branches ascending, then keywords in CLDR order zero, one, two, few, many, other. Select: source order, other included. |
translations
Section titled “translations”| Field | Type | Meaning |
|---|---|---|
locale |
string |
Declared locale. |
status |
"translated", "inherited" or "fallback" |
translated: this locale’s own catalog had a renderable value. inherited: resolved through a declared ancestor (de-AT through de); not missing. fallback: fell through to the source locale; missing. |
from |
string or null |
Locale the value came from, for inherited and fallback. |
reason |
"missing", "blank", "invalid" or null |
For fallback: no value, an empty value, or a value that failed to parse. |
| Field | Type | Meaning |
|---|---|---|
file |
string |
POSIX, relative to the project root. |
scope |
string or null |
Deepest enclosing function, method, class or variable declaration. null at module scope. |
What is deterministic, and why
Section titled “What is deterministic, and why”- Messages sorted by key, locales sorted,
argsin first-appearance order, usage sites deduplicated and sorted by file then scope. Two-space indent, POSIX separators, LF, trailing newline. - No timestamps, absolute paths or tool version.
- No line numbers: one added import would churn every entry below it on a pull request that touched
no string, and two pull requests editing one component would conflict inside generated JSON. Line,
column and snippet stay in the JSON reporter and in
BuildResult.program.usagesfromloclizr/compiler. - Host ICU data and Node version are not inputs. Only diagnostics read
Intlat build time (LZ3007,LZ3013, andLZ1019for small ICU), so a laptop and a runner can disagree on those, never on this file. Pin Node (enginesor.nvmrc) for the diagnostics.
| Part, on the example | Share |
|---|---|
| Whole file | 23,774 bytes over 26 messages, about 914 per message |
translations |
close to half |
usage |
about an eighth |
source, sourceHash, description, args, variants, markup |
about a quarter |
The last row is the contract the gate compares and a reviewer reads; the two arrays are context.
Reference hashes: a826cf6a40d3293e for the cart.items source above, 3a78695388b38b5c for
Home (nav.home). Different ones mean a different canonical form.
The staleness gate
Section titled “The staleness gate”| Rule | Runs in | Reports |
|---|---|---|
LZ5003 record-stale |
check |
The committed record differs from the one this tree would produce. |
LZ5007 record-rewritten |
build |
The record it just wrote differed from the committed one. |
Both compare a projection: usage and translations are removed from every message on both
sides, and the rest is compared after a stable re-serialization. A committed record that is
missing or fails to parse counts as differing.
| In the file | In the comparison | |
|---|---|---|
schema, sourceLocale, locales |
yes | yes |
key, id, module, kind |
yes | yes |
source, sourceHash, description |
yes | yes |
args, variants, markup |
yes | yes |
translations |
yes | no |
usage |
yes | no |
Why those two come out
Section titled “Why those two come out”usagemoves with renames, file moves and tokenizer fixes; nothing the scan produces may fail a build by default.translationsmoves with every translator commit or TMS sync, which is not a contract change.- Copy edits, added or removed messages and argument changes all stay in the comparison.
buildstill writes the whole file on any byte difference; only the rule compares the projection.buildreplaces only a file it can tell is a record: an object withschema: 1, or one a merge left conflict markers in. Anything else at the path isLZ5001 output-unwritableand stays.
The record has no translation text or hashes, so the gettext fuzzy signal is a two-file diff: a
copy edit changes sourceHash and not de.json; a translation changes de.json and not the
record, unless coverage changed.
Run check in CI, not build, which rewrites the record and passes; see Continuous
integration.
Reviewing a string change
Section titled “Reviewing a string change”diff --git a/locales/en.json b/locales/en.jsonindex e1732a1..567cd72 100644--- a/locales/en.json+++ b/locales/en.json@@ -21,7 +21,7 @@ "cart": "Cart" }, "cart": {- "greeting": "Hi {name}, your cart is ready",+ "greeting": "Hello {name}, your cart is ready", "items": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}", "total": "Total: {amount, number, ::currency/USD}", "updated": "Updated {at, date, medium}"diff --git a/locales/loclizr.context.json b/locales/loclizr.context.jsonindex 561f905..328a8e3 100644--- a/locales/loclizr.context.json+++ b/locales/loclizr.context.json@@ -582,8 +582,8 @@ "id": "cart_greeting", "module": "messages/cart.js", "kind": "text",- "source": "Hi {name}, your cart is ready",- "sourceHash": "54db56b1aeaf77ae",+ "source": "Hello {name}, your cart is ready",+ "sourceHash": "cb7288f2f820ef90", "description": null, "args": [ {A rewording in a copy of the example: source and sourceHash move together, nothing else, and
de.json is untouched.
diff --git a/locales/loclizr.context.json b/locales/loclizr.context.jsonindex 328a8e3..f0bfa6f 100644--- a/locales/loclizr.context.json+++ b/locales/loclizr.context.json@@ -582,8 +582,8 @@ "id": "cart_greeting", "module": "messages/cart.js", "kind": "text",- "source": "Hello {name}, your cart is ready",- "sourceHash": "cb7288f2f820ef90",+ "source": "Hello {name}, your {count} items are ready",+ "sourceHash": "220d19181cdee30a", "description": null, "args": [ {@@ -591,6 +591,12 @@ "type": "text", "options": null, "note": null+ },+ {+ "name": "count",+ "type": "text",+ "options": null,+ "note": null } ], "variants": [],Adding a placeholder grows args, and the same build fails LZ3004 arg-missing for de and
de-AT, whose translations no longer carry it.
Check, in diff order:
- The record has a hunk under the same key as the catalog hunk.
- A new placeholder shows as an
argsentry with the type the call site will demand; a new plural shows undervariants. - The description still fits, or changed with the text.
- Every target stays
translatedafter a rewording; deleting a target value flips one tofallback. - The
usagesites are the files a translator would open to see the string.
What it does not carry
Section titled “What it does not carry”Of five things a translator wants at every string, the record has two:
| Has | Does not have |
|---|---|
| The render site: file and enclosing declaration | Element role: a button label or a heading |
| Argument types, including a select’s options and whether a number selects a plural | Surrounding copy on the same screen |
| A glossary |
The description is where a human puts element role and surrounding copy today; LZ3012 ambiguous-source asks for
one when two keys share a source text.
The usage field is a heuristic, not a resolution:
- Imports bind by specifier suffix:
./loclizr/messages,@/loclizr/messagesand#app/loclizr/groupsall bind; no alias map is read. - A computed access on a bound group counts against every member; on a plain namespace object it is not resolved. A renamed re-export is not followed.
- It errs toward false positives: an extra site is still a real file, a missed one loses the payload.