web-i18n-vue-i18n · git:20260906.3dc53ce · 2026-09-06 · sha256 3ca0f28acf925dcd
web-i18n-vue-i18n git:20260906.3dc53ceB
Immutable. This exact content is served forever at /api/v1/blob/3ca0f28acf925dcd.
---
name: web-i18n-vue-i18n
description: Type-safe i18n for Vue 3 Composition API — useI18n, pipe-syntax plurals, i18n-t/d/n components, lazy-loaded locales. Load when a project imports vue-i18n.
---
# vue-i18n Internationalization Patterns
> **Quick Guide:** `createI18n({ legacy: false })` selects Composition API mode, and `useI18n()`
> returns `t` for messages, `d` for dates, `n` for numbers and `locale` as a writable ref. Plurals
> are pipe-separated rather than ICU. `<i18n-t>`, `<i18n-d>` and `<i18n-n>` put components and
> styling inside a formatted result. Version boundary: Legacy API mode, the `v-t` directive and the
> Rails `%{var}` format are deprecated in v11 and removed in v12; `$tc()` is already gone.
**Detailed Resources:**
- [examples/core.md](examples/core.md) — setup, useI18n, interpolation, linked messages, plurals, `<i18n-t>`/`<i18n-d>`, types, locale switching
- [examples/formatting.md](examples/formatting.md) — datetime and number format configuration, `<i18n-d>`/`<i18n-n>` scoped slots, dynamic currency
- [examples/lazy-loading.md](examples/lazy-loading.md) — dynamic imports, loading before render, feature splitting, retry and fallback, SSR-safe detection
- [reference.md](reference.md) — decision trees, anti-pattern code, checklists, plural-rule tables, v8→v9 migration and v11/v12 removals
---
## Which path applies
- **Messages shared across the app** — global scope, the default. `useI18n()` reads the instance
created by `createI18n`. Follow [examples/core.md](examples/core.md).
- **Messages belonging to one component** — local scope, `useI18n({ messages: { en: { ... } } })`.
Linked messages (`@:key`) resolve against global messages only, so a locally-scoped message cannot
reference one.
- **Locales loaded on demand rather than bundled** — `setLocaleMessage` after a dynamic import, and
the locale is not switched until the import resolves. Follow
[examples/lazy-loading.md](examples/lazy-loading.md).
---
<critical_requirements>
## Before writing vue-i18n code
**Set `legacy: false` in `createI18n`.** It is what enables `useI18n()`; the default is the Options
API mode that v11 deprecates and v12 removes.
**Take everything from one `useI18n()` call per component.** Destructure `t`, `d`, `n` and `locale`
together — separate calls can resolve to separate composer instances that then disagree about the
current locale.
**Await the message load before assigning `locale.value`.** Switching first renders the raw keys
until the import resolves.
**Set `fallbackLocale`.** Without it a key missing from the active locale renders as the key rather
than as the default locale's text.
</critical_requirements>
---
**Auto-detection:** vue-i18n, useI18n, createI18n, legacy: false, setLocaleMessage, i18n-t, i18n-d,
i18n-n, keypath, pluralRules, datetimeFormats, numberFormats, fallbackLocale, globalInjection
**Applies to:**
- Message rendering with named interpolation, linked messages and pipe-syntax pluralization
- Locale-aware date and number formatting through named format definitions
- Putting components or per-part styling inside a formatted result
- Loading and swapping locale message sets at runtime
- Typing message keys and format names so a wrong one fails at compile time
**Handled elsewhere:**
- Locale-aware routing — this skill settles loading messages before a view renders, not how URLs map
to locales; the hook shape is in [examples/lazy-loading.md](examples/lazy-loading.md)
- Component authoring and reactivity — `locale` is an ordinary ref and needs nothing special
- Where the preferred locale is stored and how it is detected — this skill consumes a locale code
- Bundling and build configuration, beyond the message pre-compilation options in
[reference.md](reference.md)
---
<philosophy>
Messages are plain JSON resolved through a reactive locale ref, so a locale change re-renders every
component that read a message — no subscription, no invalidation step. Two consequences shape the
API. Because the composer holds that ref, one component wants one composer, which is why repeated
`useI18n()` calls are the recurring bug. And because `t()` returns a string rather than a VNode,
anything needing markup inside a translation goes through `<i18n-t>` instead — which is also what
keeps `v-html` out of translated content.
</philosophy>
---
<patterns>
## Core patterns
### Pattern 1: Project setup
One instance, registered once on the app.
```typescript
export const i18n = createI18n({
legacy: false, // enables useI18n()
locale: DEFAULT_LOCALE,
fallbackLocale: DEFAULT_LOCALE,
messages: { en },
});
app.use(i18n);
```
`globalInjection` defaults to `true`, which is what puts `$t`, `$d` and `$n` in templates.
Full code: [examples/core.md](examples/core.md)
### Pattern 2: useI18n
One call, everything destructured from it.
```vue
<script setup lang="ts">
const { t, d, n, locale, availableLocales } = useI18n();
</script>
<template>
<h1>{{ t("dashboard.title") }}</h1>
<p>{{ d(updatedAt, "long") }} · {{ n(total, "currency") }}</p>
</template>
```
Full code: [examples/core.md](examples/core.md)
### Pattern 3: Interpolation and linked messages
Named placeholders, `{'@'}` to escape a literal, and `@:key` to reference another message with an
optional case modifier.
```json
{
"app": { "name": "My App" },
"greeting": "Hello, {name}!",
"welcome": "Welcome to @:app.name!",
"shout": "@.upper:app.name"
}
```
Full code: [examples/core.md](examples/core.md)
### Pattern 4: Pluralization
Pipe-separated forms rather than ICU. `{n}` and `{count}` both resolve to the value passed as the
second argument.
```json
{
"car": "car | cars",
"apple": "no apples | one apple | {count} apples"
}
```
Languages needing more than three forms take a `pluralRules` function per locale, which returns the
index of the form to use.
Full code: [examples/core.md](examples/core.md)
### Pattern 5: Component interpolation
`<i18n-t>` puts components into a message through named slots, keeping the sentence whole.
```vue
<i18n-t keypath="tos" tag="p">
<template #terms>
<a href="/terms">{{ t("termsLink") }}</a>
</template>
</i18n-t>
```
`:plural` selects the form, and `<i18n-d>` / `<i18n-n>` expose each formatted part — `month`, `day`,
`currency`, `integer` — as its own scoped slot.
Full code: [examples/core.md](examples/core.md), scoped-slot styling in
[examples/formatting.md](examples/formatting.md)
### Pattern 6: Named datetime and number formats
Define the formats per locale once and refer to them by name, so a change lands everywhere.
```typescript
createI18n({
legacy: false,
locale: "en-US",
datetimeFormats, // note the lowercase 't'
numberFormats,
});
d(new Date(), "long"); // "Friday, April 19, 2024 at 2:30 PM"
n(10000, "currency"); // "$10,000.00"
```
Full code: [examples/formatting.md](examples/formatting.md)
### Pattern 7: Lazy-loaded locales
Register the messages, then move the locale — never the other way round.
```typescript
export async function setLocale(locale: SupportedLocale): Promise<void> {
const messages = await import(`../locales/${locale}.json`);
i18n.global.setLocaleMessage(locale, messages.default);
i18n.global.locale.value = locale;
document.documentElement.setAttribute("lang", locale);
}
```
Full code: [examples/lazy-loading.md](examples/lazy-loading.md)
### Pattern 8: Type-safe keys and formats
Augment `DefineLocaleMessage` with the shape of a message file, and `DefineDateTimeFormat` /
`DefineNumberFormat` with the format names.
```typescript
declare module "vue-i18n" {
export interface DefineLocaleMessage extends MessageSchema {}
}
```
A wrong key, or a format name that was never defined, then fails at compile time.
Full code: [examples/core.md](examples/core.md)
</patterns>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- `createI18n` without `legacy: false` — `useI18n()` is unavailable, and the Options API mode it
falls back to is removed in v12
- Assigning `locale` directly instead of `locale.value` — it is a ref, so the assignment does nothing
- `$tc()` — removed in v11; `t(key, count)` replaces it
- The `v-t` directive — deprecated in v11 and removed in v12; use `t()` or `<i18n-t>`
- `dateTimeFormats` as the config key — the option is `datetimeFormats`, and the misspelled key is
silently ignored, so every named format resolves to nothing
**Surprising behaviour:**
- Two `useI18n()` calls in one component can yield two composers, and a locale change in one is
invisible to the other
- Setting `locale.value` before the messages load renders the raw keys until the import resolves
- Without `fallbackLocale`, a key missing from the active locale renders as the key itself
- `@:linked.key` resolves against global messages only — it finds nothing from a locally-scoped
`useI18n({ messages })`
- A `pluralRules` function returns the index of the form, not the form
- `t()` returns a string, so markup in a message needs `<i18n-t>`; reaching for `v-html` instead puts
whatever is in the message straight into the DOM
- The document's `lang` attribute does not follow `locale` — update it on every switch; screen
readers choose their pronunciation from it, and crawlers read it as the page's language
- Concatenating translated fragments assumes English word order, which most languages do not share
Anti-patterns with the code that fixes them, and the full v8→v9 and v11/v12 migration tables:
[reference.md](reference.md).
</red_flags>