第 139 章:国际化 i18n
学习目标
- 掌握 Vue I18n 在 Vue 3 中的集成
- 学会多语言切换、消息格式化、复数处理
- 理解命名空间、懒加载、复数形式
- 在项目中实现完整的国际化方案
一、为什么需要国际化
二、Vue I18n
2.1 安装
bash
pnpm add vue-i18n@92.2 基础配置
typescript
// i18n/index.ts
import { createI18n } from 'vue-i18n';
import zhCN from './locales/zh-CN';
import enUS from './locales/en-US';
const i18n = createI18n({
legacy: false, // 使用 Composition API
locale: 'zh-CN',
fallbackLocale: 'en-US',
messages: {
'zh-CN': zhCN,
'en-US': enUS
}
});
export default i18n;2.3 注入
typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import i18n from './i18n';
const app = createApp(App);
app.use(i18n);
app.mount('#app');三、消息文件
3.1 简单结构
typescript
// locales/zh-CN.ts
export default {
hello: '你好',
welcome: '欢迎,{name}!',
items: '项 | 项 | 项'
};typescript
// locales/en-US.ts
export default {
hello: 'Hello',
welcome: 'Welcome, {name}!',
items: 'no items | one item | {count} items'
};3.2 嵌套结构
typescript
export default {
common: {
save: '保存',
cancel: '取消',
delete: '删除',
confirm: '确认'
},
user: {
title: '用户管理',
list: '用户列表',
create: '新建用户',
edit: '编辑用户',
delete: '删除用户'
},
errors: {
network: '网络错误',
unauthorized: '未授权',
serverError: '服务器错误'
}
};四、基本用法
4.1 Composition API
vue
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
function switchLang() {
locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN';
}
</script>
<template>
<p>{{ t('hello') }}</p>
<p>{{ t('welcome', { name: 'Tom' }) }}</p>
<button @click="switchLang">切换语言</button>
</template>4.2 Options API
vue
<script>
export default {
methods: {
greet() {
return this.$t('hello');
}
}
};
</script>
<template>
<p>{{ $t('hello') }}</p>
</template>4.3 嵌套 key
vue
<template>
<button>{{ t('common.save') }}</button>
<h1>{{ t('user.title') }}</h1>
</template>五、参数插值
5.1 命名参数
typescript
// messages
{ greet: '你好,{name},你今年 {age} 岁' }vue
<template>
<p>{{ t('greet', { name: 'Tom', age: 18 }) }}</p>
</template>5.2 列表参数
typescript
{ fruits: '我喜欢 {0} 和 {1}' }vue
<p>{{ t('fruits', ['苹果', '香蕉']) }}</p>5.3 链接
typescript
{
policy: '请阅读我们的 <a href="/policy">隐私政策</a>'
}vue
<p v-html="t('policy')"></p>六、复数处理
6.1 简单复数
typescript
// zh-CN
{ items: '没有项 | {count} 项' }
// en-US
{ items: 'no items | one item | {count} items' }vue
<p>{{ t('items', 0) }}</p>
<p>{{ t('items', 1) }}</p>
<p>{{ t('items', 5, { count: 5 }) }}</p>6.2 复杂复数
typescript
// en-US
{ items: 'no items | one item | {count} items' }vue
<p>{{ t('items', count, { count }) }}</p>6.3 named 复数
typescript
{
car: '{count} {count} car | {count} {count} cars'
// 实际不这样写,Vue I18n 用 n() 函数
}typescript
import { useI18n } from 'vue-i18n';
const { n } = useI18n();
n(1, 'car'); // 1 car
n(5, 'car'); // 5 cars七、数字与日期
7.1 数字格式化
typescript
import { useI18n } from 'vue-i18n';
const { n } = useI18n();
n(1234.56); // 1,234.56 (en) / 1,234.56 (zh)
n(1234.56, 'currency'); // $1,234.56
n(0.25, 'percent'); // 25%7.2 日期格式化
typescript
const { d } = useI18n();
d(new Date()); // 2026/8/13
d(new Date(), 'short'); // 2026/8/13
d(new Date(), 'long'); // 2026年8月13日
d(new Date(), {
year: 'numeric',
month: 'long',
day: 'numeric'
});7.3 自定义格式
typescript
const i18n = createI18n({
// ...
numberFormats: {
'zh-CN': {
currency: { style: 'currency', currency: 'CNY' }
},
'en-US': {
currency: { style: 'currency', currency: 'USD' }
}
},
datetimeFormats: {
'zh-CN': {
short: { year: 'numeric', month: '2-digit', day: '2-digit' }
}
}
});vue
<p>{{ n(price, 'currency') }}</p>
<p>{{ d(date, 'short') }}</p>八、命名空间
8.1 创建
typescript
// composables/useI18n.ts
import { useI18n } from 'vue-i18n';
export function useModuleI18n() {
return useI18n({
messages: {
'zh-CN': {
local: {
hello: '模块内你好'
}
},
'en-US': {
local: {
hello: 'Module Hello'
}
}
}
});
}8.2 使用
vue
<script setup lang="ts">
const { t } = useModuleI18n();
</script>
<template>
<p>{{ t('local.hello') }}</p>
</template>九、动态切换语言
9.1 切换函数
typescript
// composables/useLocale.ts
import { useI18n } from 'vue-i18n';
export function useLocale() {
const { locale } = useI18n();
function setLocale(lang: string) {
locale.value = lang;
localStorage.setItem('locale', lang);
document.documentElement.lang = lang;
}
function initLocale() {
const saved = localStorage.getItem('locale');
if (saved) {
locale.value = saved;
}
}
return { locale, setLocale, initLocale };
}9.2 持久化
typescript
// main.ts
const savedLocale = localStorage.getItem('locale') || 'zh-CN';
const i18n = createI18n({
locale: savedLocale,
// ...
});9.3 切换组件
vue
<template>
<select :value="locale" @change="handleChange">
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
</select>
</template>
<script setup lang="ts">
import { useLocale } from '@/composables/useLocale';
const { locale, setLocale } = useLocale();
function handleChange(e: Event) {
setLocale((e.target as HTMLSelectElement).value);
}
</script>十、懒加载语言包
10.1 动态导入
typescript
// i18n/index.ts
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
locale: 'zh-CN',
messages: {} // 初始为空
});
// 懒加载语言包
export async function loadLocale(lang: string) {
const messages = await import(`./locales/${lang}.ts`);
i18n.global.setLocaleMessage(lang, messages.default);
i18n.global.locale.value = lang;
}
export default i18n;10.2 使用
typescript
async function switchLang(lang: string) {
await loadLocale(lang);
}10.3 预加载
typescript
// 入口预加载默认语言
await loadLocale('zh-CN');十一、组件库集成
11.1 Element Plus
typescript
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
import { useLocale } from 'element-plus';
const i18n = createI18n({
// ...
legacy: false
});
// 切换
const { locale: elLocale } = useLocale();
watch(locale, (val) => {
elLocale.value = val === 'zh-CN' ? zhCn : en;
});11.2 Naive UI
typescript
import { zhCN, enUS, dateZhCN, dateEnUS } from 'naive-ui';
// 在 n-config-provider 中
<n-config-provider :locale="naiveLocale" :date-locale="naiveDateLocale">
<app />
</n-config-provider>十二、复数规则
typescript
// i18n/index.ts
const i18n = createI18n({
// ...
pluralRules: {
'ru': (choice, choicesLength) => {
// 俄语复杂复数
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;
}
}
});十三、TypeScript 增强
13.1 模式定义
typescript
// types/i18n.d.ts
import 'vue-i18n';
declare module 'vue-i18n' {
interface DefineLocaleMessage {
hello: string;
welcome: string;
common: {
save: string;
cancel: string;
};
}
}13.2 类型化 t
typescript
const { t } = useI18n();
t('hello'); // ✅ 强类型
t('unknown'); // ❌ 报错十四、消息同步
14.1 服务端同步
typescript
async function loadMessagesFromServer(lang: string) {
const res = await http.get(`/api/i18n/${lang}`);
return res.data;
}
export async function loadLocale(lang: string) {
const messages = await loadMessagesFromServer(lang);
i18n.global.setLocaleMessage(lang, messages);
i18n.global.locale.value = lang;
}14.2 缓存
typescript
const messageCache = new Map<string, any>();
export async function loadLocale(lang: string) {
if (messageCache.has(lang)) {
i18n.global.setLocaleMessage(lang, messageCache.get(lang));
return;
}
const messages = await loadMessagesFromServer(lang);
messageCache.set(lang, messages);
i18n.global.setLocaleMessage(lang, messages);
}十五、SEO 适配
15.1 html lang
typescript
function setHtmlLang(lang: string) {
document.documentElement.lang = lang;
}15.2 标题
typescript
const route = useRoute();
const { t, locale } = useI18n();
watchEffect(() => {
document.title = t(route.meta.title as string);
});十六、翻译管理工具
| 工具 | 特点 |
|---|---|
| i18next | 业界标准 |
| i18n-ally | VSCode 插件 |
| Crowdin | 在线协作 |
| Lokalise | 翻译管理 |
| locize | 实时翻译 |
十七、测试
typescript
import { mount } from '@vue/test-utils';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
locale: 'zh-CN',
messages: {
'zh-CN': { hello: '你好' },
'en-US': { hello: 'Hello' }
}
});
it('renders', () => {
const wrapper = mount(MyComponent, {
global: { plugins: [i18n] }
});
expect(wrapper.text()).toContain('你好');
});十八、本章小结
| 概念 | 关键 |
|---|---|
| 基础 | createI18n + useI18n |
| 消息 | key + 嵌套结构 |
| 复数 | n() + 语法 |
| 格式化 | n() / d() |
| 切换 | setLocaleMessage |
| 懒加载 | 动态 import |
| 命名空间 | composable 包裹 |
| TypeScript | DefineLocaleMessage |
动手练习
- 基础集成:配置 vue-i18n,实现中英文切换
- 消息文件:抽取所有文案到语言文件
- 参数化:实现带参数的国际化文案
- 复数:实现 items 数量的复数处理
- 懒加载:异步加载语言包,优化首屏
推荐阅读
- 📖 Vue I18n 官方文档 — 完整指南
- 📖 ICU 消息格式 — 语法
- 🌐 i18n-ally — VSCode 插件
- 📖 国际化最佳实践 — 经验
下一章:第 140 章:主题与暗色模式 →