web-i18n-vue-i18n
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
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
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 languages with complex rules (Russian, Arabic, Polish)
const i18n = createI18n({
legacy: false,
locale: "ru",
pluralRules: {
ru: (choice: number, choicesLength: number) => {
if (choice === 0) return 0;
const teen = choice > 10 && choice < 20;
const endsWithOne = choice % 10 === 1;
if (!teen && endsWithOne) return 1;
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) return 2;
return choicesLength < 4 ? 2 : 3;
},
},
messages: {
ru: {
apple: "нет яблок | {n} яблоко | {n} яблока | {n} яблок",
},
},
});
Why good: custom pluralRules handle languages with more than two forms, function receives choice count and returns index into plural array
Pattern 5: Component Interpolation
Use i18n-t, i18n-d, and i18n-n components for rich text with Vue components inside translations.
i18n-t for Rich Text
{
"tos": "I agree to the {terms}.",
"termsLink": "Terms of Service"
}
<template>
<i18n-t keypath="tos" tag="p">
<template #terms>
<a href="/terms">{{ t("termsLink") }}</a>
</template>
</i18n-t>
</template>
Why good: translation string stays translatable, Vue components can be inserted via named slots, tag prop controls wrapper element
i18n-t with Pluralization
{
"items": "no items | {n} item | {n} items"
}
<template>
<i18n-t keypath="items" :plural="count" tag="p">
<template #n>
<strong>{{ count }}</strong>
</template>
</i18n-t>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(5);
</script>
Why good: plural value passed via :plural prop, #n slot allows styling the number, result: "5 items"
i18n-d and i18n-n for Styled Parts
<template>
<!-- DateTime with styled parts -->
<i18n-d :value="date" format="long" tag="time">
<template #month="{ month }">
<span class="month">{{ month }}</span>
</template>
<template #day="{ day }">
<span class="day">{{ day }}</span>
</template>
</i18n-d>
<!-- Number with styled parts -->
<i18n-n :value="price" format="currency" tag="span">
<template #currency="{ currency }">
<span class="currency-symbol">{{ currency }}</span>
</template>
<template #integer="{ integer }">
<span class="integer">{{ integer }}</span>
</template>
</i18n-n>
</template>
Why good: scoped slots expose formatted parts (month, day, currency, integer), enables fine-grained styling of formatted values
Pattern 6: DateTime and Number Formatting
Configure and use locale-aware formatting for dates, times, and numbers.
DateTime Format Configuration
const datetimeFormats = {
"en-US": {
short: {
year: "numeric",
month: "short",
day: "numeric",
},
long: {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
hour: "numeric",
minute: "numeric",
},
},
"ja-JP": {
short: {
year: "numeric",
month: "short",
day: "numeric",
},
long: {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
hour: "numeric",
minute: "numeric",
hour12: false,
},
},
};
const i18n = createI18n({
legacy: false,
locale: "en-US",
datetimeFormats, // Note: camelCase, not dateTimeFormats
});
const { d } = useI18n();
d(new Date(), "short"); // "Apr 19, 2024"
d(new Date(), "long"); // "Friday, April 19, 2024 at 2:30 PM"
Why good: named formats ensure consistency across app, locale-specific formats handle cultural differences (12h vs 24h time)
Number Format Configuration
const numberFormats = {
"en-US": {
currency: {
style: "currency",
currency: "USD",
notation: "standard",
},
decimal: {
style: "decimal",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
percent: {
style: "percent",
useGrouping: false,
},
},
"ja-JP": {
currency: {
style: "currency",
currency: "JPY",
useGrouping: true,
currencyDisplay: "symbol",
},
},
};
const i18n = createI18n({
legacy: false,
locale: "en-US",
numberFormats,
});
const { n } = useI18n();
n(10000, "currency"); // "$10,000.00"
n(0.15, "percent"); // "15%"
Why good: Intl.NumberFormat under the hood, handles locale-specific separators and symbols automatically
Integration Guide
vue-i18n integrates with Vue's reactivity system for automatic re-renders on locale change.
Locale state guidance:
- Locale state is managed by vue-i18n -- use
locale.valuefrom useI18n to read/write - Locale changes are reactive -- all components using
t(),d(),n()update automatically globalInjectiondefaults totrue, injecting$t,$d,$ninto templates
Locale-based routing: vue-i18n works with navigation guards to load translations before route renders. See examples/lazy-loading.md for patterns.
<red_flags>
RED FLAGS
- Missing
legacy: false-- defaults to deprecated Options API mode (removed in v12) - Multiple
useI18n()calls in same component -- creates separate instances that desync - Hardcoded locale strings -- use named constants for type safety
- Missing
fallbackLocale-- missing translations cause visible errors instead of graceful fallback - Using
v-htmlwith translations -- XSS vulnerability, use<i18n-t>instead - String concatenation -- word order varies by language, use complete messages with interpolation
- Setting locale before messages load -- shows raw keys, always await loading first
- Using
dateTimeFormatsinstead ofdatetimeFormats-- the config key uses lowercase 't' - Using
$tc()-- removed in v11, uset()with count parameter - Using
v-tdirective -- deprecated in v11, removed in v12, uset()or<i18n-t>
Gotchas & Edge Cases:
locale.valueis a ref -- assign with.value, not direct assignment@:linked.messagesyntax only works with global scope, not local scope- Custom
pluralRulesfunction returns an index into the array, not the form itself
See reference.md for full anti-pattern code examples and decision frameworks.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(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)
Failure to follow these rules will break i18n reactivity and cause translation inconsistencies.
</critical_reminders>
More from agents-inc/skills
web-animation-css-animations
CSS Animation patterns - transitions, keyframes, scroll-driven animations, @property, GPU-accelerated properties, accessibility with prefers-reduced-motion
20web-testing-playwright-e2e
Playwright E2E testing patterns - test structure, Page Object Model, locator strategies, assertions, network mocking, visual regression, parallel execution, fixtures, and configuration
18web-animation-framer-motion
Motion (formerly Framer Motion) animation patterns - motion components, variants, gestures, layout animations, scroll-linked animations, accessibility
17web-animation-view-transitions
View Transitions API patterns - same-document transitions, cross-document MPA transitions, shared element animations, pseudo-element styling, accessibility
16web-styling-cva
Class Variance Authority - type-safe component variant styling with cva(), compound variants, and VariantProps
16web-performance-web-performance
Bundle optimization, render performance, Core Web Vitals
16