Vue3 国际化
大约 11 分钟约 3324 字
Vue3 国际化
简介
国际化(i18n)是应用支持多语言的核心能力。Vue3 通过 vue-i18n 库实现国际化,支持翻译文本、日期格式化、数字格式化和复数处理。掌握 i18n 配置和使用,可以为应用添加多语言支持,覆盖不同地区用户。
i18n 是 "internationalization" 的缩写(首字母 i + 中间 18 个字母 + 末字母 n)。国际化不仅仅是翻译文本,还包括日期/时间格式、数字/货币格式、文字方向(LTR/RTL)、复数规则等本地化(l10n,localization)适配。
国际化的完整范围:
- 翻译:UI 文本、提示信息、错误消息
- 格式化:日期、时间、数字、货币
- 布局:RTL(阿拉伯语、希伯来语)支持
- 图片:根据地区显示不同内容
- SEO:多语言 URL 结构和 meta 信息
特点
基础配置
安装和配置
// i18n/index.ts
import { createI18n } from 'vue-i18n'
import zh from './locales/zh'
import en from './locales/en'
const i18n = createI18n({
legacy: false, // 使用 Composition API 模式
locale: localStorage.getItem('locale') || 'zh',
fallbackLocale: 'zh',
messages: { zh, en }
})
export default i18n
// main.ts
app.use(i18n)语言文件
// i18n/locales/zh.ts
export default {
common: {
confirm: '确认',
cancel: '取消',
save: '保存',
delete: '删除',
search: '搜索',
loading: '加载中...',
noData: '暂无数据'
},
menu: {
home: '首页',
users: '用户管理',
settings: '系统设置',
profile: '个人中心'
},
user: {
login: '登录',
logout: '退出登录',
username: '用户名',
password: '密码',
loginSuccess: '登录成功',
loginFailed: '用户名或密码错误'
},
validation: {
required: '{field}不能为空',
minLength: '{field}至少{min}个字符',
email: '请输入有效的邮箱地址'
}
}
// i18n/locales/en.ts
export default {
common: {
confirm: 'Confirm',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
search: 'Search',
loading: 'Loading...',
noData: 'No Data'
},
menu: {
home: 'Home',
users: 'Users',
settings: 'Settings',
profile: 'Profile'
},
user: {
login: 'Login',
logout: 'Logout',
username: 'Username',
password: 'Password',
loginSuccess: 'Login successful',
loginFailed: 'Invalid username or password'
},
validation: {
required: '{field} is required',
minLength: '{field} must be at least {min} characters',
email: 'Please enter a valid email'
}
}语言文件组织策略
// 推荐按模块拆分语言文件,便于维护
// i18n/locales/zh/index.ts
import common from './common'
import menu from './menu'
import user from './user'
import dashboard from './dashboard'
import settings from './settings'
export default {
common,
menu,
user,
dashboard,
settings,
}
// i18n/locales/zh/common.ts
export default {
confirm: '确认',
cancel: '取消',
save: '保存',
delete: '删除',
edit: '编辑',
search: '搜索',
reset: '重置',
submit: '提交',
loading: '加载中...',
noData: '暂无数据',
operationSuccess: '操作成功',
operationFailed: '操作失败',
confirmDelete: '确定要删除该{type}吗?',
deleteSuccess: '删除成功',
saveSuccess: '保存成功',
}
// i18n/locales/zh/user.ts
export default {
login: '登录',
logout: '退出登录',
register: '注册',
username: '用户名',
password: '密码',
confirmPassword: '确认密码',
email: '邮箱',
phone: '手机号',
avatar: '头像',
role: '角色',
status: '状态',
active: '启用',
inactive: '禁用',
lastLoginAt: '最后登录时间',
loginSuccess: '登录成功',
loginFailed: '用户名或密码错误',
logoutConfirm: '确定要退出登录吗?',
passwordMismatch: '两次输入的密码不一致',
userExists: '用户名已存在',
}
// i18n/locales/zh/validation.ts
export default {
required: '{field}不能为空',
minLength: '{field}至少{min}个字符',
maxLength: '{field}最多{max}个字符',
email: '请输入有效的邮箱地址',
phone: '请输入有效的手机号',
url: '请输入有效的URL',
number: '请输入有效的数字',
integer: '请输入整数',
positive: '请输入正数',
range: '{field}应在{min}到{max}之间',
pattern: '{field}格式不正确',
unique: '{field}已存在',
}
// i18n/locales/zh/dashboard.ts
export default {
title: '仪表盘',
totalUsers: '用户总数',
activeUsers: '活跃用户',
todayOrders: '今日订单',
totalRevenue: '总收入',
trendChart: '趋势图',
recentActivities: '最近动态',
welcomeMessage: '欢迎回来,{name}',
todaySummary: '今日概览',
}使用方式
模板和脚本中使用
<template>
<!-- 模板中使用 -->
<h1>{{ t('menu.home') }}</h1>
<button>{{ t('common.save') }}</button>
<!-- 带参数 -->
<p>{{ t('validation.required', { field: t('user.username') }) }}</p>
<!-- 复数处理 -->
<p>{{ t('items', count, { count }) }}</p>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
// 脚本中获取翻译文本
const title = t('menu.home')
// 切换语言
function switchLanguage(lang: string) {
locale.value = lang
localStorage.setItem('locale', lang)
document.documentElement.lang = lang
}
</script>翻译函数详解
const { t, rt, d, n, te } = useI18n()
// t() — 基础翻译
t('common.confirm') // '确认'
t('common.confirm', { named: { key: 'val' }}) // 带命名参数
t('user.welcomeMessage', { name: '张三' }) // '欢迎回来,张三'
// rt() — 富文本翻译(支持 HTML 标签)
// locales/zh.ts 中定义
// richText: '欢迎 <b>{name}</b>,请查看 <a href="/help">帮助</a>'
const richResult = rt('richText', { name: '张三' })
// 返回 VNode,可以在模板中渲染
// te() — 检查翻译键是否存在
if (te('menu.home')) {
console.log(t('menu.home'))
}
// tm() — 获取翻译消息对象
const messages = tm('menu')
// 返回整个 menu 对象 { home: '首页', users: '用户管理', ... }
// n() — 数字格式化
n(123456.789) // '123,456.789'
n(123456.789, { style: 'currency', currency: 'CNY' }) // '¥123,456.79'
n(0.85, { style: 'percent' }) // '85%'
n(1234, { notation: 'compact' }) // '1.2K'
// d() — 日期格式化
d(new Date(), 'short') // '2024/1/15'
d(new Date(), 'long') // '2024年1月15日'
d(new Date(), { year: 'numeric', month: 'long', day: 'numeric' })复数处理
// i18n/locales/zh.ts
export default {
items: '0 个项目 | {count} 个项目 | {count} 个项目',
// 或者使用管道符指定不同数量
apple: '没有苹果 | 一个苹果 | {count} 个苹果',
}
// i18n/locales/en.ts
export default {
items: 'no items | one item | {count} items',
apple: 'no apples | {count} apple | {count} apples',
}
// 使用
t('items', 0) // zh: '0 个项目' en: 'no items'
t('items', 1) // zh: '1 个项目' en: 'one item'
t('items', 5) // zh: '5 个项目' en: '5 items'
// 带参数的复数
t('apple', 5, { count: 5 }) // en: '5 apples'在组件外部使用 i18n
// 在 Vue 组件外部(如工具函数、API 层)使用 i18n
import i18n from '@/i18n'
// 方式 1:直接访问全局实例
const message = i18n.global.t('common.confirm')
// 方式 2:封装翻译工具函数
// utils/i18n.ts
export function $t(key: string, params?: Record<string, any>): string {
return i18n.global.t(key, params)
}
// 在 API 错误处理中使用
// api/request.ts
import { $t } from '@/utils/i18n'
axios.interceptors.response.use(
response => response,
error => {
const message = $t(`error.${error.response?.data?.code || 'unknown'}`)
ElMessage.error(message)
return Promise.reject(error)
}
)
// 在路由守卫中使用
// router/guard.ts
import i18n from '@/i18n'
router.beforeEach((to) => {
document.title = i18n.global.t(`route.${to.name}`)
})语言切换组件
切换器
<!-- LanguageSwitch.vue -->
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { locale, t } = useI18n()
const languages = [
{ value: 'zh', label: '简体中文' },
{ value: 'en', label: 'English' }
]
function changeLanguage(lang: string) {
locale.value = lang
localStorage.setItem('locale', lang)
// 更新 HTML lang 属性
document.documentElement.lang = lang
}
</script>
<template>
<el-dropdown @command="changeLanguage">
<span class="lang-switch">
{{ languages.find(l => l.value === locale)?.label }}
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="lang in languages"
:key="lang.value"
:command="lang.value">
{{ lang.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>语言切换持久化与初始化
// i18n/index.ts —— 生产级配置
import { createI18n } from 'vue-i18n'
import zh from './locales/zh'
import en from './locales/en'
// 支持的语言列表
export const SUPPORT_LOCALES = ['zh', 'en'] as const
export type SupportLocale = (typeof SUPPORT_LOCALES)[number]
// 获取初始语言
function getInitialLocale(): SupportLocale {
// 优先级:localStorage > navigator.language > 默认值
const stored = localStorage.getItem('locale')
if (stored && SUPPORT_LOCALES.includes(stored as SupportLocale)) {
return stored as SupportLocale
}
const browserLang = navigator.language.split('-')[0]
if (SUPPORT_LOCALES.includes(browserLang as SupportLocale)) {
return browserLang as SupportLocale
}
return 'zh'
}
const i18n = createI18n({
legacy: false,
locale: getInitialLocale(),
fallbackLocale: 'zh', // 翻译缺失时使用的语言
globalInjection: true, // 允许在模板中直接使用 $t
messages: {
zh,
en,
},
datetimeFormats: {
en: {
short: {
year: 'numeric',
month: 'short',
day: 'numeric',
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
},
},
zh: {
short: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
},
},
},
numberFormats: {
en: {
currency: {
style: 'currency',
currency: 'USD',
notation: 'standard',
},
},
zh: {
currency: {
style: 'currency',
currency: 'CNY',
notation: 'standard',
},
},
},
})
export default i18n懒加载语言包
按需加载
// i18n/lazy.ts
import type { I18n } from 'vue-i18n'
const loadedLanguages: string[] = []
export async function loadLanguageAsync(i18n: I18n, lang: string) {
if (loadedLanguages.includes(lang)) return
const messages = await import(`./locales/${lang}.ts`)
i18n.global.setLocaleMessage(lang, messages.default)
loadedLanguages.push(lang)
}
// 路由切换时加载
router.beforeEach(async (to) => {
const lang = to.query.lang as string || 'zh'
await loadLanguageAsync(i18n, lang)
})高级懒加载方案
// i18n/lazy.ts —— 带回退和错误处理的懒加载
const loadedLanguages = new Set<string>()
const loadingPromises = new Map<string, Promise<void>>()
export async function loadLanguageAsync(
i18n: ReturnType<typeof createI18n>,
lang: string,
fallback = 'zh'
): Promise<void> {
// 已经加载过,直接返回
if (loadedLanguages.has(lang)) {
i18n.global.locale.value = lang
return
}
// 正在加载中,复用同一个 Promise(防止并发加载)
if (loadingPromises.has(lang)) {
await loadingPromises.get(lang)
i18n.global.locale.value = lang
return
}
const promise = (async () => {
try {
const messages = await import(`./locales/${lang}/index.ts`)
i18n.global.setLocaleMessage(lang, messages.default)
loadedLanguages.add(lang)
// 同步加载日期和数字格式
try {
const datetimeFormats = await import(`./locales/${lang}/datetime.ts`)
i18n.global.setDateTimeFormat(lang, datetimeFormats.default)
} catch { /* 没有日期格式文件,忽略 */ }
try {
const numberFormats = await import(`./locales/${lang}/number.ts`)
i18n.global.setNumberFormat(lang, numberFormats.default)
} catch { /* 没有数字格式文件,忽略 */ }
} catch (error) {
console.error(`Failed to load language "${lang}":`, error)
// 回退到默认语言
i18n.global.locale.value = fallback
} finally {
loadingPromises.delete(lang)
}
})()
loadingPromises.set(lang, promise)
await promise
if (loadedLanguages.has(lang)) {
i18n.global.locale.value = lang
}
}
// 使用
// router/index.ts
router.beforeEach(async (to) => {
const lang = to.query.lang as string || localStorage.getItem('locale') || 'zh'
await loadLanguageAsync(i18n, lang)
})与 Element Plus 集成
// Element Plus 的组件文案也需要国际化
// i18n/index.ts
import { createI18n } from 'vue-i18n'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
const i18n = createI18n({
legacy: false,
locale: 'zh',
messages: { zh, en },
})
export default i18n
// 在 App.vue 中使用
import { useI18n } from 'vue-i18n'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
const { locale } = useI18n()
const elementLocale = computed(() => locale.value === 'zh' ? zhCn : en)<template>
<el-config-provider :locale="elementLocale">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
const { locale } = useI18n()
const elementLocale = computed(() => locale.value === 'zh' ? zhCn : en)
</script>翻译缺失处理
// 开发环境检测缺失的翻译键
// plugins/i18n-reporter.ts
import type { I18n } from 'vue-i18n'
export function setupMissingKeyReporter(i18n: I18n) {
if (import.meta.env.PROD) return
const missingKeys = new Set<string>()
i18n.global.t = new Proxy(i18n.global.t, {
apply(target, thisArg, args) {
const key = args[0] as string
const result = target.apply(thisArg, args)
// 如果翻译结果和 key 相同,说明翻译缺失
if (result === key && !key.includes('[')) {
if (!missingKeys.has(key)) {
missingKeys.add(key)
console.warn(`[i18n] Missing translation for key: "${key}"`)
}
}
return result
},
})
// 暴露所有缺失的键
;(window as any).__missingI18nKeys = missingKeys
}RTL(从右到左)支持
// RTL 语言支持(阿拉伯语、希伯来语)
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur']
function setDirection(lang: string) {
const isRtl = RTL_LOCALES.includes(lang)
document.documentElement.dir = isRtl ? 'rtl' : 'ltr'
document.documentElement.classList.toggle('rtl', isRtl)
}
// 在语言切换时调用
function changeLanguage(lang: string) {
locale.value = lang
setDirection(lang)
localStorage.setItem('locale', lang)
}
// CSS 中的 RTL 适配
/* styles/rtl.scss */
.rtl {
direction: rtl;
text-align: right;
}
/* 使用逻辑属性替代物理属性 */
/* margin-left → margin-inline-start */
/* padding-right → padding-inline-end */
/* text-align: left → text-align: start */
/* float: left → float: inline-start */优点
缺点
总结
Vue3 国际化推荐 vue-i18n(Composition API 模式)。语言文件按模块组织(common/menu/user)。模板用 t() 函数,带参数用插值 {field}。语言切换保存到 localStorage 持久化。大量语言包用动态 import 懒加载。
最佳实践清单:
- 语言文件按模块拆分,每个文件控制在 100 行以内
- 使用 legacy: false 开启 Composition API 模式
- 配置 fallbackLocale 防止翻译缺失导致显示 key
- 懒加载非默认语言包,减小初始包体积
- 与 Element Plus 集成时同步切换 el-config-provider 的 locale
- 开发环境启用缺失键检测,上线前检查覆盖率
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
适用场景
- 当你准备把《Vue3 国际化》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Vue3 国际化》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Vue3 国际化》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Vue3 国际化》最大的收益和代价分别是什么?
