git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/agents-inc/skills/web-i18n-vue-i18n" ~/.claude/skills/neversight-learn-skills-dev-web-i18n-vue-i18n && rm -rf "$T"
data/skills-md/agents-inc/skills/web-i18n-vue-i18n/SKILL.mdvue-i18n Internationalization Patterns
Quick Guide: Use vue-i18n v11+ for type-safe internationalization in Vue 3.
composable for translations,useI18nfor dates,d()for numbers,n()component for rich text. Seti18n-tfor Composition API mode (Legacy API is deprecated in v11, removed in v12).legacy: false
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST set
in createI18n for Composition API mode)legacy: false
(You MUST use a SINGLE
call per component - destructure all needed functions from one call)useI18n()
(You MUST await locale message loading before setting
- setting locale before messages are loaded shows raw keys)locale.value
(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
composable withuseI18n()
for modern Vue 3 patternslegacy: false - Single composable call: Destructure all functions (
,t
,d
,n
) from ONElocale
calluseI18n() - 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 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
</patterns><integration>
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
from useI18n to read/writelocale.value - Locale changes are reactive -- all components using
,t()
,d()
update automaticallyn()
defaults toglobalInjection
, injectingtrue
,$t
,$d
into templates$n
Locale-based routing: vue-i18n works with navigation guards to load translations before route renders. See examples/lazy-loading.md for patterns.
</integration><red_flags>
RED FLAGS
- Missing
-- defaults to deprecated Options API mode (removed in v12)legacy: false - Multiple
calls in same component -- creates separate instances that desyncuseI18n() - Hardcoded locale strings -- use named constants for type safety
- Missing
-- missing translations cause visible errors instead of graceful fallbackfallbackLocale - Using
with translations -- XSS vulnerability, usev-html
instead<i18n-t> - 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
instead ofdateTimeFormats
-- the config key uses lowercase 't'datetimeFormats - Using
-- removed in v11, use$tc()
with count parametert() - Using
directive -- deprecated in v11, removed in v12, usev-t
ort()<i18n-t>
Gotchas & Edge Cases:
is a ref -- assign withlocale.value
, not direct assignment.value
syntax only works with global scope, not local scope@:linked.message- Custom
function returns an index into the array, not the form itselfpluralRules
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
in createI18n for Composition API mode)legacy: false
(You MUST use a SINGLE
call per component - destructure all needed functions from one call)useI18n()
(You MUST await locale message loading before setting
- setting locale before messages are loaded shows raw keys)locale.value
(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>