Skip to content
loclizr
src/loclizr/messages.d.ts
export type AppLocale = 'de' | 'de-AT' | 'en'
export declare function getLocale(): AppLocale
export declare function setLocale(locale: AppLocale, options?: SetLocaleOptions): void
export declare function subscribe(listener: () => void): () => void
export { 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.

src/Switcher.tsx
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'.

First match wins:

  1. An active server request scope.
  2. What setLocale stored.
  3. With a DOM, detected once: the cookie, then <html lang>.
  4. 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.

src/main.tsx
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-CH to de).
  • It does nothing once a cookie is set. locale= is the default name; match your cookie setting.
  • setLocale(match) persists, so a later browser language change is not re-detected. { persist: false } re-detects each visit until the visitor picks.

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.

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' })

setLocale writes the store, which sets the cookie and notifies useLocale subscribers. A root keyed on the locale remounts; a component passing the locale per call re-renders in place. Message functions read the store at call time either way.

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.

src/main.tsx
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.

src/CourierNote.tsx
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.