Frameworks
Vue
A Vue 3 composable over subscribe and getLocale, and the two ways to make templates follow a switch.
A Vite and Vue 3 app whose templates follow setLocale, with a test that proves it. There is no
loclizr/vue entry: a fourteen-line composable over subscribe and getLocale is the whole
binding.
-
Create the app
Section titled “Create the app”Terminal window npm create vite@latest app -- --template vue-tscd appnpm inpm i loclizrnpx loclizr initinitwriteslocales/en.json, the same catalog as the Quickstart. Add the German one: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": "vue-tsc -b && vite build","preview": "vite preview","pretypecheck": "loclizr build","typecheck": "vue-tsc -b","pretest": "loclizr build","test": "vitest run"}typecheckisvue-tsc -b, as in the template’s ownbuild. The roottsconfig.jsonhas"files": [], sonpx vue-tsc --noEmitthere exits 0 on a broken template. -
Write the composable
Section titled “Write the composable”src/useLocale.ts import { onMounted, onUnmounted, shallowRef, type ShallowRef } from 'vue'import * as m from './loclizr/messages'export function useLocale(): ShallowRef<m.AppLocale> {const locale = shallowRef(m.getLocale())let off = (): void => {}onMounted(() => {off = m.subscribe(() => {locale.value = m.getLocale()})})onUnmounted(() => off())return locale}subscribecalls its listener with no argument and never on attach, so the ref starts fromgetLocale(). The listener attaches inonMountedand detaches inonUnmounted. Neither hook runs in a server render, so a server-rendered component, such as an Astro island, leaves no listener on the process-wide store. CalluseLocalefrom a component’s setup../loclizr/messagesresolves once a build has run:npm run dev,npm testornpx loclizr buildwrites it. -
Read the ref where you call a message
Section titled “Read the ref where you call a message”src/App.vue <script setup lang="ts">import { computed } from 'vue'import Cart from './components/Cart.vue'import * as m from './loclizr/messages'import { useLocale } from './useLocale'const locale = useLocale()const greeting = computed(() => m.cart_greeting({ name: 'Ada' }, { locale: locale.value }))</script><template><nav><buttonv-for="tag in m.locales":key="tag"type="button":aria-pressed="tag === locale"@click="m.setLocale(tag)">{{ tag }}</button></nav><h1>{{ greeting }}</h1><Cart :count="3" /></template>Passing
{ locale: locale.value }is what makes thecomputeddepend on the ref. A message call reads the store, which Vue does not track:computed(() => m.nav_home())staysHomeafter a switch.src/components/Cart.vue <script setup lang="ts">import * as m from '../loclizr/messages'defineProps<{ count: number }>()</script><template><p>{{ m.cart_items({ count }) }}</p></template>Cartreads no ref and its prop does not change, so after a switch it still says3 items in your cart. -
Key the root on the locale
Section titled “Key the root on the locale”src/Root.vue <script setup lang="ts">import App from './App.vue'import { useLocale } from './useLocale'const locale = useLocale()</script><template><App :key="locale" /></template>src/main.ts import { createApp } from 'vue'import './style.css'import Root from './Root.vue'createApp(Root).mount('#app')The key remounts
Appon a switch, soCartrenders German too. A remount drops component state below the key; keep what must survive above it, or use step 4’scomputedform. -
Test it
Section titled “Test it”Terminal window npm i -D vitest @vue/test-utils jsdomsrc/locale.test.ts // @vitest-environment jsdomimport { mount } from '@vue/test-utils'import { computed, defineComponent, h, nextTick, type ShallowRef } from 'vue'import { setLocale } from 'loclizr'import { beforeEach, expect, test } from 'vitest'import App from './App.vue'import Root from './Root.vue'import * as m from './loclizr/messages'import { useLocale } from './useLocale'beforeEach(() => {setLocale('en', { persist: false })})test('a computed that reads the ref updates; a child that does not stays stale', async () => {const wrapper = mount(App)setLocale('de', { persist: false })await nextTick()expect(wrapper.get('h1').text()).toBe('Hallo Ada, dein Warenkorb ist bereit')expect(wrapper.get('p').text()).toBe('3 items in your cart')})test('a key on the root re-renders every message', async () => {const wrapper = mount(Root)setLocale('de', { persist: false })await nextTick()expect(wrapper.get('h1').text()).toBe('Hallo Ada, dein Warenkorb ist bereit')expect(wrapper.get('p').text()).toBe('3 Artikel in deinem Warenkorb')})test('a computed that never reads the ref is cached', async () => {const Home = defineComponent(() => {const label = computed(() => m.nav_home())return () => h('span', label.value)})const wrapper = mount(Home)setLocale('de', { persist: false })await nextTick()expect(wrapper.text()).toBe('Home')})test('the subscription ends on unmount', () => {let locale!: ShallowRef<m.AppLocale>const Probe = defineComponent(() => {locale = useLocale()return () => h('span', locale.value)})mount(Probe).unmount()setLocale('de', { persist: false })expect(locale.value).toBe('en')})npm test wrote 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 warningsTest Files 1 passed (1)Tests 4 passed (4)The first three lines come from
pretest: the build runs before Vitest, so a fresh clone tests without a manual build.
In the browser, setLocale also writes the locale cookie and <html lang>; see
Language switching and the Runtime API.
This page is a client-rendered app; Nuxt is out of scope for v0.1.