Frameworks
Svelte
Svelte 5 runes over subscribe and getLocale, and the two ways to make markup follow a switch.
A Vite and Svelte 5 app whose markup follows setLocale, with a test that proves it. There is no
loclizr/svelte entry: an eleven-line module over subscribe and getLocale is the whole binding.
-
Create the app
Section titled “Create the app”Terminal window npm create vite@latest app -- --template svelte-tscd appnpm inpm i loclizrnpx loclizr initinitwrotelocales/en.jsonwith the Quickstart messages. Add German: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}}"}} -
Add the scripts
Section titled “Add the scripts”package.json "scripts": {"predev": "loclizr build --no-fail","dev": "vite","prebuild": "loclizr build","build": "vite build","preview": "vite preview","pretypecheck": "loclizr build","typecheck": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json","pretest": "loclizr build","test": "vitest run"}The template calls its type check
check; renaming ittypecheckletspretypecheckrun first.pretestgeneratessrc/loclizrbefore the mount test in the last step. -
Wrap the store in a rune
Section titled “Wrap the store in a rune”src/locale.svelte.ts import { createSubscriber } from 'svelte/reactivity'import { getLocale, subscribe, type AppLocale } from './loclizr/messages'const track = createSubscriber(subscribe)export const locale = {get current(): AppLocale {track()return getLocale()},}createSubscriberwants(update) => cleanup, which is the shape of loclizr’ssubscribe, so it passes straight in. Any$derived, effect or markup that readslocale.currentreruns on a switch. -
Add a switcher
Section titled “Add a switcher”src/Switcher.svelte <script lang="ts">import * as m from './loclizr/messages'import { locale } from './locale.svelte'</script><div>{#each m.locales as tag (tag)}<button type="button" aria-pressed={tag === locale.current} onclick={() => m.setLocale(tag)}>{tag}</button>{/each}</div> -
Make the markup follow
Section titled “Make the markup follow”src/App.svelte <script lang="ts">import * as m from './loclizr/messages'import { locale } from './locale.svelte'import Cart from './Cart.svelte'import Switcher from './Switcher.svelte'</script><Switcher />{#key locale.current}<h1>{m.cart_greeting({ name: 'Ada' })}</h1>{/key}<Cart />src/Cart.svelte <script lang="ts">import * as m from './loclizr/messages'import { locale } from './locale.svelte'let count = $state(3)const items = $derived(m.cart_items({ count }, { locale: locale.current }))</script><p>{items}</p><button type="button" onclick={() => count++}>+1</button>A bare
{m.nav_cart()}never reruns: the message reads the store, and the store is not a rune. Pick one of the two forms:Form On a switch {#key locale.current}remounts the block; component state inside it resets { locale: locale.current }per callreruns that expression in place; state survives -
Test the switch
Section titled “Test the switch”Terminal window npm i -D vitest jsdomvite.config.ts import { svelte } from '@sveltejs/vite-plugin-svelte'import { defineConfig } from 'vitest/config'export default defineConfig({plugins: [svelte()],resolve: process.env.VITEST ? { conditions: ['browser'] } : undefined,test: { environment: 'jsdom' },})Without the
browsercondition vitest loads Svelte’s server build andmountthrowslifecycle_function_unavailable.src/App.test.ts import { afterEach, expect, test } from 'vitest'import { flushSync, mount, unmount } from 'svelte'import * as m from './loclizr/messages'import App from './App.svelte'let app: ReturnType<typeof mount> | undefinedafterEach(() => {if (app) unmount(app)m.setLocale('en')flushSync()})test('a switch re-renders the heading and keeps the cart count', () => {app = mount(App, { target: document.body })document.querySelector<HTMLButtonElement>('p + button')?.click()flushSync()expect(document.querySelector('h1')?.textContent).toBe('Hi Ada, your cart is ready')expect(document.querySelector('p')?.textContent).toBe('4 items in your cart')m.setLocale('de')flushSync()expect(document.querySelector('h1')?.textContent).toBe('Hallo Ada, dein Warenkorb ist bereit')expect(document.querySelector('p')?.textContent).toBe('4 Artikel in deinem Warenkorb')})setLocalewrites the cookie in jsdom too, soafterEachswitches back; then runnpm test:npm test Test Files 1 passed (1)Tests 1 passed (1)
A store for $locale
Section titled “A store for $locale”import { readable } from 'svelte/store'import { getLocale, subscribe } from './loclizr/messages'
export const localeStore = readable(getLocale(), (set) => { set(getLocale()) return subscribe(() => set(getLocale()))})The store contract calls a subscriber at once, which loclizr’s subscribe does not, so readable
supplies the first value and set(getLocale()) refreshes it whenever the first subscriber
arrives. Components then read $localeStore.
SvelteKit server rendering is out of scope here; Server rendering has the request-scoped shape. Language switching covers persistence and the per-call override, and Runtime API lists every export.