Frameworks
Vanilla TypeScript
Messages in the DOM, a select that switches language, and subscribe re-rendering, with no framework.
A Vite app with no framework: three messages rendered into the DOM, a <select> that switches
language without a reload, and a test that proves the switch. Every framework integration is this
page plus that framework’s way of re-rendering.
-
Create the app
Section titled “Create the app”Terminal window npm create vite@latest app -- --template vanilla-tscd appnpm inpm i loclizrnpx loclizr initinitwritesloclizr.config.tsand a seededlocales/en.json, then prints the scripts to add. The template’scounter.ts,style.cssandassets/go unused oncemain.tsis replaced. -
Add the scripts
Section titled “Add the scripts”package.json "scripts": {"predev": "loclizr build --no-fail","dev": "vite","prebuild": "loclizr build","build": "tsc && vite build","preview": "vite preview","pretypecheck": "loclizr build","typecheck": "tsc","pretest": "loclizr build","test": "vitest run"}typecheckis plaintsc, since the template’stsconfig.jsonis onenoEmitproject, andpretestbuilds the tree the test imports. Quickstart explains the other hooks and why there is noprepare. -
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}}"}}Same keys as the seeded
en.json, German values. -
Render, switch, re-render
Section titled “Render, switch, re-render”src/main.ts import * as m from './loclizr/messages'const app = document.querySelector<HTMLDivElement>('#app')!app.innerHTML = `<select id="locale" aria-label="Language"></select><h1 id="greeting"></h1><p id="items"></p>`const select = app.querySelector<HTMLSelectElement>('#locale')!for (const tag of m.locales) select.add(new Option(tag, tag))select.addEventListener('change', () => m.setLocale(select.value as m.AppLocale))function render(): void {select.value = m.getLocale()document.title = m.nav_cart()app.querySelector('#greeting')!.textContent = m.cart_greeting({ name: 'Ada' })app.querySelector('#items')!.textContent = m.cart_items({ count: 3 })}m.subscribe(render)render()- Each message call reads the locale when it runs, so
renderis the whole update. subscribe(render)calls it after everysetLocale, with no argument. It does not fire on attach, hence the explicitrender()on the last line.select.valueis astring; the cast narrows it to the generatedAppLocaleunion.
- Each message call reads the locale when it runs, so
-
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 && vite buildvite v8.3.2 building client environment for production...transforming...✓ 11 modules transformed.rendering chunks...computing gzip size...dist/index.html 0.37 kB │ gzip: 0.26 kBdist/assets/index-CmgNSv69.js 4.37 kB │ gzip: 1.96 kB✓ built in 85msnpm run previewand pickde: the heading readsHallo Ada, dein Warenkorb ist bereit, the tab titleWarenkorb. -
Test the switch
Section titled “Test the switch”Terminal window npm i -D vitest jsdomsrc/main.test.ts // @vitest-environment jsdomimport { expect, test } from 'vitest'test('a switch re-renders the page in German', async () => {document.body.innerHTML = '<div id="app"></div>'await import('./main')expect(document.querySelector('h1')?.textContent).toBe('Hi Ada, your cart is ready')const select = document.querySelector('select')!select.value = 'de'select.dispatchEvent(new Event('change'))expect(document.querySelector('h1')?.textContent).toBe('Hallo Ada, dein Warenkorb ist bereit')expect(document.querySelector('p')?.textContent).toBe('3 Artikel in deinem Warenkorb')expect(document.title).toBe('Warenkorb')expect(document.documentElement.lang).toBe('de')})main.tsrenders on import, so the test creates#appfirst and imports after. Deletem.subscribe(render)and the German heading assertion fails withReceived: "Hi Ada, your cart is ready".npm test Test Files 1 passed (1)Tests 1 passed (1)
Other frameworks
Section titled “Other frameworks”There is no Svelte, Vue, Solid or Astro entry. Those frameworks wire the same two calls into their
own reactivity: subscribe says when, getLocale says what. useLocale in loclizr/react is
that through useSyncExternalStore.
Every message also takes the locale per call, m.cart_items({ count }, { locale }), which wins
over the store for that one call. Language switching covers it and
the resolution order; Runtime API has every signature.