← 返回 Skill 列表
extension
分类: 开发与工程API Key 暂未确认

web-i18n-vue-i18n

Vue 3 Composition API 的类型安全国际化

person作者: jakexiaohubgithub

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 — setup, useI18n, interpolation, linked messages, plurals, <i18n-t>/<i18n-d>, types, locale switching
  • examples/formatting.md — datetime and number format configuration, <i18n-d>/<i18n-n> scoped slots, dynamic currency
  • examples/lazy-loading.md — dynamic imports, loading before render, feature splitting, retry and fallback, SSR-safe detection
  • 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.
  • 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.

<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
  • 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

<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.

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

Pattern 2: useI18n

One call, everything destructured from it.

<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

Pattern 3: Interpolation and linked messages

Named placeholders, {'@'} to escape a literal, and @:key to reference another message with an optional case modifier.

{
  "app": { "name": "My App" },
  "greeting": "Hello, {name}!",
  "welcome": "Welcome to @:app.name!",
  "shout": "@.upper:app.name"
}

Full code: 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.

{
  "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

Pattern 5: Component interpolation

<i18n-t> puts components into a message through named slots, keeping the sentence whole.

<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, scoped-slot styling in 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.

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

Pattern 7: Lazy-loaded locales

Register the messages, then move the locale — never the other way round.

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

Pattern 8: Type-safe keys and formats

Augment DefineLocaleMessage with the shape of a message file, and DefineDateTimeFormat / DefineNumberFormat with the format names.

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

</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.

</red_flags>