背景与问题界定

随着前端应用向全球化发展,国际化(i18n)已成为大多数商业项目的标配能力。Vue3 生态中最成熟的 i18n 方案是 vue-i18n,它在 v9 版本中全面拥抱 Vue3 Composition API 和 TypeScript。但在实际项目中,简单的 $t('key') 使用方式远远不够。几个典型的工程挑战:一是语言包膨胀——当应用包含 20 个国际化模块时,全量加载所有语言的 JSON 文件可能导致首屏体积增加数百 KB;二是运行时切换的响应式性能——语言切换时大量 DOM 节点的文本更新可能造成界面卡顿;三是动态内容的翻译——后端返回的模板字符串(如"您有 {count} 条未读消息")如何在前端正确解析不同语言下的占位符序差异;四是类型安全——翻译 key 的拼写错误只在运行时暴露,缺乏编译期检查。

目标拆解与工程约束

  • 语言包按需加载:只有当前语言的翻译资源加载到内存中,切换语言时异步下载新语言包,下载期间降级显示 key 本身或 fallback 语言。
  • 模板中的翻译类型安全:翻译 key 必须通过 TypeScript 类型约束,IDE 输入翻译 key 时有自动补全,拼写错误在编译期报错。
  • 运行时切换不卡顿:语言切换后,页面中所有使用 $t 的文本应在 16ms 内完成更新,不出现明显的布局偏移或闪烁。
  • 复数规则与格式化:支持不同语言的字数规则(如中文"1 条消息"/“2 条消息"不分单复数,而英文要求单复数区分的语法)。

方案设计

vue-i18n 集成与类型安全

使用 vue-i18n v9 的 Composition API 模式,配合 TypeScript Schema 生成翻译 key 的类型签名:

// locales/schema.ts — 定义翻译资源类型
export type MessageSchema = {
  common: {
    confirm: string
    cancel: string
    empty: string
  }
  user: {
    login: string
    logout: string
    welcome: (name: string) => string
    unread: (count: number) => string
  }
}

// i18n.ts — 类型化的 createI18n
import { createI18n } from 'vue-i18n'
import type { MessageSchema } from './locales/schema'

const i18n = createI18n<[MessageSchema], 'zh-CN' | 'en-US' | 'ja-JP'>({
  locale: 'zh-CN',
  fallbackLocale: 'en-US',
  messages: {
    'zh-CN': {
      common: { confirm: '确认', cancel: '取消', empty: '暂无数据' },
      user: { login: '登录', logout: '退出', welcome: (n) => `您好,${n}!`, unread: (c) => `您有 ${c} 条未读消息` }
    }
  }
})

这种模式下,组件中的 t('user.login') 会得到完整的类型检查和自动补全。

动态语言包加载

为避免全量加载所有语言包,我们利用 Webpack/Vite 的动态 import 实现按需加载:

const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'] as const
type Locale = typeof SUPPORTED_LOCALES[number]

const localeLoaders: Record<Locale, () => Promise<any>> = {
  'zh-CN': () => import('./locales/zh-CN.json'),
  'en-US': () => import('./locales/en-US.json'),
  'ja-JP': () => import('./locales/ja-JP.json'),
}

async function switchLocale(locale: Locale) {
  const messages = await localeLoaders[locale]()
  i18n.global.setLocaleMessage(locale, messages.default ?? messages)
  i18n.global.locale.value = locale
  document.documentElement.lang = locale
}

Vite 的 import() 在构建时会自动将这些 JSON 文件拆分为独立的 chunk,用户首次访问时只下载当前语言的 chunk(一般在 2~8KB 之间,gzip 后)。

运行时切换性能

vue-i18n v9 使用 Vue3 的响应式系统追踪翻译依赖。语言切换时,所有 $tt() 的依赖会触发 re-render。为了优化体验,我们采用两阶段策略:

  1. 立即降级显示 fallback 语言:切换操作发出后,i18n fallbackLocale 已预加载,用户界面不会出现空白。
  2. 新语言包加载完成后无感切换:通过 <Transition> 包裹整个页面内容,利用 mode="out-in" 实现平滑过渡。

对于大型表格或列表中的静态翻译(如表头、按钮文本),使用 <i18n-t> 组件替代函数调用,该组件在语言切换时只更新文本节点而非重建元素。

服务端语言探测

在 SSR 场景中,我们通过请求的 Accept-Language header 和 cookie 中的语言偏好决定初始语言:

function detectLocale(req: IncomingMessage): Locale {
  const cookieLocale = parseCookies(req)['locale']
  if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as Locale)) {
    return cookieLocale as Locale
  }
  const preferred = acceptLanguage(req.headers['accept-language'] || 'zh-CN')
  return preferred.length > 0 ? preferred[0] as Locale : 'zh-CN'
}

实施路径与关键决策

  • 翻译 key 的命名规范:采用 模块.子模块.具体描述 的三段式命名,如 user.profile.editTitle,避免扁平化的 key 造成大量冲突。
  • JSON 与 TypeScript 混合使用:纯字符串翻译存放 JSON(便于非开发人员编辑),包含动态占位符的翻译用 TypeScript 函数定义。
  • i18n 的提取与检查工具:在 CI 中添加 i18n 覆盖率检查,确保每个页面模块的翻译 key 在三种语言中均有实现。
  • 翻译文件的版本管理:翻译内容变更与代码提交解耦,通过 Lokalise / Crowdin 等翻译管理平台维护,自动 PR 同步到代码仓库。

验证指标与可持续迭代

首屏的语言相关 JS 体积控制在 10KB(gzip)以内。语言切换延迟不超过 100ms(含网络加载时间)。翻译 key 的编译期覆盖率达 100%——暴露给用户的文本必须通过类型化的 i18n key 引用,不允许硬编码字符串。后续迭代方向:基于 ICU MessageFormat 的强类型复数规则,AI 辅助翻译的自动 lint,以及翻译变更的灰度发布能力。

工程落地思考

国际化看似是一个技术问题,其实是一个产品问题。多数 i18n 方案的技术难点不在于翻译 key 的管理或动态加载,而在于让所有参与角色(开发者、设计师、产品经理、翻译者)在使用同一套语言资源时保持同步。技术方案应该尽可能降低这个同步成本:为开发者提供类型安全,为翻译者提供友好的编辑格式,为产品经理提供覆盖率报告。真正好的国际化方案,是在任何语言版本中用户都意识不到"这是一个翻译后的产品”——文本的布局、语序和格式都应该与目标语言的自然表达习惯一致。