vue-i18n Internationalization Patterns
Quick Guide: Use vue-i18n v11+ for type-safe internationalization in Vue 3.
useI18ncomposable for translations,d()for dates,n()for numbers,i18n-tcomponent for rich text. Setlegacy: falsefor Composition API mode (Legacy API is deprecated in v11, removed in v12).
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST set legacy: false in createI18n for Composition API mode)
(You MUST use a SINGLE useI18n() call per component - destructure all needed functions from one call)
(You MUST await locale message loading before setting locale.value - setting locale before messages are loaded shows raw keys)
(You MUST use named constants for locale codes - NO inline locale strings)
</critical_requirements>
Auto-detection: vue-i18n, useI18n, createI18n, i18n-t, i18n-d, i18n-n, locale detection, pluralization, Vue 3 i18n, Composition API i18n
When to use:
- Implementing internationalization in Vue 3 applications
- Rendering localized messages with interpolation and pluralization
- Formatting dates, numbers, and currency per locale
- Setting up locale-based routing and lazy loading
- Building type-safe translation systems with TypeScript
Key patterns covered:
- Project setup with createI18n and Composition API
- useI18n composable for messages, dates, numbers
- Pluralization with pipe syntax and custom rules
- Component interpolation with i18n-t, i18n-d, i18n-n
- Lazy loading translations for performance
- TypeScript integration for type-safe keys
When NOT to use:
- Simple single-locale applications (skip i18n complexity)
- Legacy Vue 2 applications (use vue-i18n v8)
- Non-Vue applications (use framework-specific i18n solution)
Detailed Resources:
- examples/core.md -- Setup, useI18n, interpolation, pluralization, component interpolation, TypeScript, locale switching
- examples/formatting.md -- DateTime formats, number formats, i18n-d/i18n-n components with scoped slots
- examples/lazy-loading.md -- Dynamic imports, route-based loading, feature splitting, error handling, SSR
- reference.md -- Decision frameworks, anti-patterns, checklists, pluralization rules, migration notes
<philosophy>
Philosophy
vue-i18n follows the principle of locale-aware, reactive rendering with support for complex message formatting. Translations are organized as JSON objects, loaded globally or per-component. The Composition API mode (legacy: false) provides a modern, type-safe approach using the useI18n composable.
Core principles:
- Composition API first: Use
useI18n()composable withlegacy: falsefor modern Vue 3 patterns - Single composable call: Destructure all functions (
t,d,n,locale) from ONEuseI18n()call - Locale reactivity: Locale changes automatically trigger re-renders via Vue's reactivity system
- Message format standard: Use pipe-separated plurals and named interpolation for translator-friendly messages
<patterns>
Core Patterns
Pattern 1: Project Setup
Set up vue-i18n with Composition API mode using the standard file structure.
File Structure
src/
i18n/
index.ts # Main i18n configuration
types.ts # TypeScript type declarations
locales/
en.json # English translations
ja.json # Japanese translations
fr.json # French translations
main.ts # App entry with i18n plugin
Configuration
// src/i18n/index.ts
import { createI18n } from "vue-i18n";
import en from "../locales/en.json";
export const SUPPORTED_LOCALES = ["en", "ja", "fr"] as const;
export const DEFAULT_LOCALE = "en";
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
export const i18n = createI18n({
legacy: false, // REQUIRED for Composition API
locale: DEFAULT_LOCALE,
fallbackLocale: DEFAULT_LOCALE,
// globalInjection defaults to true - injects $t, $d, $n into templates
messages: {
en,
},
});
Why good: legacy: false enables Composition API mode, named constants for locales enable type-safe usage, fallbackLocale prevents missing translation errors, globalInjection enables template shorthand (default true since v9.2)
// main.ts
import { createApp } from "vue";
import { i18n } from "./i18n";
import App from "./App.vue";
const app = createApp(App);
app.use(i18n);
app.mount("#app");
Why good: i18n plugin registered once at app root, all components inherit translation capability
Pattern 2: useI18n Composable
Use the useI18n composable in components for translations, formatting, and locale management.
Basic Usage
<script setup lang="ts">
import { useI18n } from "vue-i18n";
// CRITICAL: Single call, destructure all needed functions
const { t, d, n, locale, availableLocales } = useI18n();
const switchLocale = (newLocale: string) => {
locale.value = newLocale;
};
</script>
<template>
<h1>{{ t("greeting") }}</h1>
<p>{{ t("messages.welcome", { name: "Vue" }) }}</p>
<p>{{ d(new Date(), "long") }}</p>
<p>{{ n(1000, "currency") }}</p>
<select :value="locale" @change="switchLocale($event.target.value)">
<option v-for="loc in availableLocales" :key="loc" :value="loc">
{{ loc }}
</option>
</select>
</template>
Why good: single useI18n call prevents sync issues, locale.value is reactive and triggers re-renders, destructuring provides all needed functions
<!-- BAD - Multiple useI18n calls cause sync issues -->
<script setup lang="ts">
const { t } = useI18n();
const { locale } = useI18n(); // WRONG: Second call!
const { d } = useI18n(); // WRONG: Third call!
</script>
Why bad: multiple useI18n calls create separate instances that may not stay synchronized, leads to subtle bugs
Pattern 3: Message Interpolation
Use named placeholders and linked messages for flexible translations.
Named Interpolation
// locales/en.json
{
"greeting": "Hello, {name}!",
"items": "You have {count} items in your cart.",
"email": "{account}{'@'}{domain}"
}
const { t } = useI18n();
t("greeting", { name: "John" }); // "Hello, John!"
t("items", { count: 5 }); // "You have 5 items in your cart."
t("email", { account: "user", domain: "example.com" }); // "user@example.com"
Why good: named placeholders are explicit and refactorable, literal interpolation ({'@'}) escapes special characters
Linked Messages
{
"app": {
"name": "My App"
},
"welcome": "Welcome to @:app.name!",
"brand": "vue i18n",
"message": {
"upper": "@.upper:brand",
"lower": "@.lower:brand",
"capitalize": "@.capitalize:brand"
}
}
t("welcome"); // "Welcome to My App!"
t("message.upper"); // "VUE I18N"
t("message.capitalize"); // "Vue i18n"
Why good: linked messages (@:key) avoid duplication, built-in modifiers (upper, lower, capitalize) transform referenced values
Pattern 4: Pluralization
Use pipe-separated syntax for plural forms with automatic {n} and {count} injection.
Basic Plural Syntax
{
"car": "car | cars",
"apple": "no apples | one apple | {count} apples",
"items": "no items | {n} item | {n} items"
}
const { t } = useI18n();
t("car", 1); // "car"
t("car", 2); // "cars"
t("apple", 0); // "no apples"
t("apple", 1); // "one apple"
t("apple", 10); // "10 apples"
t("items", 5); // "5 items"
Why good: pipe syntax is translator-friendly, {n} and {count} are auto-injected with the plural value, three forms handle zero/one/many
Custom Plural Rules
// For lang