Skip to content
loclizr

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.

  1. Terminal window
    npm create vite@latest app -- --template vue-ts
    cd app
    npm i
    npm i loclizr
    npx loclizr init

    init writes locales/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}}"
    }
    }
  2. 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"
    }

    typecheck is vue-tsc -b, as in the template’s own build. The root tsconfig.json has "files": [], so npx vue-tsc --noEmit there exits 0 on a broken template.

  3. 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
    }

    subscribe calls its listener with no argument and never on attach, so the ref starts from getLocale(). The listener attaches in onMounted and detaches in onUnmounted. 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. Call useLocale from a component’s setup.

    ./loclizr/messages resolves once a build has run: npm run dev, npm test or npx loclizr build writes it.

  4. 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>
    <button
    v-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 the computed depend on the ref. A message call reads the store, which Vue does not track: computed(() => m.nav_home()) stays Home after 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>

    Cart reads no ref and its prop does not change, so after a switch it still says 3 items in your cart.

  5. 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 App on a switch, so Cart renders German too. A remount drops component state below the key; keep what must survive above it, or use step 4’s computed form.

  6. Terminal window
    npm i -D vitest @vue/test-utils jsdom
    src/locale.test.ts
    // @vitest-environment jsdom
    import { 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.json
    commit locales/loclizr.context.json; `loclizr check` compares it
    4 messages, 2 locales (source en), 0 errors, 0 warnings
    Test 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.