Files
lucky_shop/docs/i18n-guidelines.md
ZF sun 55a5a68517 refactor: 统一多语言key的组件命名格式为短横线分隔
根据i18n规范,将所有组件相关的多语言key从驼峰式改为短横线分隔格式,同步更新文档中的示例说明。
2026-06-30 10:28:36 +08:00

13 KiB
Raw Blame History

国际化(i18n)技术说明文档

适用范围:shop-projects/frontend(uni-app Vue2 项目) 核心模块:common/js/lang.js 维护要求:新增/修改翻译 key 后,必须运行 npm run i18n:check


一、整体架构

项目采用 本地语言包 + 远端覆写 的混合方案:

  • 本地语言包:位于 lang/zh-cn/ 与 lang/en-us/,随包体发布,作为默认文案。
  • 远端覆写:启动/切换语言时从后端拉取扁平化覆写 map,按租户、平台、语种三维隔离,用于运营 Hotfix 或 SaaS 多租户文案定制。
  • 解析入口:全局注入的 $lang(key, ...args) 与 $langConfig 对象,核心实现在 common/js/lang.js。

运行时的语言上下文由以下四层按顺序深度合并而成:

  1. lang/<locale>/common.js(全局公共文案)
  2. SHARED_LANG_PACKS 中注册的组件/公共 mixin 语言包
  3. 当前页面对应的页面语言包
  4. 远端覆写 map(若配置为 remote / hybrid)

合并使用深度合并(deepMergeLang),嵌套 key 不会被整体覆盖。


二、语言包结构与命名规范

2.1 文件位置

lang/
├── zh-cn/
│   ├── common.js              # 全局公共文案
│   ├── components/
│   │   ├── payment.js         # 组件级文案
│   │   └── ns-goods-sku.js
│   ├── components-diy/
│   │   ├── diy-article.js
│   │   └── diy-listmenu.js
│   ├── goods/
│   │   ├── detail.js
│   │   └── list.js
│   ├── order/
│   │   └── list.js
│   └── ...
└── en-us/
    └── ...(与 zh-cn 镜像结构一致)

每个语言包文件统一导出:

export const lang = {
    // 文案 key / 嵌套对象
};

2.2 页面语言包路径映射

页面路由到语言包路径的映射由 processRoutePath 处理,主要规则:

页面路径 语言包路径 说明
pages/index/index index/index 去掉 pages 前缀
pages_tool/member/index member/index pages_tool 子目录直接提升
pages_goods/goods/detail goods/detail pages_goods 映射为 goods
pages_order/order/list order/list pages_order 映射为 order
pages_member/member/index member/index pages_member 映射为 member
pages_promotion/point/order_list point/order_list point/fenxiao/merch 子模块提升
pages_promotion/fenxiao/order fenxiao/order 同上

因此,新增页面语言包时,必须按上述映射关系创建文件,否则 $lang() 找不到对应 key。

2.3 命名空间规范

组件语言包

组件语言包统一挂在 components 或 components-diy 命名空间下,组件名使用 camelCase(短横线/下划线会被转换)。

例如 components-diy/diy-article.js:

export const lang = {
    viewMore: '查看更多',
    noArticles: '暂无文章',
};

运行时会被归一化为:

{
    componentsDiy: {
        diyArticle: {
            viewMore: '查看更多',
            noArticles: '暂无文章',
        }
    }
}

调用方式:

{{ $lang('components-diy.diy-article.viewMore') }}

注意:组件名 diy-article 归一化后为 diyArticle,不是 diy-article,也不是 diyArticle 以外的任何形式。若语言包本身已按 { componentsDiy: { diyArticle: {...} } } 嵌套,则直接透传,不再二次包装。

页面语言包

页面语言包优先使用扁平结构,key 直接放在 export const lang = { ... } 顶层。例如 order/list.js:

export const lang = {
    title: '我的订单',
    status: {
        all: '全部',
        waitpay: '待付款',
    },
};

页面内可直接调用:

{{ $lang('title') }}
{{ $lang('status.waitpay') }}

也可以按完整命名空间调用(经过 normalizePageLangPack 后会生成):

{{ $lang('order.list.title') }}
{{ $lang('order.list.status.waitpay') }}

不建议把页面语言包写成 { order: { list: { title: '...' } } } 这种完整嵌套结构,除非旧代码已经这样写。扁平结构更利于页面内直接调用,同时归一化逻辑会自动补全 order.list.* 命名空间。

公共 JS 文案

公共 JS(如 common/js/auth.js)使用的文案应放入 lang/<locale>/common.js,按 common.* 命名空间调用。

// common.js
export const lang = {
    auth: {
        miniProgramConfigError: '小程序配置错误',
    },
};

