Guides
Dynamic keys
A typed lookup for a key chosen at run time from a known set.
import { defineConfig } from 'loclizr'
export default defineConfig({ locales: ['en', 'de', 'de-AT'], sourceLocale: 'en', groups: { errors: 'errors' },})Looking up an API error code as m['errors_' + code] loses the types and hides the call from the
usage scan. A group gives a typed record and key union.
-
Declare a prefix
Section titled “Declare a prefix”Map a group name to a key prefix under
groups, as above. It matches on a dot boundary:errorscaptureserrors.forbidden, nevererrorsforbidden. -
loclizr buildwritesgroups.jsandgroups.d.tsbeside the barrel. -
Index the record
Section titled “Index the record”src/Problems.tsx import { errors, type ErrorsKey } from './loclizr/groups'const problems: readonly ErrorsKey[] = ['forbidden', 'not_found', 'rate_limited']export function Problems({ seconds }: { seconds: number }) {return (<ul>{problems.map((code) => (<li key={code}>{errors[code]({ seconds })}</li>))}</ul>)}
What is generated
Section titled “What is generated”import { errors_forbidden, errors_not_found, errors_rate_limited } from './messages/errors.js'
export const errors = /*#__PURE__*/ Object.freeze({ __proto__: null, forbidden: errors_forbidden, not_found: errors_not_found, rate_limited: errors_rate_limited,})export type ErrorsKey = 'forbidden' | 'not_found' | 'rate_limited'export interface ErrorsArgs { forbidden: EmptyArgs not_found: EmptyArgs rate_limited: { seconds: number }}export declare const errors: Readonly<{ [K in ErrorsKey]: (args: ErrorsArgs[K], opts?: MessageOptions) => string}>- Each member is the key minus prefix and dot.
- Null prototype: a member named
constructorcannot reachObject.prototype. - Type names are the group name in PascalCase plus
KeyandArgs:nav.maingivesNavMainKey.
Typing
Section titled “Typing”| Key | Typing |
|---|---|
Literal, errors.rate_limited(...) |
Exact: { seconds: 30 } compiles, { nope: 1 } does not. |
Union, errors[code](...) |
Every argument any member takes is required. |
Calling a union of signatures intersects their parameters, so errors[code]() and
errors[code]({}) both fail on seconds. Sound, but one new member taking { retryAt } breaks
every existing dynamic call site.
Mixed argument sets
Section titled “Mixed argument sets”Members that disagree warn. A group that mixes them on purpose turns the rule off, as the example app does:
import { defineConfig } from 'loclizr'
export default defineConfig({ locales: ['en', 'de', 'de-AT'], sourceLocale: 'en', groups: { errors: 'errors' }, severity: { 'group-args-heterogeneous': 'off' },})With the rule on, the same build prints:
warn LZ4006 group-args-heterogeneous
The group "errors" has members with different argument sets, so every dynamic call site must pass the union: seconds.
errors.forbidden takes no arguments errors.not_found takes no arguments errors.rate_limited takes seconds
fix Split the group by argument shape, or give the odd members bare {x} arguments.An empty group
Section titled “An empty group”error LZ4004 group-empty
The group "nope" matched no keys under the prefix "nope".
fix A prefix matches on a dot boundary, so "nope" captures "nope.forbidden" and never "nopeforbidden". Check the prefix.A prefix that matches nothing is an error. The build still emits NopeKey = never, so every call
through it fails typecheck too.
Tree-shaking
Section titled “Tree-shaking”- Using a group retains all its members; an untouched group drops like any unused export.
- The barrel does not re-export
groups.js: only an app importing it pays. Messages called by name stay individually tree-shaken. - The usage scan counts
errors.forbidden(...)for that message anderrors[code](...)for every member, so a message reached only dynamically never reads as unused.
Content unknown at build time
Section titled “Content unknown at build time”A CMS title or user-typed text is not a message and belongs outside the catalog. A runtime
format() for it would ship an ICU parser to the browser; it is deferred (Limits).