Start here
Introduction
What loclizr is, what it refuses to do, and who it is for.
import * as m from './loclizr/messages'
m.cart_greeting({ name: 'Ada' }) // "Hi Ada, your cart is ready"m.cart_items({ count: 3 }) // "3 items in your cart"m.cart_items({ count: '3' }) // type error: count is a numberloclizr is a command line compiler. loclizr build reads JSON catalogs under locales/ and
writes typed ESM message functions into your source tree.
- No bundler plugin, no AST pass: nothing rewrites application code, so a framework’s next compiler release has no transform order to break.
- One output runs in a Vite SPA, a server render, React Native and plain Node.
- ICU MessageFormat, the catalog syntax for arguments and plurals (Catalogs), is parsed at build time; the browser ships no parser.
- The locale is read at call time, so switching language needs no reload.
What one build produces
Section titled “What one build produces”| Output | What it gives you |
|---|---|
| Message modules | One module per top-level key segment. A function’s first parameter is exactly its ICU argument set, so a renamed placeholder, or a plural argument handed a string, fails typecheck at the call site instead of rendering undefined. |
| Diagnostics | Fifty-nine rules with stable codes, on from the first build; an error exits 1. All but three re-level to off, warn or error in config. |
| Context record | locales/loclizr.context.json, committed. Per message: canonical source text and its hash, argument names and types, plural or select shape, the file and function that use it, per-locale status, and your description if you wrote one. |
Checking is part of build, not a step you wire up. It catches missing and blank
translations, ICU syntax errors, arguments that differ between locales, plural categories a locale
can never select, and keys differing only by a homoglyph or an invisible joiner. A missing translation renders the
source text and is reported.
No timestamps or line numbers: the record moves when the contract moves, in the string edit’s pull request, for whoever translates next (a person, a model or a TMS).
Who it is for
Section titled “Who it is for”A new app gets a hard gate from its first commit; an app with English literals in JSX retrofits by hand, below.
On i18next? Point catalogs at your files (Importing i18next catalogs).
Format is decided per file, so i18next and ICU files mix, and nothing is written back: i18next
keeps serving the app while you read the diagnostics.
Retrofitting by hand
Section titled “Retrofitting by hand”No extractor: run this loop one screen at a time.
-
Pick a literal and a key
Section titled “Pick a literal and a key”checkout.titlefor the checkout heading. One top-level segment per screen is one module (messages/checkout.js), so a deleted screen takes its module with it. -
Move the string
Section titled “Move the string”Put it in
locales/en.jsonunder that key. -
Call the function
Section titled “Call the function”Replace the literal with
m.checkout_title(), orm.checkout_items({ count })for a plural. -
Build and typecheck
Section titled “Build and typecheck”loclizr build, typecheck, next literal. -
Turn on
Section titled “Turn on unused-message”unused-messageWhen the last literal is gone, raise
LZ5005, which shipsoff, underseverityinloclizr.config.ts:severity: { 'unused-message': 'warn' }It lists keys with no call found, and fires only once the scan finds a generated import, so a wrong glob costs one warning, not one per key.
What it deliberately does not do
Section titled “What it deliberately does not do”| Not in v0.1 | What that means |
|---|---|
| Per-locale delivery | Every locale is inlined into each used message function; bundle size grows with locale count. Paraglide ships one locale per request. |
| Extraction, codemod | Nothing reads or rewrites your components; catalogs are authored or imported. |
| A second framework | React with Vite is the tested example; the message layer is framework free. Next.js is not first-class; next-intl is built for it. Servers: Server rendering. |
Runtime catalogs or format() |
A translation fix is a commit and a deploy. A TMS that opens pull requests fits; a dashboard for fixing a production typo live does not. |
--watch |
loclizr build --no-fail in predev, plus a few inline lines in the example’s own vite.config.ts that rebuild on catalog edits (How it works). |
| Stability | 0.x: a minor release may break. |
Next: Quickstart. Honest limits explains each row; Comparison sets loclizr beside its nearest tools.