调用:

this.$lang('common.auth.miniProgramConfigError');

三、SHARED_LANG_PACKS 注册机制

所有共享组件/公共 mixin 的语言包必须显式注册在 common/js/lang.js 的 SHARED_LANG_PACKS 数组中,否则组件在任意页面调用 $lang('components.xxx') 时会出现 raw key。

const SHARED_LANG_PACKS = [
    'components/payment',
    'components-diy/diy-article',
    'member/public',
    // ...
];

新增共享组件语言包时,必须同时完成以下三步:

  1. 创建 lang/zh-cn/<path>.js 与 lang/en-us/<path>.js;
  2. 将 <path> 加入 SHARED_LANG_PACKS;
  3. 运行 npm run i18n:check 确保两包 key 一致。

四、$lang() 使用说明

4.1 模板中使用

<!-- 无参数 -->
<text>{{ $lang('order.list.title') }}</text>

<!-- 位置参数 -->
<text>{{ $lang('goods.soldCount', 100) }}</text>
<!-- 文案:已售 {0} 件 -->

<!-- 命名参数 -->
<text>{{ $lang('goods.pointSuffix', { point: 500 }) }}</text>
<!-- 文案:{point}积分 -->

4.2 JS 中使用

// Vue 实例内
const title = this.$lang('order.list.title');

// 带默认值兜底(推荐用于动态 key)
const name = this.$lang('components.payment.' + type, item.name);

4.3 参数替换规则

  • 位置参数:{0}、{1}、{2}...
  • 命名参数:{name}、{count}、{point}...
  • 全局替换:同一个占位符出现多次,全部替换。
  • 命名参数从第二个参数起传入对象;其余情况视为位置参数。
// 位置参数
$lang('hello', 'Tom'); // 你好,{0} → 你好,Tom

// 命名参数
$lang('hello', { name: 'Tom' }); // 你好,{name} → 你好,Tom

4.4 默认值兜底

从 P2 改造后,动态 key 调用建议传入第二参数作为默认值:

{{ $lang('components.payment.' + item.type, item.name) }}

行为:

  • key 存在 → 返回翻译结果;
  • key 不存在且传入默认值 → 返回默认值;
  • key 不存在且无默认值 → 生产环境返回空串,开发环境返回 raw key 并输出 warning。

五、解析流程与关键函数

5.1 lang(field) 主流程

  1. 取当前页面 route,通过 processRoutePath 得到 langPath;
  2. 构造合并缓存 key:${locale}__${langPath}__${_overrideVersion};
  3. 按顺序合并 common.js → SHARED_LANG_PACKS → 页面语言包 → 远端覆写;
  4. 按 . 分割 field,在合并后的对象中逐层查找;
  5. 未命中且 field 段数 ≥ 3 时,尝试 tryResolveQualifiedPageField(按前缀自动加载其他页面包);
  6. 仍未命中时,尝试 tryResolveLoadedPageField(在已加载的页面包中查找,要求唯一命中);
  7. 缓存字段解析结果。

5.2 normalizeSharedLangPack(path, langData)

将组件语言包归一化到 components.<ComponentCamelCase> 或 components-diy.<ComponentCamelCase> 命名空间。

5.3 normalizePageLangPack(path, langData)

将页面语言包展开为三层访问方式:

  • 扁平 key(如 $lang('title'))
  • 文件名命名空间(如 $lang('list.title'))
  • 完整路径命名空间(如 $lang('order.list.title'))

5.4 tryResolveQualifiedPageField(locale, field)

用于处理跨页面包引用。例如 components/ns-goods-sku.vue 中调用 $lang('goods.detail.stock') 时,即使当前页面不是 goods/detail,也会自动加载 lang/<locale>/goods/detail.js 并解析。


六、远端覆写机制

6.1 配置

在 common/js/config-external.js 的 langConfig 中配置:

{
    source: 'remote', // 'local' | 'remote' | 'hybrid'
    defaultLang: 'zh-cn',
    langList: ['zh-cn', 'en-us'],
    remote: {
        overridesUrl: '/api/lang/overrides',
        requestTimeout: 5000,
        bootTimeout: 3000,
        storagePrefix: 'lang_override_',
        refreshOnUniacidChange: true,
    }
}

6.2 行为

  • local:完全走本地语言包,不拉远端。
  • remote / hybrid:启动时先从本地 storage 读取上一份覆写作为首屏兜底,再异步拉取远端;成功后合并到内存并通知全局刷新。
  • 拉取失败时不抛错,静默使用本地兜底,并触发 lang:fallback 事件。
  • 切换语言/租户时自动清理对应缓存并重新拉取。

