Vue I18n多语言实战:从配置到动态切换的完整指南
我的写法,亲测靠谱
在 Vue 项目里做国际化,我基本都用 Vue I18n。但说实话,一开始用得特别糙,直接在组件里写 $t('xxx'),语言包全塞在一个文件里,后期维护简直噩梦。后来折腾了几个项目,踩了不少坑,才慢慢摸索出一套自己觉得顺手的写法。
我现在一般这么干:把语言资源按模块拆分,配合动态导入,再加一层封装函数避免重复代码。比如用户模块、商品模块、支付流程,各自有独立的 zh-CN.json 和 en-US.json。这样结构清晰,谁改哪块一目了然。
初始化 I18n 的时候,我习惯用动态加载的方式,而不是一股脑全 import 进来。特别是中后台项目,语言包可能上百 KB,首屏加载压力大。下面是我现在项目的标准配置:
import { createI18n } from 'vue-i18n'
const loadLocaleMessages = () => {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,s]+.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
const i18n = createI18n({
legacy: false,
locale: localStorage.getItem('lang') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: loadLocaleMessages()
})
export default i18n
这里注意:legacy: false 很关键!Vue 3 + Vue I18n 9+ 默认是 Composition API 模式,如果你还用 $t 这种 Options API 的写法,会报错或者不生效。我一开始没注意,折腾了半天发现是这个配置问题。
这几种错误写法,别再踩坑了
先说一个最蠢但我真干过的事:在模板里直接拼字符串。
<!-- 别这么干! -->
<p>{{ $t('hello') + userName }}</p>
这种写法不仅破坏了语义,还让翻译人员抓狂。正确做法是用占位符:
{
"greeting": "你好,{name}!"
}
<!-- 正确 -->
<p>{{ $t('greeting', { name: userName }) }}</p>
另一个高频错误:把翻译 key 写得太具体,比如 user_profile_edit_button_text。看起来很清晰,但一旦 UI 改了文字,你得改两处——代码和语言包。我建议用语义化命名,比如 actions.edit,然后在不同上下文复用。当然,如果确实语义不同(比如“编辑资料”和“编辑商品”),那就分开,但别为了偷懒把 UI 文字直接当 key。
还有人喜欢在 JS 逻辑里硬编码语言判断,比如:
// 千万别这么干
if (this.$i18n.locale === 'zh-CN') {
// 中文逻辑
} else {
// 英文逻辑
}
这种写法把业务逻辑和语言耦合死了。万一以后加个日语,你得到处改。正确做法是把差异放到语言包里,或者用配置项驱动,而不是靠 locale 值做判断。
实际项目中的坑
最近一个项目,产品要求支持“阿拉伯语”,从右到左(RTL)布局。我一开始以为只要切语言就行,结果发现很多样式没适配。Vue I18n 本身不处理 RTL,得配合 CSS 方向属性。我的做法是在根组件监听 locale 变化,动态加 class:
// main.js 或 App.vue
watch(
() => i18n.global.locale,
(newLocale) => {
document.documentElement.dir = newLocale === 'ar' ? 'rtl' : 'ltr'
},
{ immediate: true }
)
另外,动态加载语言包时要注意缓存。我之前每次切换语言都重新 fetch,结果网络慢的时候按钮点好几次才生效。后来加了内存缓存,第一次加载后存起来,切换就快了:
const loadedLanguages = []
async function setI18nLanguage(lang) {
if (i18n.global.locale !== lang) {
if (!loadedLanguages.includes(lang)) {
const msgs = await import(@/locales/${lang}.json)
i18n.global.setLocaleMessage(lang, msgs.default)
loadedLanguages.push(lang)
}
i18n.global.locale = lang
localStorage.setItem('lang', lang)
}
}
还有一个细节:日期、数字格式。很多人直接用 new Date().toLocaleDateString(),但这个方法依赖浏览器语言环境,不一定和你当前应用语言一致。稳妥做法是用 Vue I18n 的 d() 和 n() 方法,它会自动用你设置的 locale 格式化。
<!-- 在 setup() 里 -->
<script setup>
import { useI18n } from 'vue-i18n'
const { d, n } = useI18n()
const formatDate = d(new Date(), 'short')
const formatNumber = n(123456.789, 'currency')
</script>
配套的语言包里要定义这些格式:
{
"dateTimeFormats": {
"short": {
"year": "numeric",
"month": "short",
"day": "numeric"
}
},
"numberFormats": {
"currency": {
"style": "currency",
"currency": "CNY",
"currencyDisplay": "symbol"
}
}
}
封装一下,少写点重复代码
我发现很多组件里都要写 useI18n() 然后解构 t,特别啰嗦。于是我在项目里加了个小工具函数:
// composables/useT.js
import { useI18n } from 'vue-i18n'
export function useT() {
const { t } = useI18n()
return t
}
用的时候就清爽多了:
// 组件里
const t = useT()
const title = t('page.title')
虽然只是少写一行,但几十个组件累积下来,体验提升很明显。而且万一以后 Vue I18n API 变了,我只改一处就行。
对了,测试也别忘了。我以前觉得翻译不用测,结果上线后发现某个 key 拼错了,页面直接显示 key 字符串。现在我会在单元测试里 mock I18n,确保所有 $t 调用都有对应 key:
// vitest 或 jest 里
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key // 直接返回 key,方便断言
})
}))
结尾唠叨两句
以上是我这几年用 Vue I18n 踩坑后总结的一套实践。说实话,这套方案也不是完美的——比如动态加载语言包在 SSR 场景下还得额外处理,不过对于大多数 SPA 项目已经够用了。
核心就几点:别把语言包堆成一坨、别在模板里拼字符串、别用 locale 做业务判断、记得处理 RTL 和格式化。剩下的,根据项目规模灵活调整就行。
以上是我个人对 Vue I18n 的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如和 CMS 对接自动更新语言包),后续会继续分享这类博客。

暂无评论