Frameworks
Solid
A Solid signal over the locale store, so messages update on a switch.
A Vite and Solid app whose messages switch language in place, with a test that proves it. There
is no loclizr/solid entry: one small file wires subscribe and getLocale into a signal.
-
Create the app
Section titled “Create the app”Terminal window npm create vite@latest app -- --template solid-ts --no-interactivecd appnpm inpm i loclizrnpx loclizr initinitwritesloclizr.config.tsandlocales/en.json, and prints three scripts. Quickstart walks through its output. -
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","preview": "vite preview","pretypecheck": "loclizr build","typecheck": "tsc -b","test": "vitest run"},typecheckistsc -b: the template’stsconfig.jsononly lists project references, sotsc -pchecks nothing. -
Add a second locale
Section titled “Add a second locale”locales/de.json {"nav": {"home": "Startseite","cart": "Warenkorb"},"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}}"}} -
Wrap the store in a signal
Section titled “Wrap the store in a signal”src/locale.ts import { createSignal, onCleanup, type Accessor } from 'solid-js'import * as m from './loclizr/messages'export function createLocale(): Accessor<m.AppLocale> {const [locale, setLocale] = createSignal(m.getLocale())onCleanup(m.subscribe(() => setLocale(m.getLocale())))return locale}subscribedoes not call the listener on attach, so the signal starts fromgetLocale(). CallcreateLocale()inside a component:onCleanupremoves the listener when it unmounts. -
Pass the locale per call
Section titled “Pass the locale per call”src/App.tsx import { createLocale } from './locale'import * as m from './loclizr/messages'import { Cart } from './Cart'import { Switcher } from './Switcher'export default function App() {const locale = createLocale()return (<><Switcher /><h1>{m.cart_greeting({ name: 'Ada' }, { locale: locale() })}</h1><Cart count={3} /></>)}src/Cart.tsx import { createLocale } from './locale'import * as m from './loclizr/messages'export function Cart(props: { count: number }) {const locale = createLocale()return <p>{m.cart_items({ count: props.count }, { locale: locale() })}</p>}src/Switcher.tsx import { For } from 'solid-js'import { createLocale } from './locale'import * as m from './loclizr/messages'export function Switcher() {const locale = createLocale()return (<div><For each={m.locales}>{(tag) => (<button type="button" aria-pressed={tag === locale()} onClick={() => m.setLocale(tag)}>{tag}</button>)}</For></div>)}A Solid component runs once; only a JSX expression reruns, and only when it reads a signal.
{ locale: locale() }is that read, so the text updates in place and component state survives. -
Or key the root
Section titled “Or key the root”src/KeyedRoot.tsx import { Show } from 'solid-js'import { createLocale } from './locale'import * as m from './loclizr/messages'function Greeting() {return <h1>{m.cart_greeting({ name: 'Ada' })}</h1>}export function KeyedRoot() {const locale = createLocale()return (<Show when={locale()} keyed><Greeting /></Show>)}To make it the root instead,
src/index.tsxbecomes:src/index.tsx /* @refresh reload */import { render } from 'solid-js/web'import './index.css'import { KeyedRoot } from './KeyedRoot'const root = document.getElementById('root')render(() => <KeyedRoot />, root!)<Show keyed>rebuilds its children wheneverlocale()changes, discarding their local state, so calls below it need no option. The steps below keep<App />as the root, and the test covers both. -
npm run build > app@0.0.0 prebuild> loclizr buildwrote src/loclizr (11 files) and locales/loclizr.context.jsoncommit locales/loclizr.context.json; `loclizr check` compares it4 messages, 2 locales (source en), 0 errors, 0 warnings> app@0.0.0 build> tsc -b && vite buildvite v8.3.2 building client environment for production...transforming...✓ 18 modules transformed.rendering chunks...computing gzip size...dist/index.html 0.45 kB │ gzip: 0.29 kBdist/assets/index-nqMpL4T3.css 1.78 kB │ gzip: 0.81 kBdist/assets/index-BY-jzfPm.js 15.70 kB │ gzip: 6.36 kB✓ built in 494msprebuildruns the compiler first, andtsc -bthen checks every message call against its arguments. Commitlocales/loclizr.context.jsonas the output says. -
Test the switch
Section titled “Test the switch”Terminal window npm i -D vitest jsdom @solidjs/testing-libraryvite.config.ts import { defineConfig } from 'vitest/config'import solid from 'vite-plugin-solid'export default defineConfig({plugins: [solid()],test: { environment: 'jsdom' },})src/App.test.tsx import { afterEach, expect, test } from 'vitest'import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'import * as m from './loclizr/messages'import App from './App'import { KeyedRoot } from './KeyedRoot'afterEach(() => {cleanup()m.setLocale('en')})test('per-call locale updates in place', () => {render(() => <App />)expect(screen.getByRole('heading').textContent).toBe('Hi Ada, your cart is ready')fireEvent.click(screen.getByRole('button', { name: 'de' }))expect(screen.getByRole('heading').textContent).toBe('Hallo Ada, dein Warenkorb ist bereit')expect(screen.getByText('3 Artikel in deinem Warenkorb')).toBeTruthy()})test('keyed root rebuilds on setLocale', () => {render(() => <KeyedRoot />)m.setLocale('de')expect(screen.getByRole('heading').textContent).toBe('Hallo Ada, dein Warenkorb ist bereit')})test('a bare call reads no signal and stays stale', () => {render(() => <h1>{m.cart_greeting({ name: 'Ada' })}</h1>)m.setLocale('de')expect(screen.getByRole('heading').textContent).toBe('Hi Ada, your cart is ready')})npx vitest run --reporter verbose RUN v5.0.3 /path/to/app✓ src/App.test.tsx > per-call locale updates in place 125ms✓ src/App.test.tsx > keyed root rebuilds on setLocale 5ms✓ src/App.test.tsx > a bare call reads no signal and stays stale 5msTest Files 1 passed (1)Tests 3 passed (3)Start at 21:21:32Duration 780ms (environment 54%, tests 19%, transform 18%, import 8%)The third test is the trap: a message called with no signal read renders once and stays in the old language.
cleanup()is explicit because the testing library only registers it itself when vitest globals are on.
setLocale writes the locale cookie and <html lang>, so a reload keeps the choice; see
Language switching. SolidStart is not covered here: on the server,
read the locale per request as Server rendering shows.
Runtime API has every signature.