Reference
Configuration
Every field of loclizr.config.ts, its type, default and the rules on it.
import { defineConfig } from 'loclizr'
export default defineConfig({ locales: ['en', 'de', 'de-AT'], sourceLocale: 'en', groups: { errors: 'errors' }, severity: { 'ambiguous-source': 'error' },})Optional: the defaults below apply without one. Groups and format styles need a file.
-
defineConfigonly adds the type. Export the object, not a promise. A missing,nullorundefineddefault export isLZ1001. -
Discovery takes the first of
loclizr.config.ts,.mts,.js,.mjsat the project root, unless--confignames one. TypeScript loads throughjiti. -
Fields are re-checked at load time: a failure is
LZ1001 config-invalid, exit 2, hint a pasteable example. -
A field the config does not have is
LZ1001too, so a typo in a JavaScript config, which gets no type check, cannot fall back to the default in silence. The hint names the nearest field:outdir: 'src/i18n' error LZ1001 config-invalid loclizr.config.js`outdir` is not a config field.fix did you mean `outDir`? -
Paths are POSIX, relative to the project root (the CLI’s directory, or
--cwd).
Fields
Section titled “Fields”Catalogs
Section titled “Catalogs”| Field | Type | Default | Meaning |
|---|---|---|---|
locales |
string[] |
every locale catalogs expands to |
Locales to compile, one arm each in every message function. |
sourceLocale |
string |
'en' if found, else the single locale |
Defines the message set, argument types and the end of every fallback chain. |
catalogs |
string |
'locales/{locale}.json' |
Path with tokens, never a glob. |
catalogFormat |
'auto' | 'icu' | 'i18next' |
'auto' |
How each file is read. |
i18nextMarkup |
'literal' | 'tags' |
'literal' |
Tags in i18next files: text, or markup arguments. |
meta |
string | false |
'locales/{sourceLocale}.meta.json' |
Description sidecar. |
Output
Section titled “Output”| Field | Type | Default | Meaning |
|---|---|---|---|
outDir |
string |
'src/loclizr' |
Generated tree, inside the project root. |
record |
string | false |
'locales/loclizr.context.json' |
Context record. |
augmentLocale |
boolean |
true |
Augment LocaleRegistry in loclizr, which types useLocale(). |
groups |
Record<string, string> |
{} |
Group name to key prefix, each a typed record in groups.js. |
identifiers |
Record<string, string> |
{} |
Catalog key to generated identifier. |
Runtime
Section titled “Runtime”| Field | Type | Default | Meaning |
|---|---|---|---|
cookie |
string |
'locale' |
Cookie setLocale writes and the server reads; pass m.cookie in the server options. |
fallback |
'bcp47' | Record<string, string[]> |
'bcp47' |
Where a locale finds a message it lacks. |
formats.timeZone |
string |
unset | IANA zone for every date and time format. |
formats.number |
Record<string, IntlOptions> |
{} |
Named Intl.NumberFormat styles. |
formats.dateTime |
Record<string, IntlOptions> |
{} |
Named Intl.DateTimeFormat styles. |
Scan and severity
Section titled “Scan and severity”| Field | Type | Default | Meaning |
|---|---|---|---|
scan.include |
string[] |
['src/**/*.{ts,tsx,js,jsx,mts,mjs,svelte,vue,astro}'] |
Globs for the usage scan. |
scan.exclude |
string[] |
['**/node_modules/**', '**/dist/**'] |
Skipped globs; outDir always added. |
severity |
Partial<Record<RuleName, Severity>> |
{} |
Rule name to 'off', 'warn' or 'error'. |
IntlOptions is Readonly<Record<string, string | number | boolean>>.
Locales and the source locale
Section titled “Locales and the source locale”| Case | Result |
|---|---|
locales unset |
every locale catalogs expands to, sorted |
Tag Intl.getCanonicalLocales rejects |
LZ1002 locale-tag-invalid, build stops |
| Declared, no catalog | LZ1005 catalog-missing, an error that still emits; the locale falls back to source text |
| Catalog, undeclared | LZ1006 catalog-undeclared at warn, file ignored |
sourceLocale unset, no en.json, not exactly one locale |
LZ1004 source-catalog-missing; the hint is a config file to paste, its locales one line per discovered locale and file |
sourceLocale not in locales |
LZ1001 |
A region tag whose base is undeclared (de-AT without de) |
LZ1018 locale-base-missing at warn: runtime matching truncates subtags, so Accept-Language: de would silently get English |
catalogs
Section titled “catalogs”Exactly one {locale}, at most one {ns}, no {sourceLocale}, no glob characters. Anything else
is LZ1001, the hint naming the character.
- A token matches one path segment or basename stem, never crossing
/or., soen.meta.jsonandloclizr.context.jsonstay out of the locale set. - A resolved
metaorrecordpath the pattern matches isLZ1001: a file is a catalog or an artifact, never both. A record a build already left at such a path is skipped withLZ1006, never read as a locale; delete it. 'public/locales/{locale}/{ns}.json'is thei18next-fs-backend,i18next-http-backendandnext-i18nextdefault. Keys gain the namespace:nav.homeincommon.jsoniscommon.nav.home.
catalogFormat
Section titled “catalogFormat”Decided per file after flattening, never per value. 'icu' and 'i18next' force every file.
'auto' reads a file as |
When |
|---|---|
| i18next | any value contains {{, or any key has a foldable CLDR plural suffix (X_one beside X_other) |
| ICU | otherwise |
- No hybrid parse. ICU syntax in a file read as i18next stays literal text;
{{name}}in a file read as ICU is a literal brace around an argument. {count, plural, ...}in a file classed as i18next is the one mistake'auto'could hide:LZ1020 icu-in-i18next-file, the hint naming the{{or suffixed key that decided.
Importing i18next catalogs has examples.
i18nextMarkup
Section titled “i18nextMarkup”Files read as i18next only.
| Value | Effect |
|---|---|
'literal' |
Tags such as <b>here</b> are escaped to text; each such value raises LZ1016 i18next-markup-literal. |
'tags' |
Catalog-wide, tags lower to markup arguments: the function returns an array of parts, each tag a required handler at the call site. |
Flat-key JSON, per key a description and placeholders map. The path takes {sourceLocale}.
- An entry for a key the source catalog lacks:
LZ1015 meta-orphan. - A
placeholderskey that names no argument of its message:LZ1022 meta-placeholder-orphan. falseswitches descriptions off;LZ3012andLZ5006then hint at settingmeta.- The default stays under
locales/whatevercatalogssays.loclizr initsets it beside catalogs that live elsewhere; a hand-written config moves it the same way:meta: 'lang/{sourceLocale}.meta.json'.
outDir
Section titled “outDir”- Inside the project root and not the root itself (
LZ1007 outdir-unsafe), rechecked through symlinks before each generated file is read or written, inbuildandcheck(LZ5001 output-unwritable). Pruning deletes header-carrying files it did not produce, so give it its own directory. - May not be, or sit above, a directory that holds catalogs (
localesunder the default), nor hold the resolvedmetaorrecordpath (LZ1001): the tree’s self-ignoring.gitignorewould keep them out of commits. Paths compare without case, sooutDir: 'Locales'is refused too. - Moving
outDirleaves an earlier build’s.gitignoreand generated files behind; delete them, or catalogs added later stay ignored.
record
Section titled “record”The path takes {sourceLocale}. false writes no record and skips LZ5003 record-stale and
LZ5007 record-rewritten.
-
The default stays under
locales/whatevercatalogssays.loclizr initsets it beside catalogs that live elsewhere; a hand-written config moves it the same way:record: 'lang/loclizr.context.json'. -
A path the
catalogspattern matches isLZ1001:record: 'locales/context.json' error LZ1001 config-invalid loclizr.config.ts`record` is `locales/context.json`, which the `catalogs` pattern `locales/{locale}.json` also matches.fix a file the pattern matches is either a catalog or the record file, never both. Name one it cannot match, such as 'locales/loclizr.context.json'. Then delete any record a build already wrote at 'locales/context.json'. -
buildwrites over an existing file there only when it parses to an object withschema: 1, or carries merge conflict markers and still has a"schema": 1line of its own. Anything else isLZ5001 output-unwritable, and the file is kept.
cookie
Section titled “cookie”An RFC 6265 name: letters, digits and !#$%&'*+-.^_`|~; anything else is LZ1001, since
document.cookie could never read it back. The client checks it first for the initial locale.
Attributes are fixed: path=/, one year max-age, SameSite=Lax, no Domain, no Secure.
augmentLocale
Section titled “augmentLocale”Controls one block in messages.d.ts:
declare module 'loclizr' { interface LocaleRegistry { locale: AppLocale }}- Types
getLocale(),useLocale()andsetLocale()fromloclizrwith your locale union. - Two trees declaring it in one TypeScript program is TS2717. Set
falsein the second;initdoes when it finds one. See Monorepo. - A tree shipped inside a published package sets
false: the consumer’s own tree augments the same registry, which is TS2717 insidenode_moduleswithskipLibCheckoff. - The barrel’s own
getLocale,setLocaleandlocalesstay typed either way.
groups
Section titled “groups”{ errors: 'errors' } exports every errors. key from groups.js as a frozen errors record,
typed Readonly<{ [K in ErrorsKey]: ... }> so errors[code] checks each member’s arguments.
Prefixes match on a dot boundary: err does not capture errors.forbidden.
| Case | Result |
|---|---|
| No key matches | LZ4004 group-empty |
| Members with differing argument sets | LZ4006 group-args-heterogeneous: a dynamic call must pass the union |
Two groups sharing a name or its PascalCased types (ErrorsKey, ErrorsArgs) |
LZ4001 identifier-collision |
| A group id equal to a grouped message’s id | LZ4001 identifier-collision |
Object as a group id or grouped message id |
LZ4002 identifier-reserved |
identifiers
Section titled “identifiers”identifiers: { 'nav.home': 'navHome' }Replaces the mangled identifier: fixes a collision without renaming a key i18next still reads.
- Passes mangling steps 2 to 4: an illegal character
becomes
_, and a leading digit, reserved word,defaultorthengains a$prefix. LZ4002refuses an override that starts with$and a lowercase letter, names a barrel export (cookie,locales,sourceLocale,getLocale,setLocale,subscribe) or is__proto__,constructororprototype.- Fixes a top-level segment that mangles to
_locale,_formatsor_root, or two that differ only by case (a case-insensitive filesystem merges them). - An entry whose key is no source key, top-level segment or group name is
LZ4007 identifier-orphan, a warning naming the closest one, so a typo or a renamed key is not silently ignored.
fallback
Section titled “fallback”Resolved at build time into each locale’s compiled arm; the runtime never walks a chain.
The default 'bcp47' gives [L, ...declared BCP 47 truncations of L, sourceLocale]: de-AT
resolves through de, then en. Messages a sparse de-AT.json lacks are inherited from de;
only reaching source counts as a missing translation.
fallback: { nb: ['no'] }A map replaces the middle for the locales it names; the rest keep truncation. Source is always last.
formats
Section titled “formats”formats: { timeZone: 'UTC', number: { compact: { notation: 'compact', maximumFractionDigits: 1 } }, dateTime: { weekday: { weekday: 'long', month: 'long', day: 'numeric' } },}Named styles beyond ICU’s own, used as {views, number, compact} or {when, date, weekday}. An
undefined style is LZ2002 icu-style-unknown.
Option sets are checked at load time against Intl with a fixed probe locale, so every machine
agrees. A rejected set is LZ1001, hint from Intl, not a throw in the browser. So is a
non-finite number (NaN, Infinity) as an option value.
timeZone
Section titled “timeZone”Merged into every date and time option set; ICU skeletons cannot carry an IANA zone. Leave it
unset in a browser SPA. Set it when the server renders dates, so both sides of hydration agree. Turn on LZ3011 date-without-timezone (default off)
to list the messages that format a date without one.
- Reads
includeminusexclude, always minusoutDir. - Follows symbolic links, including ones that point outside the project root. A path whose target is not a regular file, such as a FIFO or a device, is skipped.
- Binds generated modules by specifier suffix, no resolver or alias map:
./loclizr/messages,@/loclizr/messagesand#app/loclizr/groupsall bind. Everym.<id>access and bound bare call lands in the record’susage. - Fails nothing by default:
LZ5004 scan-found-nothingwarns only when files were read and none imported the tree,LZ5005 unused-messageisoff, the record gate ignoresusage.
severity
Section titled “severity”severity: { 'ambiguous-source': 'error', 'missing-translation': 'warn', 'unused-message': 'error',}Keys are the rule names printed beside each code. An unknown name is LZ1001, as is naming one
of the three fixed rules, also the only ones that exit 2:
| Rule | Why it is fixed |
|---|---|
LZ1001 config-invalid |
Self-referential: the field that turns it down is the one it validates. |
LZ1007 outdir-unsafe |
A build could write outside the project root and exit 0. |
LZ5001 output-unwritable |
Nothing downstream can act on a tree that is not there. |
- Applied once, at the end: it never changes what is emitted.
- A fatal rule at
warnoroffstill writes no tree and exits 1. It prints as a warning, except a rule raised before the config resolves (LZ1003with no catalogs on disk), which prints as an error. The report names it in anothing generated: 1 fatal (LZ1003)line and, outside--quiet, still closes on the counts line. - Only
LZ3012reads it, to name in its hint the severity it is not running at.
Sharing a config across projects
Section titled “Sharing a config across projects”import { defineConfig } from 'loclizr'import { base } from '@acme/loclizr-config'
export default defineConfig({ ...base, locales: ['en', 'fr'], severity: { ...base.severity, 'missing-translation': 'warn' },})There is no extends: spread a plain object. Spread severity too, or the override drops every
rule base set.
What reaches the runtime
Section titled “What reaches the runtime”Only locales, sourceLocale and cookie, through $configure1 in the generated _locale.js.
loclizr/server takes the same values in NegotiateOptions and never reads the generated tree.