- 语言包全部走 HTTP 缓存 + CDN(Immutable + Brotli),不随业务 JS 一起打包
- 按 “语言维度” + “页面维度” 拆包 → 每个用户最多下载 n(语言) × m(页面) 份 JSON,且可并行预取
- 首屏只拉 1 份「当前语言 + 当前页面」最小 JSON(≈3–8 kB),其他资源懒加载或闲时预加载
- 本地 IndexedDB 持久化,二次进入 0 请求
- 构建阶段做 key 去重 + 压缩 + 雪碧合并,降低总流量 30–50%
二、目录与构建约定
public/lang/ // CDN 实际目录
├─ zh-CN/
│ ├─ common.json // 全局高频词条(按钮、提示)
│ ├─ page-home.json
│ ├─ page-about.json
│ └─ …
├─ en-US/
│ └─ …(同构)
构建脚本(webpack/vite 插件)自动完成:
- 提取各页面
this.$t('xxx')出现的 key → 生成单页 JSON - 所有页面都出现的 key → 抽进
common.json - 对 JSON 做
lz-string压缩 → 体积再降 15–25% - 生成
lang-manifest.json(哈希表),供运行期做「差量更新」
三、运行期架构
- 创建 i18n 实例(空 messages)
export const i18n = createI18n({
legacy: false,
locale: getPreferredLocale(), // 用户上次选择或浏览器语言
fallbackLocale: 'en-US',
messages: {}
})
- 通用加载器
const manifest = await fetch('/lang/lang-manifest.json').then(r => r.json())
const cache = await openDB('i18n', 1, { upgrade: db => db.createObjectStore('lang') })
export async function loadLocaleBundle(locale: string, page: string) {
const bundleId = `${locale}/${page}` // 如 zh-CN/page-home
// 1. indexedDB 命中则直接返回
const hit = await cache.get('lang', bundleId)
if (hit) return hit
// 2. 否则 CDN 拉取(带 etag + br)
const url = `/lang/${bundleId}.json`
const json = await fetch(url).then(r => r.json())
// 3. 写入缓存 & 注册到 i18n
await cache.put('lang', bundleId, json)
i18n.global.setLocaleMessage(locale, {
...i18n.global.getLocaleMessage(locale),
...json
})
return json
}
- 路由级自动加载
router.beforeEach(async (to, from, next) => {
const locale = i18n.global.locale.value
const page = to.name as string // 路由 name 与 JSON 文件名保持一致
await loadLocaleBundle(locale, page) // 耗时 20–60 ms(CDN)
next()
})
- 语言切换
async function changeLanguage(locale: string) {
// 先拉 common,再拉当前页面
await loadLocaleBundle(locale, 'common')
await loadLocaleBundle(locale, router.currentRoute.value.name as string)
i18n.global.locale.value = locale
localStorage.setItem('locale', locale)
}
四、性能细节
- 首屏
- 只多 1 个 JSON 请求(common + 当前页面已合并)→ 加载体积 <10 kB
- 与业务 JS 并行,不阻塞渲染
- 服务器开启
br+cache-control: max-age=31536000, immutable
- 后续页面
- 路由跳转前 20 ms 触发加载,用户无感知;失败时回退到 key 本身(兜底)
- 二次访问
- IndexedDB 命中 → 0 网络请求,直接解析 JSON(<5 ms)
- 预加载策略(可选)
- 利用
requestIdleCallback在浏览器空闲时把「同语言其余页面」拉回来 → 后续跳转 0 延迟 - PWA 场景可配
workbox做后台同步
五、常见坑 & 解决
- 打包时动态
import()会把所有语言目录打进去 → 改用 纯运行时 fetch,不用 import() - 页面 key 重复率高 → 构建阶段做 字典去重 + 缩写,可把 400 kB 原始文本压到 120 kB
- 服务端渲染(SSR) → 在
serverPrefetch里同步拉取对应 JSON,直出带翻译的 HTML,防止水合闪烁 - 右侧-to-Left 语言 → 在
changeLanguage里同步设置document.dir='rtl',并动态加载 rtl 样式