国际化 i18n 在 Vue3 项目完整实践
国际化i18n在Vue3项目完整实践
引言
随着企业级应用的全球化需求增长,国际化(Internationalization,简称 i18n)已成为标配功能。一个支持多语言的系统不仅能服务更多用户群体,还能提升产品的专业度和竞争力。然而,国际化不仅仅是文本翻译,还涉及日期格式、数字格式、货币符号、布局方向等多维度的本地化处理。
本文分享在 Vue 3 + vue-i18n 项目中实现国际化的完整实践,涵盖中文/英文/繁体中文三语言支持、语言切换即时生效、翻译文件组织、以及新增语言包的扩展方案。
核心内容
一、国际化整体架构
flowchart TB
A[国际化架构] --> B[翻译资源层]
A --> C[状态管理层]
A --> D[视图渲染层]
A --> E[工具函数层]
B --> B1[语言包JSON文件]
B --> B2[按模块拆分]
B --> B3[懒加载支持]
C --> C1[Pinia 语言状态]
C --> C2[localStorage 持久化]
C --> C3[服务端同步]
D --> D1[$t 函数]
D --> D2[v-t 指令]
D --> D3[组件内翻译]
E --> E1[日期格式化]
E --> E2[数字格式化]
E --> E3[复数处理]
二、vue-i18n 基础配置
2.1 安装依赖
npm install vue-i18n@9
2.2 创建 i18n 实例
/**
* i18n 国际化配置
* 支持中文简体、英文、繁体中文
*/
import { createI18n } from 'vue-i18n'
// 支持的语言列表
export const SUPPORT_LOCALES = [
{ code: 'zh-CN', name: '简体中文', flag: '🇨🇳' },
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'zh-TW', name: '繁體中文', flag: '🇹🇼' },
]
// 默认语言
const DEFAULT_LOCALE = 'zh-CN'
// 创建 i18n 实例
const i18n = createI18n({
legacy: false, // 使用 Composition API 模式
locale: DEFAULT_LOCALE, // 默认语言
fallbackLocale: 'zh-CN', // 回退语言
globalInjection: true, // 全局注入 $t 函数
messages: {}, // 初始为空,按需加载
})
export default i18n
三、翻译文件组织
3.1 目录结构
src/locales/
├── index.js # i18n 入口配置
├── zh-CN/ # 简体中文
│ ├── common.json # 公共翻译
│ ├── menu.json # 菜单翻译
│ ├── validation.json # 表单验证
│ ├── table.json # 表格相关
│ ├── button.json # 按钮文本
│ └── modules/ # 业务模块
│ ├── user.json
│ ├── role.json
│ ├── dept.json
│ └── dict.json
├── en/ # 英文
│ ├── common.json
│ ├── menu.json
│ ├── validation.json
│ ├── table.json
│ ├── button.json
│ └── modules/
│ ├── user.json
│ ├── role.json
│ ├── dept.json
│ └── dict.json
└── zh-TW/ # 繁体中文
├── common.json
├── menu.json
├── validation.json
├── table.json
├── button.json
└── modules/
├── user.json
├── role.json
├── dept.json
└── dict.json
3.2 翻译文件示例
zh-CN/common.json:
{
"app": {
"title": "管理系统",
"loading": "加载中...",
"noData": "暂无数据",
"success": "操作成功",
"fail": "操作失败",
"confirm": "确认",
"cancel": "取消",
"close": "关闭",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"add": "新增",
"search": "搜索",
"reset": "重置",
"export": "导出",
"import": "导入",
"refresh": "刷新",
"back": "返回",
"more": "更多",
"detail": "详情",
"status": "状态",
"createTime": "创建时间",
"updateTime": "更新时间",
"remark": "备注",
"operation": "操作"
}
}
en/common.json:
{
"app": {
"title": "Management System",
"loading": "Loading...",
"noData": "No Data",
"success": "Operation Successful",
"fail": "Operation Failed",
"confirm": "Confirm",
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"search": "Search",
"reset": "Reset",
"export": "Export",
"import": "Import",
"refresh": "Refresh",
"back": "Back",
"more": "More",
"detail": "Detail",
"status": "Status",
"createTime": "Create Time",
"updateTime": "Update Time",
"remark": "Remark",
"operation": "Operation"
}
}
zh-CN/validation.json:
{
"required": "{field}不能为空",
"minLength": "{field}长度不能少于{min}个字符",
"maxLength": "{field}长度不能超过{max}个字符",
"email": "请输入有效的邮箱地址",
"phone": "请输入有效的手机号码",
"idCard": "请输入有效的身份证号",
"number": "{field}必须为数字",
"integer": "{field}必须为整数",
"url": "请输入有效的URL地址",
"range": "{field}必须在{min}到{max}之间"
}
en/validation.json:
{
"required": "{field} is required",
"minLength": "{field} must be at least {min} characters",
"maxLength": "{field} must be at most {max} characters",
"email": "Please enter a valid email address",
"phone": "Please enter a valid phone number",
"idCard": "Please enter a valid ID card number",
"number": "{field} must be a number",
"integer": "{field} must be an integer",
"url": "Please enter a valid URL",
"range": "{field} must be between {min} and {max}"
}
四、语言包按需加载
大型项目中翻译文件可能很大,按需加载可以优化首屏性能:
/**
* 语言包加载器
* 支持按需加载和全量加载两种模式
*/
import i18n from '@/locales'
/** 语言包模块映射 */
const messageModules = import.meta.glob('../locales/*/modules/*.json')
/**
* 加载指定语言的公共翻译
* @param {string} locale 语言代码
*/
async function loadCommonMessages(locale) {
const modules = [
`../locales/${locale}/common.json`,
`../locales/${locale}/menu.json`,
`../locales/${locale}/validation.json`,
`../locales/${locale}/table.json`,
`../locales/${locale}/button.json`,
]
const messages = {}
const results = await Promise.all(
modules.map(path => import(path /* @vite-ignore */))
)
results.forEach((mod, index) => {
const key = modules[index].split('/').pop().replace('.json', '')
messages[key] = mod.default
})
return messages
}
/**
* 加载指定语言和模块的翻译
* @param {string} locale 语言代码
* @param {string} module 模块名称
*/
async function loadModuleMessages(locale, module) {
const path = `../locales/${locale}/modules/${module}.json`
try {
const mod = await import(path /* @vite-ignore */)
return { [module]: mod.default }
} catch (e) {
console.warn(`加载翻译模块失败: ${locale}/modules/${module}`)
return {}
}
}
/**
* 设置当前语言并加载翻译
* @param {string} locale 语言代码
*/
export async function setI18nLanguage(locale) {
// 加载公共翻译
const messages = await loadCommonMessages(locale)
// 合并到 i18n
i18n.global.setLocaleMessage(locale, messages)
i18n.global.locale.value = locale
// 设置 HTML lang 属性
document.documentElement.setAttribute('lang', locale)
// 持久化
localStorage.setItem('locale', locale)
}
/**
* 按需加载业务模块翻译
* @param {string} module 模块名称
*/
export async function loadModule(module) {
const locale = i18n.global.locale.value
const messages = await loadModuleMessages(locale, module)
// 合并到现有翻译中
const existing = i18n.global.getLocaleMessage(locale)
i18n.global.setLocaleMessage(locale, { ...existing, ...messages })
}
五、语言切换即时生效
5.1 语言状态管理
/**
* 国际化状态管理
*/
import { defineStore } from 'pinia'
import { setI18nLanguage, SUPPORT_LOCALES } from '@/locales'
export const useLocaleStore = defineStore('locale', {
state: () => ({
/** 当前语言 */
currentLocale: localStorage.getItem('locale') || 'zh-CN',
/** 支持的语言列表 */
supportedLocales: SUPPORT_LOCALES,
}),
getters: {
/** 当前语言信息 */
currentLocaleInfo: (state) => {
return SUPPORT_LOCALES.find(l => l.code === state.currentLocale)
},
},
actions: {
/** 切换语言 */
async switchLocale(locale) {
if (this.currentLocale === locale) return
await setI18nLanguage(locale)
this.currentLocale = locale
// 同步到服务端(用户偏好)
try {
await updateUserLocale({ locale })
} catch (e) {
// 服务端同步失败不影响本地使用
console.warn('语言偏好同步服务端失败', e)
}
},
},
})
5.2 语言切换组件
<template>
<el-dropdown trigger="click" @command="onLocaleChange">
<span class="locale-switcher">
<el-icon><Globe /></el-icon>
{{ currentLocaleInfo?.name }}
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="locale in localeStore.supportedLocales"
:key="locale.code"
:command="locale.code"
:class="{ active: localeStore.currentLocale === locale.code }"
>
{{ locale.flag }} {{ locale.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
import { computed } from 'vue'
import { Globe } from '@element-plus/icons-vue'
import { useLocaleStore } from '@/store/modules/locale'
const localeStore = useLocaleStore()
const currentLocaleInfo = computed(() => localeStore.currentLocaleInfo)
const onLocaleChange = (locale) => {
localeStore.switchLocale(locale)
}
</script>
六、组件中使用翻译
6.1 模板中使用
<template>
<div class="user-management">
<!-- 使用 $t 函数 -->
<h2>{{ $t('user.title') }}</h2>
<!-- 带参数的翻译 -->
<p>{{ $t('user.welcome', { name: userName }) }}</p>
<!-- 在属性中使用 -->
<el-input :placeholder="$t('user.searchPlaceholder')" />
<!-- 在表单验证中使用 -->
<el-form :rules="formRules" :model="formData">
<el-form-item :label="$t('user.username')" prop="username">
<el-input v-model="formData.username" />
</el-form-item>
</el-form>
</div>
</template>
6.2 Composition API 中使用
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
// 在 JS 逻辑中使用翻译
const formRules = computed(() => ({
username: [
{ required: true, message: t('validation.required', { field: t('user.username') }), trigger: 'blur' },
{ min: 2, max: 20, message: t('validation.range', { field: t('user.username'), min: 2, max: 20 }), trigger: 'blur' },
],
email: [
{ required: true, message: t('validation.required', { field: t('user.email') }), trigger: 'blur' },
{ type: 'email', message: t('validation.email'), trigger: 'blur' },
],
}))
// 动态翻译表格列
const tableColumns = computed(() => [
{ prop: 'username', label: t('user.username') },
{ prop: 'email', label: t('user.email') },
{ prop: 'phone', label: t('user.phone') },
{ prop: 'status', label: t('common.app.status') },
{ prop: 'createTime', label: t('common.app.createTime') },
])
</script>
七、日期与数字格式化
7.1 日期格式化
/**
* 日期格式化工具
* 根据当前语言自动调整日期格式
*/
import { useI18n } from 'vue-i18n'
/** 各语言的日期格式偏好 */
const DATE_FORMATS = {
'zh-CN': {
date: 'YYYY年MM月DD日',
datetime: 'YYYY年MM月DD日 HH:mm:ss',
short: 'MM-DD',
time: 'HH:mm:ss',
},
'en': {
date: 'MM/DD/YYYY',
datetime: 'MM/DD/YYYY HH:mm:ss',
short: 'MM/DD',
time: 'HH:mm:ss',
},
'zh-TW': {
date: 'YYYY年MM月DD日',
datetime: 'YYYY年MM月DD日 HH:mm:ss',
short: 'MM-DD',
time: 'HH:mm:ss',
},
}
/**
* 格式化日期
* @param {Date|string} date 日期
* @param {string} format 格式类型:date/datetime/short/time
* @param {string} locale 语言代码
*/
export function formatDate(date, format = 'datetime', locale = 'zh-CN') {
const dayjs = // 使用 dayjs 或其他日期库
const formatStr = DATE_FORMATS[locale]?.[format] || DATE_FORMATS['zh-CN'][format]
return dayjs(date).format(formatStr)
}
7.2 数字格式化
/**
* 数字格式化工具
* 根据当前语言自动调整数字格式(千分位、小数点等)
*/
/**
* 格式化数字
* @param {number} number 数字
* @param {string} locale 语言代码
* @param {object} options Intl.NumberFormat 选项
*/
export function formatNumber(number, locale = 'zh-CN', options = {}) {
return new Intl.NumberFormat(locale, options).format(number)
}
// 使用示例:
// formatNumber(1234567.89, 'zh-CN') → "1,234,567.89"
// formatNumber(1234567.89, 'en') → "1,234,567.89"
/**
* 格式化货币
*/
export function formatCurrency(number, locale = 'zh-CN', currency = 'CNY') {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(number)
}
// 使用示例:
// formatCurrency(1234.5, 'zh-CN', 'CNY') → "¥1,234.50"
// formatCurrency(1234.5, 'en', 'USD') → "$1,234.50"
八、新增语言包扩展方案
当需要支持新语言时,遵循以下流程:
flowchart TB
A[新增语言需求] --> B[创建语言目录
如: src/locales/ja/]
B --> C[复制现有语言包
作为翻译模板]
C --> D[逐文件翻译
替换所有文本值]
D --> E[在 SUPPORT_LOCALES
中注册新语言]
E --> F[测试验证
所有页面翻译正确]
F --> G[发布上线]
style A fill:#FF9800,color:#fff
style G fill:#4CAF50,color:#fff
8.1 扩展步骤
// 步骤1:在 SUPPORT_LOCALES 中添加新语言
export const SUPPORT_LOCALES = [
{ code: 'zh-CN', name: '简体中文', flag: '🇨🇳' },
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'zh-TW', name: '繁體中文', flag: '🇹🇼' },
// 新增日语支持
{ code: 'ja', name: '日本語', flag: '🇯🇵' },
]
# 步骤2:创建语言包目录和文件
mkdir -p src/locales/ja/modules
# 复制 zh-CN 下的所有 JSON 文件到 ja/ 目录
# 然后逐个翻译 JSON 中的值(注意只翻译值,不要翻译键名)
// src/locales/ja/common.json
{
"app": {
"title": "管理システム",
"loading": "読み込み中...",
"noData": "データなし",
"success": "操作成功",
"fail": "操作失敗",
"confirm": "確認",
"cancel": "キャンセル",
"save": "保存",
"delete": "削除",
"edit": "編集",
"add": "追加",
"search": "検索",
"reset": "リセット"
}
}
8.2 翻译完整性检查
/**
* 翻译完整性检查工具
* 对比不同语言包的 key 是否一致,发现遗漏的翻译
*/
export function checkTranslationIntegrity(baseLocale = 'zh-CN') {
const baseMessages = i18n.global.getLocaleMessage(baseLocale)
const issues = []
SUPPORT_LOCALES.forEach(({ code }) => {
if (code === baseLocale) return
const targetMessages = i18n.global.getLocaleMessage(code)
const missingKeys = findMissingKeys(baseMessages, targetMessages, code)
if (missingKeys.length > 0) {
issues.push({
locale: code,
missingKeys,
count: missingKeys.length,
})
}
})
return issues
}
/**
* 递归查找缺失的翻译 key
*/
function findMissingKeys(source, target, prefix = '') {
const missing = []
for (const key in source) {
const fullKey = prefix ? `${prefix}.${key}` : key
if (!(key in target)) {
missing.push(fullKey)
} else if (typeof source[key] === 'object' && source[key] !== null) {
missing.push(...findMissingKeys(source[key], target[key] || {}, fullKey))
}
}
return missing
}
九、服务端翻译同步
sequenceDiagram
participant C as 客户端
participant S as 服务端
participant DB as 数据库
C->>S: 登录时获取用户语言偏好
S->>DB: 查询 user_config
DB-->>S: locale = 'en'
S-->>C: 返回语言偏好
C->>C: 设置语言为 en
C->>C: 加载英文翻译包
Note over C: 用户切换语言
C->>S: PUT /api/user/locale {locale: 'zh-CN'}
S->>DB: 更新用户语言偏好
S-->>C: 更新成功
C->>C: 切换为中文
C->>C: 加载中文翻译包
结论与建议
核心要点
- 翻译文件按模块拆分:避免单个文件过大,支持按需加载
- Composition API 模式:使用
useI18n()而非this.$t,更灵活 - 语言切换即时生效:无需刷新页面,切换后所有文本自动更新
- 参数化翻译:使用
{field}占位符,避免翻译拼接 - 完整性检查:开发阶段检查翻译遗漏,确保每种语言的 key 一致
最佳实践建议
| 建议 | 说明 |
|---|---|
| 键名使用点分路径 | user.management.title 比 userManagementTitle 更清晰 |
| 翻译值不要拼接 | 用参数化代替字符串拼接,避免语序问题 |
| 日期数字本地化 | 使用 Intl API 处理日期和数字格式 |
| 懒加载翻译包 | 大型项目按模块懒加载,优化首屏性能 |
| 翻译复用 | 公共文本(按钮、状态等)放在 common.json,避免重复 |
| 自动化检查 | CI 流程中加入翻译完整性检查 |
| 上下文标注 | 为翻译人员提供上下文注释,避免歧义 |