vue-i18n
Vue I18n is the main internationalization plugin for Vue 3. It connects locale messages to components through the useI18n composable, formats plurals, numbers and dates, supports global and component-local message scopes, and can lazy-load translation files. Version 11 includes both the older Vue-style API and the Composition API, but new code should explicitly choose Composition mode because the legacy API is deprecated and scheduled for removal in version 12.
The default choice for a Vue 3 application that needs serious localization, provided you opt into Composition mode and wire production compilation deliberately. Do not start new code on the legacy API, and do not assume development behavior proves dynamically loaded messages will compile in production.
Use it if
- You are building a Vue 3 application and want translations to react automatically when the active locale changes
- You need plural rules, locale fallback, date formatting and number formatting behind one Vue-aware API
- You want component-local messages or i18n blocks in single-file components as well as a shared global catalog
- You need TypeScript to check locale names and translation resource shapes from a master message file
- Your application is still on Vue 2: Vue I18n 8 was the compatible line, and the project says that version has reached end of life
- Your build or CI runtime is below Node 22: the current 11.4.8 package declares Node 22 or newer even though the browser output itself runs client-side
- You need one translation engine shared unchanged across React, Vue and backend services: this package is deliberately coupled to Vue, while i18next or another framework-neutral core fits that architecture better
- You plan to keep the legacy API, $tc or v-t: legacy mode and v-t are deprecated in version 11, $tc was removed in version 11, and the migration guide says legacy mode and v-t will not work in version 12
- Your translations arrive from an API at runtime but you want the smallest runtime-only build: the official bundler plugin requires runtimeOnly: false in that case, otherwise placeholders in uncompiled messages can remain literal in production
Setup reality
Install vue-i18n alongside Vue 3, but check the runtime first: version 11.4.8 declares Node >=22 and vue ^3.0.0 as a peer. Create one i18n instance, pass legacy: false for the Composition API, register it with app.use(i18n), then call useI18n at the top of <script setup>. Omitting legacy: false selects the older mode by default in version 11, which is exactly the API the migration guide marks for removal in version 12. Large catalogs need a loading strategy because the package does not split locale files on its own. Dynamic import, setLocaleMessage and a router guard are the usual combination, and you should also update the document lang attribute. Production bundling adds another moving part: @intlify/unplugin-vue-i18n precompiles files matched by its include pattern and defaults to a runtime-only production build. A bad include glob can make {name} placeholders work in development but stay unexpanded in production. If messages come from an API or database rather than build-time files, set runtimeOnly: false so the message compiler remains. Single-file <i18n> blocks also require that plugin. Date, number and plural behavior depends on the host's Intl implementation, so older or restricted runtimes may need polyfills. For SSR, create an i18n instance per request instead of mutating one process-wide locale, or concurrent users can leak locale state into one another.
Patterns
Create a Vue 3 i18n instanceinstall-composition-mode
// src/i18n.js
import { createI18n } from 'vue-i18n'
export const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {
en: { greeting: 'Hello, {name}!' },
fr: { greeting: 'Bonjour, {name} !' },
},
})
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import { i18n } from './i18n.js'
createApp(App).use(i18n).mount('#app')Set legacy: false explicitly. Version 11 defaults to legacy mode, and that mode is deprecated for removal in version 12.
Translate with named interpolationtranslate-message
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<h1>{{ t('greeting', { name: 'Ada' }) }}</h1>
</template>Call useI18n at the top of <script setup>. Do not pass untrusted HTML through v-html; use component interpolation when markup is required.
Select a plural formpluralize-message
const messages = {
en: { apples: 'no apples | one apple | {count} apples' },
}
const { t } = useI18n()
console.log(t('apples', 0))
console.log(t('apples', 1))
console.log(t('apples', 12))The same t function handles pluralization and supplies count and n implicitly. The old $tc helper is not part of version 11.
Change the active locale reactivelyswitch-locale
<script setup>
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n({ useScope: 'global' })
watch(locale, (value) => {
document.documentElement.lang = value
})
</script>
<template>
<select v-model="locale">
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</template>locale is a ref in Composition mode. Updating the HTML lang attribute is separate and matters to screen readers and search engines.
Define an explicit fallback chainconfigure-fallback
const i18n = createI18n({
legacy: false,
locale: 'de-CH',
fallbackLocale: {
'de-CH': ['fr', 'it'],
default: ['en'],
},
messages,
})Regional locales also fall back implicitly, such as de-CH to de. Add ! to a locale code when that implicit chain is unwanted.
Keep messages local to one componentscope-component-messages
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n({
useScope: 'local',
messages: {
en: { title: 'Billing settings' },
fr: { title: 'Paramètres de facturation' },
},
})
</script>
<template><h2>{{ t('title') }}</h2></template>Local scope isolates keys but inherits the global locale by default. Repeating large catalogs in components can make translation management harder.
Format currency with a named formatformat-currency
const i18n = createI18n({
legacy: false,
locale: 'en-US',
numberFormats: {
'en-US': { money: { style: 'currency', currency: 'USD' } },
'fr-FR': { money: { style: 'currency', currency: 'EUR' } },
},
})
const { n } = useI18n()
console.log(n(1234.5, 'money'))Number formatting delegates to Intl.NumberFormat, so available options and locale data depend on the JavaScript runtime.
Format a date with a named formatformat-date
const i18n = createI18n({
legacy: false,
locale: 'en-US',
datetimeFormats: {
'en-US': { short: { year: 'numeric', month: 'short', day: 'numeric' } },
},
})
const { d } = useI18n()
console.log(d(new Date('2026-08-08T12:00:00Z'), 'short'))The displayed day can change with the user's time zone. Pass an appropriate timeZone option when the product needs a fixed zone.
Put a Vue component inside a sentenceinterpolate-component
<i18n-t keypath="terms.accept" tag="p">
<template #link>
<RouterLink to="/terms">{{ t('terms.link') }}</RouterLink>
</template>
</i18n-t>
// messages.en
{
terms: {
accept: 'I accept the {link}.',
link: 'terms of service',
},
}Use i18n-t instead of putting translated HTML into v-html. Named slots let translators move the component placeholder safely.
Load a locale catalog on demandlazy-load-locale
import { nextTick } from 'vue'
import { i18n } from './i18n.js'
const loaders = {
fr: () => import('./locales/fr.json'),
ja: () => import('./locales/ja.json'),
}
export async function setLocale(locale) {
if (!i18n.global.availableLocales.includes(locale)) {
const mod = await loaders[locale]()
i18n.global.setLocaleMessage(locale, mod.default)
}
i18n.global.locale.value = locale
document.documentElement.lang = locale
await nextTick()
}Use a fixed loader map instead of importing an unchecked user string. With the runtime-only production build, every imported catalog must be precompiled.
Precompile locale files with Viteconfigure-vite-plugin
// vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
export default defineConfig({
plugins: [
vue(),
VueI18nPlugin({
include: fileURLToPath(new URL('./src/locales/**', import.meta.url)),
runtimeOnly: true,
}),
],
})The include pattern must match every build-time catalog. Set runtimeOnly: false if messages arrive as strings from an API at runtime.
Type-check locales from a master catalogtype-message-schema
import { createI18n } from 'vue-i18n'
import enUS from './locales/en-US.json'
import jaJP from './locales/ja-JP.json'
type MessageSchema = typeof enUS
type AppLocale = 'en-US' | 'ja-JP'
export const i18n = createI18n<[MessageSchema], AppLocale>({
legacy: false,
locale: 'en-US',
messages: {
'en-US': enUS,
'ja-JP': jaJP,
},
})Using one locale as the schema catches missing resource shapes during type checking, but translator-edited JSON still needs CI to run TypeScript.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| i18next-vue | npm | You already use i18next on the server or in other frontends and want Vue bindings over the same catalogs |
| fluent-vue | npm | You prefer Mozilla Fluent's translator-oriented message syntax and are comfortable adopting its catalog format |
| typesafe-i18n | npm | Compile-time key safety and generated TypeScript APIs matter more than deep Vue-specific component integration |