vue-i18n 配置与实战
High Contrast
Dark Mode
Light Mode
Sepia
Forest
2 min read462 words

vue-i18n 配置与实战

核心问题:Vue 3 + Nuxt 3 项目怎样从零搭建多语言支持?Composition API 下如何优雅地使用翻译?


真实场景

你的 Vue 3 SPA 需要支持中、英双语,后续可能扩展到日语和阿拉伯语(RTL)。从零开始配置 vue-i18n,并接入 Nuxt 3 的 SSR 流程。


vue-i18n 基础配置(Vue 3 SPA)

安装

npm install vue-i18n@9
# vue-i18n v9.x 对应 Vue 3

翻译文件

src/
├── locales/
│   ├── zh-CN.ts     # 使用 TypeScript 文件获得类型推断
│   ├── en-US.ts
│   └── index.ts     # 统一导出
// src/locales/zh-CN.ts
export default {
common: {
loading: '加载中...',
error: '出错了,请重试',
save: '保存',
cancel: '取消',
confirm: '确认',
delete: '删除',
},
nav: {
home: '首页',
products: '商品',
cart: '购物车',
account: '账户',
},
product: {
title: '商品详情',
addToCart: '加入购物车',
outOfStock: '暂时缺货',
stock: '库存:{count} 件 | 库存:{count} 件',  // vue-i18n 复数格式
price: '¥{amount}',
},
errors: {
required: '{field}不能为空',
minLength: '{field}最少需要 {min} 个字符',
email: '请输入有效的邮箱地址',
},
};
// src/locales/en-US.ts
export default {
common: {
loading: 'Loading...',
error: 'Something went wrong, please try again',
save: 'Save',
cancel: 'Cancel',
confirm: 'Confirm',
delete: 'Delete',
},
nav: {
home: 'Home',
products: 'Products',
cart: 'Cart',
account: 'Account',
},
product: {
title: 'Product Details',
addToCart: 'Add to Cart',
outOfStock: 'Out of Stock',
stock: 'No stock | {count} in stock | {count} in stock',
price: '${amount}',
},
errors: {
required: '{field} is required',
minLength: '{field} must be at least {min} characters',
email: 'Please enter a valid email address',
},
};
// src/locales/index.ts
import zhCN from './zh-CN';
import enUS from './en-US';
export const messages = {
'zh-CN': zhCN,
'en-US': enUS,
};
export type MessageSchema = typeof zhCN;
export const availableLocales = ['zh-CN', 'en-US'] as const;
export type AvailableLocale = typeof availableLocales[number];

i18n 插件配置

// src/i18n.ts
import { createI18n } from 'vue-i18n';
import { messages, MessageSchema } from './locales';
// 从浏览器获取首选语言
function getBrowserLocale(): string {
const navigatorLocale = navigator.languages?.[0] ?? navigator.language;
// 匹配支持的 locale
const supportedLocales = ['zh-CN', 'en-US'];
if (navigatorLocale) {
// 尝试精确匹配
if (supportedLocales.includes(navigatorLocale)) return navigatorLocale;
// 尝试语言匹配(zh-HK → zh-CN)
const lang = navigatorLocale.split('-')[0];
const match = supportedLocales.find(l => l.startsWith(lang));
if (match) return match;
}
return 'zh-CN'; // 默认
}
// 从 localStorage 读取用户选择
function getSavedLocale(): string | null {
return localStorage.getItem('preferred-locale');
}
const locale = getSavedLocale() ?? getBrowserLocale();
export const i18n = createI18n<[MessageSchema], 'zh-CN' | 'en-US'>({
legacy: false,        // 使用 Composition API 模式(必须 false)
locale,
fallbackLocale: 'en-US',
messages,
// 生产环境关闭警告(missing key 会 fallback 到 fallbackLocale)
missingWarn: process.env.NODE_ENV !== 'production',
fallbackWarn: process.env.NODE_ENV !== 'production',
});
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { i18n } from './i18n';
createApp(App).use(i18n).mount('#app');

Composition API 使用

基础用法

