Guides
Language switching
The locale is read at call time, so a switch needs no reload.
export type AppLocale = 'de' | 'de-AT' | 'en'
export declare function getLocale(): AppLocaleexport declare function setLocale(locale: AppLocale, options?: SetLocaleOptions): voidexport declare function subscribe(listener: () => void): () => voidexport { cookie, locales, sourceLocale } from './messages/_locale.js'The barrel re-exports the one store in loclizr, narrowed to your locales. Outside React, use
subscribe directly; it returns its remover.
import { useLocale } from 'loclizr/react'import * as m from './loclizr/messages'
const offered = [m.sourceLocale, ...m.locales.filter((tag) => tag !== m.sourceLocale)]
export function Switcher() { const locale = useLocale() const names = new Intl.DisplayNames([locale], { type: 'language' }) return ( <div className="switcher"> {offered.map((tag) => ( <button key={tag} type="button" aria-pressed={tag === locale} onClick={() => m.setLocale(tag)}> {names.of(tag) ?? tag} </button> ))} </div> )}m.locales is the frozen tuple ['de', 'de-AT', 'en']; m.sourceLocale is 'en'.
How the locale is resolved
Section titled “How the locale is resolved”First match wins:
- An active server request scope.
- What
setLocalestored. - With a DOM, detected once: the cookie, then
<html lang>. - The source locale.
Matching is exact, then by cutting subtags (de-CH to de). An unknown tag, such as a cookie
edited to sp, renders the source locale, never undefined.
First visit on a static site
Section titled “First visit on a static site”import * as m from './loclizr/messages'
function pickFirstLocale(): void { const chosen = document.cookie.split(';').some((pair) => pair.trim().startsWith('locale=')) if (chosen) return for (const tag of navigator.languages) { const wanted = tag.toLowerCase() const language = wanted.split('-')[0] const match = m.locales.find((known) => known.toLowerCase() === wanted) ?? m.locales.find((known) => known.toLowerCase() === language) if (match !== undefined) { m.setLocale(match) return } }}
pickFirstLocale()With no server, a first visit renders the source locale. v0.1 leaves that gap to you; this helper closes it:
- Call it before the first render.
- It picks the first browser language in
m.locales: exact tag (de-AT), then language subtag (de-CHtode). - It does nothing once a cookie is set.
locale=is the default name; match yourcookiesetting. setLocale(match)persists, so a later browser language change is not re-detected.{ persist: false }re-detects each visit until the visitor picks.
Persistence
Section titled “Persistence”setLocale('de') writes the cookie (cookie in loclizr.config.ts, default locale), sets
document.documentElement.lang, then notifies subscribers. Where the page keeps cookies, the cookie
is the only persistence, since a server reads it back.
| Case | Effect |
|---|---|
{ persist: false } |
this page load only |
file:// page (Electron loadFile) or cookies blocked |
this page load only; persist the choice yourself and call setLocale on startup. Outside production loclizr warns once: loclizr: the locale cookie was not stored (file:// or blocked), so the choice ends on reload. Persist it and call setLocale() on startup. |
No document, React Native included |
memory only, subscribers still notified |
Cookie attributes are fixed in v0.1: path=/, max-age one year, SameSite=Lax, no Domain, no
Secure. No Domain means host-scoped: shop.example.com and help.example.com do not share a
choice.
setLocale never sets dir; for an RTL locale set it yourself from useLocale() or a subscribe
listener.
After a reload following a persisted switch, the store reads the cookie and sets <html lang> to
match, so a client-only app sees no <html lang> warning; React covers the
server-rendered case.
The per-call override
Section titled “The per-call override”m.cart_items({ count: 3 }, { locale: 'de' }) // "3 Artikel in deinem Warenkorb"A second-argument { locale } beats the store for that call. The switcher below works this way, so the
page around it keeps its own language:
Hello, Ada!
3 items in your cart
Total: €73.50
On its way
Updated Oct 3, 2026
m.demo_items({ count: 3 }, { locale: 'en' })What re-renders, and what does not
Section titled “What re-renders, and what does not”Generated code imports nothing from React, so a component that only calls messages updates when its parent next renders:
| Pattern | On a switch | Local state |
|---|---|---|
| Key the root on the locale | subtree remounts, every string recomputed | lost below the key |
Pass { locale } per call |
component re-renders in place | kept |
Combine them: key the root for the bulk, pass per call where state must survive.
Key the root on the locale
Section titled “Key the root on the locale”function Shop() { const locale = useLocale() const [cart, setCart] = useState(openingCart) return ( <> <App key={locale} cart={cart} onChange={(patch) => setCart((current) => ({ ...current, ...patch }))} /> <CourierNote /> </> )}cart survives the remount because it lives above the key, in Shop; App’s own useState
would not.
Pass the locale per call
Section titled “Pass the locale per call”export function CourierNote() { const locale = useLocale() const [note, setNote] = useState('') return ( <section className="note"> <label htmlFor="note">{m.app_note({}, { locale })}</label> <textarea id="note" value={note} placeholder={m.app_notePlaceholder({}, { locale })} onChange={(event) => setNote(event.target.value)} /> <p>{m.app_noteKeeps({}, { locale })}</p> </section> )}CourierNote, outside the key, re-renders in place and keeps the typed note. React
has the React Compiler reason for this form.