Reference
Runtime API
Every export of the four entry points, with its signature and one call.
| Entry | Holds |
|---|---|
loclizr |
the locale store shared by generated code and your app |
loclizr/react |
three bindings over the store |
loclizr/server |
request scoping and Accept-Language negotiation |
loclizr/compiler |
the build behind the CLI |
Signatures are quoted from the published .d.ts; snippets ran against the example’s tree.
loclizr
Section titled “loclizr”export declare function defineConfig(config: LoclizrConfig): LoclizrConfigexport declare function getLocale(): Localeexport declare function setLocale(locale: Locale, options?: SetLocaleOptions): voidexport declare function subscribe(listener: LocaleListener): () => voiddefineConfig
Section titled “defineConfig”import { defineConfig } from 'loclizr'
export default defineConfig({ locales: ['en', 'de', 'de-AT'], sourceLocale: 'en' })An identity function for typing. Every field is still re-checked at load.
getLocale
Section titled “getLocale”import { getLocale } from 'loclizr'import * as m from './loclizr/messages'
getLocale() // 'en'm.nav_home() // 'Home'Returns a registered locale, reading in order:
- the active server request scope
- the tag
setLocalestored - with a
document, the configured cookie, then<html lang>, read once
Then it matches by exact tag and subtag truncation, falling back to the source locale.
- A locale read from the cookie is also written to
<html lang>, matched against the list the first generated tree registers. OncesetLocalehas run, that write is skipped. m.getLocale(), typed asAppLocale, is the usual import.- The first generated
_locale.jsto load registers the list. Called before any message module loads, it returns the stored tag or'en'and warns once outside production, naming the missing import. - On a server outside a request scope it returns the source locale, so a prerender gets source
text, not a 500. Where
loclizr/serveris loaded, it warns once to wrap the whole request inrunWithLocale; a message call outside a scope prints the same warning. - Warnings sit behind
process.env.NODE_ENV. Where there is noprocessglobal and no bundler define (a plain<script type="module">page, a Worker without Node compatibility), they are skipped, never thrown.
setLocale
Section titled “setLocale”import { getLocale, setLocale, subscribe } from 'loclizr'import * as m from './loclizr/messages'
const off = subscribe(() => console.log('now', getLocale()))setLocale('de') // logs: now dem.nav_home() // 'Startseite'off()Stores the tag and notifies subscribers. With a document it also sets <html lang> and the
cookie (path=/, one year, SameSite=Lax).
| Case | Behaviour |
|---|---|
{ persist: false } |
skips the cookie |
file:// page or cookies blocked |
updates and notifies, keeps nothing past reload; warns once outside production |
| No DOM (React Native) | updates and notifies in memory, persists nothing |
| Inside a request scope | throws setLocale() cannot run inside a request scope. Pass { locale } per call instead. |
subscribe
Section titled “subscribe”import { subscribe } from 'loclizr'import * as m from './loclizr/messages'
const unsubscribe = subscribe(() => document.title = m.app_cart())unsubscribe()Called after every setLocale; returns the remover. Listeners take no arguments, so read
getLocale() inside.
| Type | Definition |
|---|---|
Locale |
LocaleRegistry extends { locale: infer L extends string } ? L : string: your union once a generated tree is in the program, string before. |
LocaleRegistry |
Empty interface; messages.d.ts adds locale: AppLocale. |
MessageOptions |
{ readonly locale?: Locale | undefined }, a message’s second parameter. |
EmptyArgs |
{ readonly [noArguments]?: never }, keyed by a symbol loclizr does not export, for a message without arguments: f() and f({}) compile, f({ x: 1 }) does not, and completion inside f({ }) offers nothing. |
SetLocaleOptions |
{ readonly persist?: boolean | undefined } |
LocaleListener |
() => void |
LocaleResolver |
(options?: MessageOptions | undefined) => string, the type of $l |
LocaleSetup |
{ locales, sourceLocale, cookie }, all required, passed to $configure1 |
NegotiateOptions |
{ locales, sourceLocale, cookie? }, taken by loclizr/server |
IntlOptions |
Readonly<Record<string, string | number | boolean>> |
LoclizrConfig |
the config shape, see Configuration |
Internal: the $ helpers
Section titled “Internal: the $ helpers”export declare function $configure1(setup: LocaleSetup): LocaleResolverexport declare function $plural1(locale: string, value: number, ordinal: boolean): stringexport declare function $number1(locale: string, value: number, options: IntlOptions): stringexport declare function $dateTime1(locale: string, value: Date | number, options: IntlOptions): stringGenerated code’s ABI; apps never call them.
| Helper | Does |
|---|---|
$configure1 |
registers the locale list (first wins), returns the resolver bound to that tree’s list |
$plural1 |
caches one Intl.PluralRules per locale and type |
$number1, $dateTime1 |
cache formatters by options identity, hence _formats.js |
The trailing digit is the version. A breaking change renames the helper; the old name stays one minor, then a stale tree fails at link time.
loclizr/react
Section titled “loclizr/react”export declare function useLocale(): Localeexport declare function useSetLocale(): (locale: Locale, options?: SetLocaleOptions) => voidexport declare function Parts(props: { readonly of: readonly (string | ReactNode)[];}): ReactElementexport type { Locale, SetLocaleOptions }No 'use client' directive in v0.1; add one on your own file if needed.
useLocale
Section titled “useLocale”import { useLocale } from 'loclizr/react'import * as m from './loclizr/messages'
function Root() { return <App key={useLocale()} /> // whole tree remounts on a switch}
function Draft({ count }: { count: number }) { const locale = useLocale() return <p>{m.cart_items({ count }, { locale })}</p> // re-renders in place, keeps state}useSyncExternalStore(subscribe, getLocale, getLocale) with module-level references, so no
resubscribe per render and the snapshot is a primitive string. It is the only subscription: a
component calling messages without it re-renders only with its parent.
Outside production it compares <html lang> with the resolved locale, warns once when they differ
by more than letter case (naming the Content-Language header withLocale sets) or when the
attribute is missing, and assigns it. See React.
useSetLocale
Section titled “useSetLocale”import { useLocale, useSetLocale } from 'loclizr/react'import * as m from './loclizr/messages'
function Switcher() { const locale = useLocale() const setLocale = useSetLocale() return ( <select value={locale} onChange={(e) => setLocale(e.target.value as typeof locale)}> {m.locales.map((l) => <option key={l} value={l}>{l}</option>)} </select> )}Returns setLocale, so a switcher reads as a hook pair.
import { Parts } from 'loclizr/react'import * as m from './loclizr/messages'
<Parts of={m.terms_accept({ link: (chunks) => <a href="/terms">{chunks}</a> })} />The whole component is createElement(Fragment, null, ...props.of), so Parts itself needs no
keys. Once a translation nests tags, a JSX handler logs React’s dev-only key warning;
createElement('a', { href: '/terms' }, ...chunks) avoids it.
loclizr/server
Section titled “loclizr/server”export declare function runWithLocale<T>(locale: string, fn: () => T): Texport declare function withLocale<A extends unknown[]>( handler: (request: Request, ...rest: A) => Response | Promise<Response>, options: NegotiateOptions,): (request: Request, ...rest: A) => Promise<Response>export declare function withLocale<A extends unknown[]>( handler: (request: Request, ...rest: A) => Response | undefined | Promise<Response | undefined>, options: NegotiateOptions,): (request: Request, ...rest: A) => Promise<Response | undefined>export declare function negotiate(accepted: readonly string[], options: NegotiateOptions): stringexport declare function localeFromRequest(request: Request, options: NegotiateOptions): stringexport declare function localeFromHeaders(headers: { readonly cookie?: string | undefined; readonly acceptLanguage?: string | undefined;}, options: NegotiateOptions): stringexport type { NegotiateOptions }Negotiating functions take what they need in NegotiateOptions (usually m.locales,
m.sourceLocale and m.cookie), so loclizr/server never imports the tree.
runWithLocale
Section titled “runWithLocale”import { runWithLocale } from 'loclizr/server'import { getLocale } from 'loclizr'import * as m from './loclizr/messages'
runWithLocale('de-AT', () => [getLocale(), m.nav_cart()]) // ['de-AT', 'Einkaufswagerl']Runs fn in an AsyncLocalStorage scope; every getLocale() and message call on that async
path resolves to locale.
- The handle lives in
globalThis[Symbol.for('loclizr.locale')], so two module graphs share one scope (two graphs is how Next serves the wrong language under concurrency elsewhere). - Express, Fastify and Pages Router handlers call it after
localeFromHeaders.
withLocale
Section titled “withLocale”import { withLocale } from 'loclizr/server'import * as m from './loclizr/messages'
const handler = withLocale( async (request) => new Response(m.nav_cart()), { locales: m.locales, sourceLocale: m.sourceLocale, cookie: m.cookie },)
const response = await handler(new Request('http://x/', { headers: { cookie: 'locale=de' } }))await response.text() // 'Warenkorb'response.headers.get('content-language') // 'de'response.headers.get('vary') // 'Accept-Language, Cookie'Wraps a Fetch handler: negotiates, runs it inside runWithLocale, sets Content-Language and
adds Accept-Language and Cookie to Vary. A Response.error() passes through untouched, and
so does the undefined a Bun handler returns after server.upgrade(request).
The scope covers the handler and the async work it starts; a body produced as the client reads it can fall outside it. See Bodies produced later.
negotiate
Section titled “negotiate”negotiate(['fr', 'de-CH;q=0.8', 'en;q=0.5'], { locales: m.locales, sourceLocale: 'en', cookie: m.cookie }) // 'de'Hand-rolled RFC 4647 lookup over Accept-Language ranges:
- Sort by
q, then order; dropq=0and*. Aqabove 1 counts as 1, and aqthat is not a plain decimal counts asq=0. - Match each by exact tag, then subtag truncation.
- First match wins; none yields
sourceLocale.
localeFromHeaders
Section titled “localeFromHeaders”const options = { locales: m.locales, sourceLocale: 'en', cookie: m.cookie }
localeFromHeaders({ cookie: 'locale=de-AT; theme=dark', acceptLanguage: 'en' }, options) // 'de-AT'localeFromHeaders({ acceptLanguage: 'de;q=0.9, en;q=0.8' }, options) // 'de'localeFromHeaders({}, options) // 'en'The primitive: the cookie named by options.cookie (locale when omitted; pass m.cookie to match
the config), then Accept-Language, then the source locale.
- An unmatched cookie yields the source locale, not
Accept-Language, so server and client agree across hydration. - Takes two optional strings, not
Headers. Express passes{ cookie: req.headers.cookie, acceptLanguage: req.headers['accept-language'] }.
localeFromRequest
Section titled “localeFromRequest”localeFromRequest(new Request('http://x/', { headers: { 'accept-language': 'de-AT' } }), options) // 'de-AT'localeFromHeaders over the request’s headers: one implementation, no drift.
loclizr/compiler
Section titled “loclizr/compiler”export interface BuildOptions { readonly cwd?: string | undefined readonly configPath?: string | undefined readonly emit?: boolean | undefined readonly maxWarnings?: number | undefined readonly failOnError?: boolean | undefined}export declare function build(options?: BuildOptions): Promise<BuildResult>export declare function check(options?: BuildOptions): Promise<BuildResult>The CLI is a thin layer over these. build writes the tree and record; check writes nothing and
adds the LZ5002 and LZ5003 gates. Neither prints; reporters belong to the CLI.
| Option | Default | Meaning |
|---|---|---|
cwd |
process.cwd() |
project root |
configPath |
discovery | config file relative to cwd |
emit |
true |
false writes nothing, even on build; files and record are still filled |
maxWarnings |
no cap; any negative value is also no cap | --max-warnings |
failOnError |
true |
false is build --no-fail; check ignores it |
import { build } from 'loclizr/compiler'
const result = await build({ cwd: root, failOnError: false })result.summary // { errors: 0, warnings: 0, messages: 26, locales: 3, fellBack: [] }result.exitCode // 0result.written.length // 21 on a first run, 0 when nothing changedThe example’s Vite plugin makes this call on a catalog change. check has the same shape:
import { check } from 'loclizr/compiler'
const gate = await check({ cwd: root })gate.exitCode // 0 on a clean, committed treegate.diagnostics.map((d) => d.code) // []gate.written // []BuildResult
Section titled “BuildResult”| Field | Type | Meaning |
|---|---|---|
ok |
boolean |
no error diagnostic after re-levelling |
exitCode |
0 | 1 | 2 |
the CLI’s exit code |
program |
Program | null |
config, sourceLocale, locales, messages, extras, groups, usages, diagnostics; null if the run halted before reading catalogs |
diagnostics |
Diagnostic[] |
sorted, re-levelled, as the JSON reporter prints them |
files |
EmittedFile[] |
every { path, contents }, relative to outDir, even with emit: false |
record |
ContextRecord | null |
null with record: false or a halted run |
written |
string[] |
paths written this run, relative to the project root; empty for check |
summary |
Summary |
{ errors, warnings, messages, locales, fellBack } |
Diagnostic,Summary,ContextRecord,ProgramandEmittedFileare exported types.Diagnosticmatches the JSON reporter;ContextRecordis the context record.program.usageskeeps the fullUsageSite(file, line, column, scope, snippet); the committed record keeps only file and scope.