<!-- src/components/ProductCard.vue -->
<template>
<div class="product-card">
<h2>{{ product.name }}</h2>
<!-- 基础翻译 -->
<p>{{ t('product.title') }}</p>
<!-- 带参数 -->
<p>{{ t('product.price', { amount: product.price }) }}</p>
<!-- 复数 -->
<p>{{ t('product.stock', product.stock, { count: product.stock }) }}</p>
<!-- 条件按钮 -->
<button :disabled="!product.inStock">
{{ product.inStock ? t('product.addToCart') : t('product.outOfStock') }}
</button>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t, locale, n, d } = useI18n();
// t: 翻译函数
// locale: 当前 locale(ref)
// n: 数字格式化
// d: 日期格式化
defineProps<{
product: {
name: string;
price: number;
stock: number;
inStock: boolean;
};
}>();
</script>

数字和日期格式化

// src/i18n.ts 中添加 numberFormats 和 datetimeFormats
export const i18n = createI18n({
// ...
numberFormats: {
'zh-CN': {
currency: { style: 'currency', currency: 'CNY', minimumFractionDigits: 2 },
decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', useGrouping: false },
},
'en-US': {
currency: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', useGrouping: false },
},
},
datetimeFormats: {
'zh-CN': {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
time: { hour: 'numeric', minute: 'numeric', hour12: false },
},
'en-US': {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
time: { hour: 'numeric', minute: 'numeric', hour12: true },
},
},
});
<script setup lang="ts">
const { n, d } = useI18n();
</script>
<template>
<!-- 货币格式化 -->
<p>{{ n(1234.56, 'currency') }}</p>
<!-- zh-CN: ¥1,234.56  |  en-US: $1,234.56 -->
<!-- 日期格式化 -->
<p>{{ d(new Date(), 'short') }}</p>
<!-- zh-CN: 2026年3月22日  |  en-US: Mar 22, 2026 -->
<!-- 时间格式化 -->
<p>{{ d(new Date(), 'time') }}</p>
<!-- zh-CN: 15:30  |  en-US: 3:30 PM -->
</template>

语言切换

<!-- src/components/LocaleSwitcher.vue -->
<template>
<div class="locale-switcher">
<button
v-for="loc in availableLocales"
:key="loc"
:class="{ active: locale === loc }"
@click="switchLocale(loc)"
>
{{ localeNames[loc] }}
</button>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { availableLocales } from '@/locales';
const { locale } = useI18n();
const localeNames: Record<string, string> = {
'zh-CN': '中文',
'en-US': 'English',
};
function switchLocale(newLocale: string) {
locale.value = newLocale;
localStorage.setItem('preferred-locale', newLocale);
// 更新 HTML dir 属性(RTL 支持)
document.documentElement.setAttribute('lang', newLocale);
document.documentElement.setAttribute(
'dir',
newLocale.startsWith('ar') || newLocale.startsWith('he') ? 'rtl' : 'ltr'
);
}
</script>

Nuxt 3 集成(@nuxtjs/i18n)

Nuxt 3 有官方的 i18n 模块,深度集成路由和 SSR。

安装

npm install @nuxtjs/i18n

配置

// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
// 支持的 locale 列表
locales: [
{ code: 'zh-CN', name: '中文', iso: 'zh-CN', file: 'zh-CN.ts' },
{ code: 'en-US', name: 'English', iso: 'en-US', file: 'en-US.ts' },
{ code: 'ja-JP', name: '日本語', iso: 'ja-JP', file: 'ja-JP.ts' },
],
defaultLocale: 'zh-CN',
// 翻译文件目录
langDir: 'locales/',
// URL 策略
strategy: 'prefix_except_default', // 默认 locale 不加前缀,其他加 /en-US/ 前缀
// 懒加载翻译文件
lazy: true,
// 自动检测用户语言
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root',
alwaysRedirect: false,
},
// vue-i18n 配置
vueI18n: './i18n.config.ts',
},
});
// i18n.config.ts(vue-i18n 选项)
export default defineI18nConfig(() => ({
legacy: false,
locale: 'zh-CN',
fallbackLocale: 'en-US',
numberFormats: {
'zh-CN': {
currency: { style: 'currency', currency: 'CNY' },
},
'en-US': {
currency: { style: 'currency', currency: 'USD' },
},
},
}));

