在 Vue 3 + Vite 项目中实现 i18n(实战)
摘要
本文以一个真实的 Vue 3 + Vite + TypeScript 项目为例,逐步展示如何接入 vue-i18n@9、实现懒加载语言包、在组件与路由中使用翻译、与 Pinia 同步语言状态,并给出验证与发布注意点。示例代码位于项目文件:src/i18n.ts、src/components/common/ub_language_switcher.vue 等。
目标读者
前端工程师、Vue3 使用者、希望把国际化从 PoC 推到生产环境的团队。
项目与前提
- 技术栈:Vue 3 + Vite + TypeScript
- 状态管理:Pinia(
src/stores/index.ts) - UI:Arco Design(全局注册在
src/main.ts) - 关键文件(项目内):
src/i18n.ts— i18n 初始化与懒加载实现src/locales/zh.ts/src/locales/en.ts— 语言包src/components/common/ub_language_switcher.vue— 语言切换组件示例src/main.ts— 在挂载前加载语言src/stores/index.ts— 可选的语言状态同步点src/router/index.ts— 路由 title 国际化示例
1. 安装
在项目根目录运行:
bash
npm install vue-i18n@9
2. 初始化(核心思路)
使用 Composition API 模式实现 loadLocale(lang) 做懒加载语言包。核心要点:
- 初始 messages 为空
- 使用
i18n.global.setLocaleMessage动态注入语言内容 - 将用户选择的语言保存在
localStorage中,避免刷新后丢失
关键代码(摘自 src/i18n.ts):
ts
import { createI18n } from 'vue-i18n'
const DEFAULT_LANG = localStorage.getItem('lang') || 'zh'
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: DEFAULT_LANG,
fallbackLocale: 'en',
messages: {}
})
export async function loadLocale(lang: string) {
if (i18n.global.availableLocales.includes(lang)) {
i18n.global.locale.value = lang
localStorage.setItem('lang', lang)
return
}
const msgs = await import(/* @vite-ignore */ `./locales/${lang}.ts`)
i18n.global.setLocaleMessage(lang, msgs.default || msgs)
i18n.global.locale.value = lang
localStorage.setItem('lang', lang)
}
export default i18n
3. 在入口注册并预加载语言
在 src/main.ts 中注册 i18n,并在挂载前加载默认语言,避免首屏闪烁:
ts
import i18n, { loadLocale } from './i18n'
app.use(i18n)
const lang = localStorage.getItem('lang') || 'zh'
loadLocale(lang).finally(() => app.mount('#app'))
4. 组件层切换(UI 示例)
项目中示例组件:src/components/common/ub_language_switcher.vue,使用 Arco 图标(或按钮)点击切换语言,调用 loadLocale 并写入 localStorage。示例要点:
- 切换时调用
loadLocale(lang) - 可选同步 Pinia:
useStore().setLanguage(lang)(如果实现) - 可使用
useI18n()监听locale
示例片段(摘录):
vue
<template>
<div class="ub-language-switcher" @click="toggleLanguage">
<IconChineseFill v-if="current === 'zh'"/>
<IconEnglishFill v-else />
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
// 切换时调用 loadLocale(...)
</script>
5. 懒加载与构建影响
- 懒加载语言包会把每个语言打成独立 chunk(Vite 的动态 import 行为)。
- 好处:首包体积小;坏处:构建输出语言包数量增多。
- 验证:在浏览器 DevTools 的 Network 面板切换语言能看到 language chunk 请求。
6. Pinia 同步(可选)
在 src/stores/index.ts 中添加 language 状态与 setLanguage action,使语言成为全局状态:
ts
// state:
language: localStorage.getItem('lang') || 'zh',
// action:
async setLanguage(lang: string) {
this.language = lang
localStorage.setItem('lang', lang)
await import('@/i18n').then(m => m.loadLocale(lang))
}
7. 路由 title 国际化
把路由 meta.title 改为 meta.titleKey,并在切换后设置 document.title:
ts
import { tGlobal } from '@/i18n'
router.afterEach((to) => {
const key = (to.meta as any).titleKey
if (key) document.title = tGlobal(key as string) as string
})
8. 日期与数字本地化
- 推荐使用
dayjs或 Intl API。 - 若使用 dayjs,可在
loadLocale成功后动态加载 dayjs locale 并调用dayjs.locale(lang)。
9. 验证步骤
bash
npm install
npm run dev
验证点:
- localStorage 中
lang正确(zh或en) - 切换语言后页面文本变化
- Network 面板出现懒加载的 language chunk
- 如启用,浏览器标签页 title 随路由 key 变化
10. 常见问题与调试
- 未生效:确认
i18n已在main.ts注册并在挂载前加载语言 - 懒加载失败:检查语言包路径
src/locales/{lang}.ts - 非同步请求需要 locale header:在请求时读取
i18n.global.locale.value
11. SEO、可访问性与发布建议
- 使用
meta.titleKey和对应翻译填充 title/description(如需 SSR,需在服务端处理) - 为切换控件添加
aria-label或a-tooltip提升可访问性 - 发布时附带 2~3 张截图:语言切换、Network 懒加载、路由 title 变化
12. 结论与扩展
通过懒加载语言包和将语言状态与 Pinia 同步,可以把国际化做到既轻量又可维护。下一步可以考虑:机器翻译初稿 + 人工校验,或接入翻译管理平台(如 Crowdin、POEditor)。