Learn-skills.dev web-i18n-vue-i18n

Type-safe i18n for Vue 3 Composition API

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
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"
manifest: data/skills-md/agents-inc/skills/web-i18n-vue-i18n/SKILL.md
source content

vue-i18n Internationalization Patterns

Quick Guide: Use vue-i18n v11+ for type-safe internationalization in Vue 3.

useI18n
composable for translations,
d()
for dates,
n()
for numbers,
i18n-t
component for rich text. Set
legacy: false
for 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:

  1. Composition API first: Use
    useI18n()
    composable with
    legacy: false
    for modern Vue 3 patterns
  2. Single composable call: Destructure all functions (
    t
    ,
    d
    ,
    n
    ,
    locale
    ) from ONE
    useI18n()
    call
  3. Locale reactivity: Locale changes automatically trigger re-renders via Vue's reactivity system
  4. Message format standard: Use pipe-separated plurals and named interpolation for translator-friendly messages
</philosophy>
<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
    locale.value
    from useI18n to read/write
  • Locale changes are reactive -- all components using
    t()
    ,
    d()
    ,
    n()
    update automatically
  • globalInjection
    defaults to
    true
    , injecting
    $t
    ,
    $d
    ,
    $n
    into templates

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
    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-html
    with 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
    dateTimeFormats
    instead of
    datetimeFormats
    -- the config key uses lowercase 't'
  • Using
    $tc()
    -- removed in v11, use
    t()
    with count parameter
  • Using
    v-t
    directive
    -- deprecated in v11, removed in v12, use
    t()
    or
    <i18n-t>

Gotchas & Edge Cases:

  • locale.value
    is a ref -- assign with
    .value
    , not direct assignment
  • @:linked.message
    syntax only works with global scope, not local scope
  • Custom
    pluralRules
    function 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>