在 Nuxt 页面中使用

<!-- pages/products/[id].vue -->
<template>
<div>
<h1>{{ t('product.title') }}</h1>
<!-- useLocalePath 生成本地化链接 -->
<NuxtLink :to="localePath('/products')">
{{ t('nav.products') }}
</NuxtLink>
<!-- 语言切换链接 -->
<NuxtLink
v-for="loc in locales"
:key="loc.code"
:to="switchLocalePath(loc.code)"
>
{{ loc.name }}
</NuxtLink>
</div>
</template>
<script setup lang="ts">
const { t, locale, locales } = useI18n();
const localePath = useLocalePath();          // 本地化路径
const switchLocalePath = useSwitchLocalePath(); // 切换语言路径
// SEO:自动设置 hreflang(@nuxtjs/i18n 会自动处理)
useHead({
htmlAttrs: { lang: locale.value },
});
</script>

SEO 配置(自动 hreflang)

// nuxt.config.ts 中 @nuxtjs/i18n 会自动生成 hreflang 标签
// 还需配置 baseUrl
i18n: {
baseUrl: 'https://www.yoursite.com',
// ...
}
// 会自动在 <head> 生成:
// <link rel="alternate" hreflang="zh-CN" href="https://www.yoursite.com/products" />
// <link rel="alternate" hreflang="en-US" href="https://www.yoursite.com/en-US/products" />
// <link rel="alternate" hreflang="x-default" href="https://www.yoursite.com/products" />

vue-i18n 复数规则

vue-i18n 使用 | 分隔不同数量的翻译形式:

// zh-CN.ts(中文只有 1 种形式)
{
items: '{count} 件商品',
// 使用 | 提供 0/非0 两种形式
cartItems: '购物车是空的 | 购物车有 {count} 件商品',
}
// en-US.ts(英语有 singular/plural)
{
items: '{count} item | {count} items',
cartItems: 'Cart is empty | {count} item in cart | {count} items in cart',
// 3 个选项:=0 | =1 | >=2
}
<template>
<!-- 第二个参数是数量,决定选择哪个复数形式 -->
<p>{{ t('cartItems', cartCount) }}</p>
<!--
count=0: "购物车是空的" / "Cart is empty"
count=1: "购物车有 1 件商品" / "1 item in cart"
count=3: "购物车有 3 件商品" / "3 items in cart"
-->
</template>

⚠️ vue-i18n 的复数规则相对简单(基于 | 分隔),处理复杂语言(阿拉伯语有 6 种复数形式)时需要自定义 pluralRules。ICU 消息格式(react-intl / next-intl 使用)对复数支持更完整,见第 04 章。


TypeScript 类型安全

// 定义翻译消息的类型
import { DefineLocaleMessage } from 'vue-i18n';
import zhCN from './locales/zh-CN';
// 告诉 TypeScript 消息的类型
declare module 'vue-i18n' {
export interface DefineLocaleMessage {
common: typeof zhCN.common;
nav: typeof zhCN.nav;
product: typeof zhCN.product;
}
}
// 使用时有自动补全和类型检查
const { t } = useI18n();
t('common.loading');      // ✅
t('common.nonExistent'); // ❌ TypeScript 报错

常见问题

Q:切换语言后页面需要刷新吗?

A:vue-i18n v9(Composition API 模式)切换 locale.value 是响应式的,所有使用 t() 的组件会自动重新渲染,无需刷新页面。

Q:如何处理翻译文件过大的问题?

A:使用懒加载,按路由拆分翻译文件。详见下一节《动态 namespace 加载与代码分割》。

Q:v-html 中的内容能翻译吗?

<!-- 使用 v-html 时要注意 XSS 风险 -->
<div v-html="t('rich.content')"></div>
<!-- 安全的做法:使用 i18n-t 组件处理富文本 -->
<i18n-t keypath="terms" tag="p">
<template #link>
<a href="/terms">{{ t('common.terms') }}</a>
</template>
</i18n-t>

下一节:两大框架配置完成,但随着翻译文件增多,首屏会加载所有语言文件。学习如何按需加载翻译,降低 bundle 体积。

动态 namespace 加载与代码分割