扩展方案:若希望远端语言包完全替代本地语言包(即删除 lang/ 目录后仍能正常运行),参见 docs/i18n-remote-only-solution.md。

6.3 覆写数据格式

后端返回的 overrides 是一个扁平 map,key 为点分路径,value 为字符串:

{
    "code": 0,
    "data": {
        "version": 123,
        "global_version": 456,
        "overrides": {
            "common.submit": "提交",
            "order.list.title": "我的订单(定制)"
        }
    }
}

七、开发规范与强制校验

7.1 提交前必须运行

npm run i18n:check

该脚本会检查:

  1. zh-cn / en-us 文件结构是否一致;
  2. 同文件下中英文 key 是否完全一致;
  3. 粗略扫描源码中的硬编码中文(参考,含注释误报)。

7.2 新增/修改 key 流程

  1. 在 lang/zh-cn/<path>.js 中新增 key;
  2. 同步在 lang/en-us/<path>.js 中新增同名 key(即使英文暂未翻译,也先用中文占位,避免 raw key);
  3. 如果是组件/mixin 语言包,将 <path> 加入 common/js/lang.js 的 SHARED_LANG_PACKS;
  4. 源码中使用 $lang('xxx') 替换硬编码中文;
  5. 运行 npm run i18n:check;
  6. 对于动态 key,运行 node scripts/verify-p2-keys.js。

7.3 禁止事项

  • 禁止用 Object.assign() 浅合并语言包(会导致嵌套 key 丢失);
  • 禁止在生产环境返回 raw key(已兜底为空串,开发环境返回 raw key 便于调试);
  • 禁止翻译参数只替换第一次出现(必须全局替换);
  • 禁止组件语言包不注册 SHARED_LANG_PACKS 就直接使用。

八、常见问题与排查

8.1 英文环境下显示 raw key

可能原因:

  • 组件语言包未加入 SHARED_LANG_PACKS;
  • 页面语言包路径与 processRoutePath 映射不匹配;
  • 调用 key 与语言包实际结构不一致(如 diyListMenu vs diyListmenu);
  • 语言包内嵌套结构错误(如 notice/list.js 内又包了一层 notice: {})。

排查方法:

  1. 检查 common/js/lang.js 是否注册;
  2. 检查语言包文件路径是否正确;
  3. 在 lang() 中加入断点,查看 mergedLang 中是否存在目标 key;
  4. 开发环境下看 console 是否有 [i18n] missing key "xxx" warning。

8.2 嵌套 key 被覆盖

确保 deepMergeLang 正常工作,不要使用 Object.assign 合并语言包。若发现 common.currencySymbol 等子 key 丢失,检查是否有地方做了浅合并。

8.3 动态 key 无法静态校验

对于 $lang('status.' + statusItem.status) 这类动态 key,i18n-check.js 无法识别。需要:

  1. 枚举所有可能取值;
  2. 在对应语言包中补齐 key;
  3. 为调用处增加默认值兜底;
  4. 运行 node scripts/verify-p2-keys.js 验证。

九、相关脚本与文档

文件 作用
common/js/lang.js i18n 核心解析器
scripts/i18n-check.js zh-cn / en-us key 一致性检查 + 硬编码中文扫描
scripts/verify-p2-keys.js 动态 key 枚举验证
docs/i18n-audit-report.md 全量审计报告
docs/i18n-p2-solution-plan.md P2 动态 key 处理方案
docs/i18n-remote-only-solution.md 远端完全替代本地语言包方案
docs/i18n-guidelines.md 本说明文档

十、附录:示例

10.1 新增一个组件语言包

新增 components/my-badge.vue,需要文案 "新品" / "New"。

  1. 创建 lang/zh-cn/components/my-badge.js:
export const lang = {
    newTag: '新品',
};
  1. 创建 lang/en-us/components/my-badge.js:
export const lang = {
    newTag: 'New',
};
  1. 在 common/js/lang.js 的 SHARED_LANG_PACKS 中追加 'components/my-badge';
  2. 组件中使用:
<text>{{ $lang('components.myBadge.newTag') }}</text>
  1. 运行 npm run i18n:check。

10.2 新增一个页面语言包

新增页面 pages_tool/coupon/list.vue,需要文案 "优惠券列表"。

  1. 创建 lang/zh-cn/coupon/list.js:
export const lang = {
    title: '优惠券列表',
};
  1. 创建 lang/en-us/coupon/list.js:
export const lang = {
    title: 'Coupon List',
};
  1. 页面中使用:
<text>{{ $lang('title') }}</text>
<!-- 或 -->
<text>{{ $lang('coupon.list.title') }}</text>
  1. 运行 npm run i18n:check。