{T}

国际化工程化与构建优化

概述

基础接入跑通之后,国际化的重点就从"能不能切换"转向"语言资源怎么维护得住"。本文讲工程化四件事:把语言包从 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 里最省事,但文案变多后文件会迅速膨胀、职责不清。工程化目标是:

text
src/
  locales/
    langs/
      zh-CN.json
      en-US.json
  modules/
    i18n.ts        # 初始化、加载、切换统一封装

页面组件只负责"触发切换",不关心底层如何加载 JSON、如何同步组件库 locale。典型的后台框架都采用这种"模块对外暴露 changeLocale(),页面只调入口"的模式。

二、构建插件与语言文件命名

@intlify/unplugin-vue-i18n 负责预编译 locale 资源、优化打包体积,当前主流包名即此(旧称 vite-plugin-vue-i18n 属历史命名):

ts
// 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-CNen-US),而不是 cn.jsonenglish.json——这样和浏览器语言、组件库 locale、翻译平台约定一致,也便于扩展 zh-TWen-GB 等地区变体。

三、import.meta.glob 懒加载与 localesMap

Vite 下 import.meta.glob() 默认返回"路径 → 动态导入函数"的懒加载映射,只有真正切到某语言时才加载对应文件,比一次性打进首屏更省。

ts
const localeModules = import.meta.glob('../locales/langs/*.json')

但业务层更希望用语言代码当 key。用 Object.entries + map + Object.fromEntries 做一次转换:

ts
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 langdayjs

关键在于:app.use() 不会等待异步加载完成。若把异步加载塞进插件 install,首屏会先闪一下 fallback 文案。更稳的做法是显式异步初始化再挂载:

ts
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,必须单独处理。最简单是 ElConfigProviderlocale 对象;若要和 vue-i18n 统一收口,可把官方 locale 与业务 messages 合并。

合并顺序决定覆盖优先级——后展开的对象覆盖前面的同名字段:

ts
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 的 DatePickerCalendar 内部依赖 dayjs,只切 ElConfigProvider 还不够,要在 setI18nLanguage 里同步 dayjs.locale(),否则日期面板的月份、星期文本不跟随变化。

I18n Ally 是 VS Code 扩展(非运行时依赖),配置好能直接在 $t("key") 旁显示译文、检查缺失项:

json
{
  "i18n-ally.localesPaths": ["src/locales/langs"],
  "i18n-ally.sourceLanguage": "en-US",
  "i18n-ally.displayLanguage": "zh-CN"
}

它只影响编辑器显示,不参与线上运行;真正决定运行行为的仍是 vue-i18n、Element Plus locale 和你的代码。


常见问题

问题原因解决方案
切换语言第一次有延迟,第二次很快第一次真实异步加载,第二次命中缓存正常,说明 Set 缓存生效
Missing locale fileimport.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

延伸阅读