国际化工程化与构建优化
概述
基础接入跑通之后,国际化的重点就从"能不能切换"转向"语言资源怎么维护得住"。本文讲工程化四件事:把语言包从 main.ts 抽离成独立目录与模块、用 @intlify/unplugin-vue-i18n 做构建优化、用 import.meta.glob 实现懒加载、以及如何把 Element Plus 内置 locale 整合进同一套切换流程并实现覆盖。
学习目标
- 把语言资源拆到
src/locales/、初始化逻辑收到src/modules/i18n.ts - 用
@intlify/unplugin-vue-i18n预编译 locale 资源、裁剪产物体积 - 用
import.meta.glob懒加载语言包,并用localesMap把"路径"整理成"语言代码" - 区分
loadLocaleMessages(异步 IO)与setI18nLanguage(状态更新)职责 - 整合 Element Plus locale、同步
dayjs、用 I18n Ally 提升开发体验
一、从 main.ts 抽离:目录与职责划分
入门演示把 messages 直接写在 main.ts 里最省事,但文案变多后文件会迅速膨胀、职责不清。工程化目标是:
src/
locales/
langs/
zh-CN.json
en-US.json
modules/
i18n.ts # 初始化、加载、切换统一封装页面组件只负责"触发切换",不关心底层如何加载 JSON、如何同步组件库 locale。典型的后台框架都采用这种"模块对外暴露 changeLocale(),页面只调入口"的模式。
二、构建插件与语言文件命名
@intlify/unplugin-vue-i18n 负责预编译 locale 资源、优化打包体积,当前主流包名即此(旧称 vite-plugin-vue-i18n 属历史命名):
// vite.config.ts
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
VueI18nPlugin({
include: [fileURLToPath(new URL('./src/locales/**', import.meta.url))],
runtimeOnly: true, // 生产走 runtime-only,减小体积
compositionOnly: true, // 只保留组合式 API
fullInstall: true // 安装完整 API / 组件 / 指令
})语言文件名应遵循 BCP 47 / IETF 标签(zh-CN、en-US),而不是 cn.json、english.json——这样和浏览器语言、组件库 locale、翻译平台约定一致,也便于扩展 zh-TW、en-GB 等地区变体。
三、import.meta.glob 懒加载与 localesMap
Vite 下 import.meta.glob() 默认返回"路径 → 动态导入函数"的懒加载映射,只有真正切到某语言时才加载对应文件,比一次性打进首屏更省。
const localeModules = import.meta.glob('../locales/langs/*.json')但业务层更希望用语言代码当 key。用 Object.entries + map + Object.fromEntries 做一次转换:
const localesMap = Object.fromEntries(
Object.entries(localeModules).map(([path, loadModule]) => {
const locale = path.match(/([A-Za-z-]+)\.json$/)?.[1]
return [locale, async () => (await loadModule()).default]
})
) as Record<AppLocale, () => Promise<Record<string, unknown>>>转换后 await localesMap[locale]() 拿到的才是真实 message 对象,再交给 i18n.global.setLocaleMessage(locale, messages) 注册。注意 import.meta.glob 默认懒加载,eager: true 会退化成全量导入。
四、加载与切换的职责分离(setupI18n)
加载语言包是异步 IO,切换当前语言是状态更新,两者应拆成两个函数:
loadLocaleMessages(locale):动态 import +setLocaleMessage,并用Set记录已加载语言,避免重复请求。setI18nLanguage(locale):更新i18n.global.locale、写localStorage、同步html lang与dayjs。
关键在于:app.use() 不会等待异步加载完成。若把异步加载塞进插件 install,首屏会先闪一下 fallback 文案。更稳的做法是显式异步初始化再挂载:
export async function setupI18n(app: App) {
app.use(i18n)
await changeLocale(resolveLocale())
}
async function bootstrap() {
const app = createApp(App)
app.use(ElementPlus)
await setupI18n(app) // 挂载前完成初始化
app.mount('#app')
}页面只需 await changeLocale(value),不必直接操作底层 setLocaleMessage。
五、Element Plus locale 整合与覆盖
Element Plus 内置文案(分页器、空状态、日期面板)不会自动读你的 messages,必须单独处理。最简单是 ElConfigProvider 传 locale 对象;若要和 vue-i18n 统一收口,可把官方 locale 与业务 messages 合并。
合并顺序决定覆盖优先级——后展开的对象覆盖前面的同名字段:
const mergedMessages = {
...elementPlusMessages, // 先用官方翻译
...messages // 业务文案在后,同名以自己为准
}两点提醒:Element Plus 官方 locale 文件名是 zh-cn / en,与业务代码 zh-CN / en-US 不总能直接对上,涉及多库时显式维护映射表({ 'zh-CN': 'zh-cn', 'en-US': 'en' })比 toLowerCase() 更稳;想改官方翻译时只在自己的 locale 文件覆写对应 key,不要改 node_modules。
六、dayjs 同步与 I18n Ally 开发辅助
Element Plus 的 DatePicker、Calendar 内部依赖 dayjs,只切 ElConfigProvider 还不够,要在 setI18nLanguage 里同步 dayjs.locale(),否则日期面板的月份、星期文本不跟随变化。
I18n Ally 是 VS Code 扩展(非运行时依赖),配置好能直接在 $t("key") 旁显示译文、检查缺失项:
{
"i18n-ally.localesPaths": ["src/locales/langs"],
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.displayLanguage": "zh-CN"
}它只影响编辑器显示,不参与线上运行;真正决定运行行为的仍是 vue-i18n、Element Plus locale 和你的代码。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 切换语言第一次有延迟,第二次很快 | 第一次真实异步加载,第二次命中缓存 | 正常,说明 Set 缓存生效 |
报 Missing locale file | import.meta.glob 路径与文件名不匹配 | 检查路径模板、扩展名、语言文件名 |
| 所有语言包仍被打进首屏 | 用了静态 import 或 eager: true | 用默认懒加载形式的 import.meta.glob |
| 首屏先闪 fallback 再变目标语言 | i18n 初始化发生在 mount 之后 | 先 await setupI18n(app) 再 mount |
| 日期面板没完全切换 | 忘了同步 dayjs.locale() | 在 setI18nLanguage 里同步 dayjs |
$t 旁看不到译文 | 没配 I18n Ally localesPaths | 检查 .vscode/settings.json 目录配置 |
| 覆写官方翻译不生效 | 直接改了 node_modules | 在自己的 locale 文件覆写同名 key |
延伸阅读
- 上一篇:语言切换与基础国际化接入 — 最小接入与 LocaleSelect
- 下一篇:NoticeMenu 组件设计与样式演进 — 进入通知中心模块
- 相关:自研组件库 — 组件库模块总览
- 相关:vue-i18n Lazy Loading · Vite Glob Import · I18n Ally