Vue
Vue 3 on the Vite plugin with vue:true, and Nuxt 4 through the best-i18n/nuxt module
Vue runs on best-i18n/vite with one extra option,
vue: true, which lets the plugin compile macros inside .vue components
and makes every compiled locale read go through best-i18n/vue, so a
composable's computed tracks it too. Two small entry points cover what Vue
itself needs: best-i18n/vue for the reactive locale, and
best-i18n/vue/macro for <Trans>. Nuxt gets a module, best-i18n/nuxt,
that wires all of it. A full Nuxt app is in
playground/nuxt.
Setup with Vite
Install vue@^3.3 — an optional peer dependency — then put i18n(...)
before vue() in the plugins array. The macros have to be compiled away
before Vue compiles the component.
// vite.config.ts
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { i18n } from 'best-i18n/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
i18n({
messagesDir: fileURLToPath(new URL('./messages', import.meta.url)),
locales: ['en', 'zh'],
baseLocale: 'en',
staticLocale: process.env.I18N_STATIC_LOCALE,
vue: true,
}),
vue(),
],
})vue: true does two things: it widens the default file filter to .vue,
and it selects best-i18n/vue as the runtime for every file the plugin
compiles, .ts composables included. If you pass your own include, that
pattern is the whole filter.
Writing messages in a component
t, plural and aliased imports come from best-i18n/macro, exactly as in
JS/TS. They work in <script setup>, in a plain <script>, in {{ }}
interpolations, in :prop bindings and in @event handlers:
<script setup lang="ts">
import { computed } from 'vue'
import { plural, t } from 'best-i18n/macro'
const props = defineProps<{ name: string; count: number }>()
// Script text that should follow the locale lives in a computed; a plain
// `const` is evaluated once.
const title = computed(() => t`Welcome`)
</script>
<template>
<h1 :title="t`Welcome`">{{ title }}</h1>
<p>{{ t`Hello ${props.name}` }}</p>
<p>{{ plural(props.count, `One item`, `${props.count} items`) }}</p>
</template>A sentence with markup in it uses <Trans> from best-i18n/vue/macro. The
catalog stores the elements as named placeholders — Read the <a>docs</a> —
so a translation can move the link without ever seeing an attribute:
<script setup lang="ts">
import { Trans } from 'best-i18n/vue/macro'
defineProps<{ docsUrl: string }>()
</script>
<template>
<p>
<Trans>Read the <a :href="docsUrl">documentation</a> to learn more.</Trans>
</p>
<p><Trans ctx="verb">Open</Trans></p>
</template>The compiler rebuilds ordinary template markup per locale: <template v-if>
blocks over the locale branches, or just the one locale's markup under
staticLocale. Text becomes {{ }} interpolations, so what Vue renders is
exactly what the translator saw. <Trans> takes no props other than ctx
(a string literal) and locale (locale="zh" or :locale="lang") — wrap it in
an element if you need a class or an event — and its children must be text, {{ }} interpolations, elements and
components. <template>, <slot> and <component> have no place in a
sentence and are rejected at build time.
Only <script setup> imports are visible to the template, which is where
the macros have to be imported for template use. A component with only a
plain <script> can still use t inside that script.
Reactivity
Every compiled message reads the locale through best-i18n/vue, where it is
a shallowRef the render tracks. That means:
- Template expressions —
{{ t...}},:title="t...", a compiled<Trans>— update when the locale changes, with no extra code. - In a script, an ordinary initializer runs once. Text that should follow the
locale goes in
computed(() => t...). - A
.tscomposable compiled withvue: trueuses the same reactive read, so itscomputedvalues follow the locale too.
Your own code reads the locale through the same entry point. useLocale()
returns a computed ref, the shape Vue composables have; getLocale() is the
same read as a function, tracked in exactly the same places:
<script setup lang="ts">
import { switchLocale } from 'best-i18n/client'
import { useLocale } from 'best-i18n/vue'
import { i18n } from '@/i18n'
const locale = useLocale()
</script>
<template>
<button
v-for="item in i18n.locales"
:key="item"
:disabled="item === locale"
@click="switchLocale(item, i18n)"
>
{{ item }}
</button>
</template>best-i18n/vue also re-exports configure, getLocales and setLocale.
React's useI18n macro is not supported in Vue files and is rejected at
build time; computed is the Vue answer to the same problem.
Nuxt
Nuxt adds server rendering and file routing. The module does the wiring:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['best-i18n/nuxt'],
bestI18n: {
locales: ['en', 'zh'],
baseLocale: 'en',
staticLocale: process.env.I18N_STATIC_LOCALE || undefined,
},
})bestI18n takes the same shape as the URL config elsewhere — locales,
baseLocale, cookieName — plus messagesDir (default messages, relative
to the project) and staticLocale. exclude is accepted too, but Nuxt
rarely needs it: nothing is rewritten here, so its only effect is to pin the
paths it matches to the base locale — set it only if some server routes must
never be localized. With that, the module:
- adds the Vite plugin with
vue: true; - adds a copy of every page under each non-base locale prefix, so
/zh/aboutis a real route and nothing is rewritten at request time; - turns on
experimental.asyncContextand installs a Nitro plugin that resolves the locale per request — URL prefix, cookie,Accept-Language— and hands it to everygetLocale()in that request, in components, composables and server routes alike, through Nitro's async context; - installs a Nuxt plugin that stamps
<html lang>on the server and reads it back on the client before hydration, so the first client render agrees with the HTML.
Links go through localizePathname so /about becomes /zh/about while
Chinese is active; switchLocale from best-i18n/client remembers the
choice in a cookie and navigates. Nothing in the pages is Nuxt-specific.
A per-locale build — I18N_STATIC_LOCALE=zh nuxt build — collapses every
message to its Chinese literal and makes the server answer every request in
Chinese, whatever the URL or headers say. Deploy one such build per locale
behind locale-aware routing.
Extraction
i18n-extract scans .vue files alongside .ts; in a Nuxt project point it
at app:
i18n-extract --locales en,zh --src app --messages messages