Start here
Quickstart
A fresh Vite app to typed messages, catalog checks, a language switch and a committed record.
Every transcript on this page is pasted from a real run.
-
Create the app
Section titled “Create the app”Terminal window npm create vite@latest my-shop -- --template react-tscd my-shopnpm iNeeds Node 20.19 or newer and nothing else: the CLI loads its
.tsconfig itself. Only the template is Vite-specific; the generated messages are plain ESM for Node, a bundler or React Native. Under Jest, its default CommonJS transform needs a config change first. -
Install, then initialise
Section titled “Install, then initialise”Terminal window npm i loclizrnpx loclizr initnpx loclizr init wrote loclizr.config.tswrote locales/en.jsonInstall loclizr, which loclizr.config.ts and the generated code import:npm i loclizrAdd these scripts to package.json:"predev": "loclizr build --no-fail","prebuild": "loclizr build","pretypecheck": "loclizr build"Add this step to CI, before any step that runs loclizr build:# loclizr check compares the committed context record against this tree.# loclizr build would rewrite that record in the CI workspace and pass.- run: npx loclizr checkloclizr.config.ts import { defineConfig } from 'loclizr'export default defineConfig({sourceLocale: 'en',catalogs: 'locales/{locale}.json',outDir: 'src/loclizr',severity: { 'ambiguous-source': 'error' },})loclizris a regular dependency: generated code imports its locale store at run time, so a Node server needs it installed (the note prints either way).- Install before building: the config imports
loclizr, and without itbuildfails withLZ1001 config-invalid, exit 2 (all fifty-nine codes). initnever overwrites a file and never editspackage.json; it prints the scripts.ambiguous-source(two keys share a source text, one has no description) ships atwarn;initraises it toerror, since a hard gate is free before the first string exists.
-
Add the scripts
Section titled “Add the scripts”package.json "scripts": {"predev": "loclizr build --no-fail","dev": "vite","prebuild": "loclizr build","build": "tsc -b && vite build","lint": "oxlint","preview": "vite preview","pretypecheck": "loclizr build","typecheck": "tsc -b"}Script Runs before Why predevnpm run dev--no-fail: one untranslated key never stops the dev serverprebuildnpm run buildtakes the real exit code pretypechecknpm run typechecktypecheckistsc -b: the template’stsconfig.jsononly lists project references, sotsc -pchecks nothingno preparenpm ciwould rewrite the record before CI’s checkcompares itThe template has no
testscript and loclizr needs none. The complete CI workflow runsnpm testas the app’s own step; add one (Testing) or drop that step.The cost of no
prepare: on a fresh clone./loclizr/messagesis red in the editor untilnpx loclizr build,npm run devornpm run buildruns. -
Write a catalog
Section titled “Write a catalog”locales/en.json {"nav": {"home": "Home","cart": "Cart"},"cart": {"greeting": "Hi {name}, your cart is ready","items": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}"}}initseeds this file. Values are ICU MessageFormat (Catalogs):{name}is a placeholder,{count, plural, ...}picks a form by number,#is the number. Under the defaultcatalogFormat: 'auto', an i18next file can sit beside it.Optional translator notes go in a sidecar you create;
initdoes not write it. A TMS (the translation management system translators work in, such as Crowdin, Lokalise or Phrase) would ship a description written intoen.jsonas copy.locales/en.meta.json {"cart.items": {"description": "Badge under the cart icon on every page.","placeholders": {"count": "Number of line items, not total quantity."}}} -
Terminal window npx loclizr buildnpx loclizr build warn LZ5004 scan-found-nothingThe scan read 2 files and found no import of the generated messages.fix check scan.include in loclizr.config.ts. A usage site is found through an import such as import * as m from './loclizr/messages'wrote src/loclizr (11 files) and locales/loclizr.context.jsoncommit locales/loclizr.context.json; `loclizr check` compares it4 messages, 1 locale (source en), 0 errors, 1 warningOne pass emits code, runs every rule and writes the context record. The warning is expected: nothing imports the messages yet.
Directorylocales
- en.json
- en.meta.json
- loclizr.context.json committed
- loclizr.config.ts
Directorysrc
Directoryloclizr
- .gitignore self-ignoring
- messages.js
- messages.d.ts
Directorymessages
- _formats.js
- _formats.d.ts
- _locale.js
- _locale.d.ts
- cart.js
- cart.d.ts
- nav.js
- nav.d.ts
Your root
.gitignorestays as is. Context record has the schema of the committed file.src/loclizr/messages/cart.d.ts // @generated by loclizr abi=1. Do not edit; run `loclizr build`.import type { MessageOptions } from 'loclizr'/** en: "Hi {name}, your cart is ready" */export declare function cart_greeting(args: { name: string | number }, opts?: MessageOptions): string/** en: "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}" */export declare function cart_items(args: { count: number }, opts?: MessageOptions): stringOne module per top-level key segment;
abi=1versions the four helpers it imports fromloclizr. -
Use the messages
Section titled “Use the messages”src/Cart.tsx import * as m from './loclizr/messages'export function Cart({ count }: { count: number }) {return <p>{m.cart_items({ count })}</p>}Keep it under
src/: the usage scan, which notes where each message is called, readssrc/**/*.{ts,tsx,js,jsx,mts,mjs,svelte,vue,astro}by default (scan.include). Vite takes the extensionless import; Node’s own resolver needs./loclizr/messages.jsand"type": "module"(the template has it).npx loclizr build wrote locales/loclizr.context.jsoncommit locales/loclizr.context.json; `loclizr check` compares it4 messages, 1 locale (source en), 0 errors, 0 warningslocales/loclizr.context.json "usage": [{"file": "src/Cart.tsx","scope": "Cart"}]The warning is gone;
cart.itemsgains this usage entry. Now pass{ count: String(count) }:npm run typecheck > my-shop@0.0.0 pretypecheck> loclizr build4 messages, 1 locale (source en), 0 errors, 0 warnings> my-shop@0.0.0 typecheck> tsc -bsrc/Cart.tsx(4,29): error TS2322: Type 'string' is not assignable to type 'number'.A literal
{ count: '3' }fails too (plusnoUnusedLocalsfor the unread prop). Put the call back. -
Add a locale
Section titled “Add a locale”Add
de.jsonwithoutcart.items:locales/de.json {"nav": {"home": "Startseite","cart": "Warenkorb"},"cart": {"greeting": "Hallo {name}, dein Warenkorb ist bereit"}}npx loclizr build error LZ3001 missing-translation locales/de.json de cart.itemsThe de catalog has no value for this key, so this message renders en text.fix add "cart.items" to locales/de.jsonwarn LZ5007 record-rewritten locales/loclizr.context.json`locales/loclizr.context.json` was rewritten: the committed record's contract differs from the one these catalogs produce.fix commit the rewritten record with the string change, so the context lands in the same pull request.wrote src/loclizr (5 files) and locales/loclizr.context.json4 messages, 2 locales (source en), 1 error, 1 warningfell back to source text: de 1The error exits 1 with output still written:
cart_itemsrenders English inde, the fallback the summary counts.LZ5007fires because the locale list, part of the record’s contract, grew against the committed record (the last build’s; in a repo, the checked-in one).Add the key and both go quiet:
locales/de.json "cart": {"greeting": "Hallo {name}, dein Warenkorb ist bereit","items": "{count, plural, =0 {Dein Warenkorb ist leer} one {# Artikel in deinem Warenkorb} other {# Artikel in deinem Warenkorb}}"}npx loclizr build wrote src/loclizr (1 file) and locales/loclizr.context.jsoncommit locales/loclizr.context.json; `loclizr check` compares it4 messages, 2 locales (source en), 0 errors, 0 warnings -
Switch the language
Section titled “Switch the language”src/Switcher.tsx import { useLocale } from 'loclizr/react'import * as m from './loclizr/messages'export function Switcher() {const locale = useLocale()return (<div>{m.locales.map((tag) => (<button key={tag} type="button" aria-pressed={tag === locale} onClick={() => m.setLocale(tag)}>{tag}</button>))}</div>)}src/App.tsx import * as m from './loclizr/messages'import { Cart } from './Cart'import { Switcher } from './Switcher'function App() {return (<><Switcher /><h1>{m.cart_greeting({ name: 'Ada' })}</h1><Cart count={3} /></>)}export default Appsrc/main.tsx import { StrictMode } from 'react'import { createRoot } from 'react-dom/client'import { useLocale } from 'loclizr/react'import './index.css'import App from './App.tsx'function Root() {return <App key={useLocale()} />}createRoot(document.getElementById('root')!).render(<StrictMode><Root /></StrictMode>,)Messages read the locale when called and subscribe to nothing, so the
keyre-rendersApp(Language switching keeps component state instead).npm run devshowsdeandenbuttons inm.localesorder. ClickdeforHallo Ada, dein Warenkorb ist bereitand3 Artikel in deinem Warenkorb; a reload stays German becausesetLocalewrote a cookie. -
Gate the build
Section titled “Gate the build”In CI run
check, neverbuild: it runs every rulebuilddoes (soLZ3001still fails), writes nothing, and fails when the tree or record on disk is stale. Rewordcart.greetinginen.jsonwithout rebuilding:npx loclizr check error LZ5002 output-stale src/loclizr/messages/cart.d.ts`src/loclizr/messages/cart.d.ts` differs from what this build would write.fix the generated tree on disk is stale; run `loclizr build`.error LZ5002 output-stale src/loclizr/messages/cart.js`src/loclizr/messages/cart.js` differs from what this build would write.fix the generated tree on disk is stale; run `loclizr build`.error LZ5003 record-stale locales/loclizr.context.json`locales/loclizr.context.json` no longer matches the catalogs: the contract this build derived differs from the committed record.fix run `loclizr build` and commit the record with the string change.4 messages, 2 locales (source en), 3 errors, 0 warningsbuildhere would rewrite the record, warnLZ5007and exit 0. Socheckgoes before any step that runsbuild:.github/workflows/ci.yml - run: npm ci- run: npx loclizr checkNo loclizr script runs on install, so
checksees the committed record. Continuous integration has the full workflow.
Next: Catalogs for dates, prices and plurals, React for a link inside a sentence, Continuous integration for the gate, or Importing i18next catalogs for existing translations.