国际化(i18n)规范
TuraIDC 三个前端(frontend-admin-v3 / frontend-user-v3-www / frontend-user-v4-console)与后端 统一支持 四种语言:简体中文、繁體中文(台灣)、繁體中文(香港)、English。
前端负责界面文案,后端负责校验消息与业务异常消息,两侧共用同一套 locale 代码。本文是唯一规范来源; 改语言、加文案、接入未国际化的界面,都按这里执行。
自动化测试见 backend/tests/Unit/I18nLocaleConsistencyTest.php,它会在构建前拦住绝大多数回退。
一、语言集合
| 代码 | 语言 | 说明 |
|---|---|---|
zh_CN | 简体中文 | 词条真源。所有文案先写在这里 |
zh_TW | 繁體中文(台灣) | 由 zh_CN 经 OpenCC cn→twp 生成,不手工维护 |
zh_HK | 繁體中文(香港) | 由 zh_CN 经 OpenCC cn→hkp 生成,不手工维护 |
en_US | English | 人工翻译 |
繁体必须分台湾与香港
繁体「字」大多相同,差异在「词」,两者不能互相替代:
| 简体 | 台灣 zh_TW | 香港 zh_HK |
|---|---|---|
| 数据 | 資料 | 數據 |
| 网络 | 網路 | 網絡 |
| 软件 | 軟體 | 軟件 |
| 用户 | 使用者 | 用戶 |
| 文件 | 檔案 | 文件 |
| 默认 | 預設 | 默認 |
| 视频 | 影片 | 視頻 |
| 邮箱 | 電子郵件 | 電郵 |
用一份繁体词条覆盖两地,会有一半用词是错的。
代码沿用 TDesign / Element Plus 的原生命名,不自造 zh-Hans 之类的变体, 这样组件库语言包可以直接挂载。
默认语言是 zh_CN,fallbackLocale 同为 zh_CN:任何语言缺词条时回落到简体中文, 而不是显示空白或裸 key。
locale 从哪来
localStorage 里的 turaidc-locale / tdesign-starter-locale
→ 命中 supportedLocales 则采用
→ 否则 zh_CN管理端、用户控制台沿用 TDesign starter 的 tdesign-starter-locale;官网用自己的 turaidc-locale。两者互不干扰,因为它们是不同应用。
二、词条文件与 key 命名
frontend-<app>/src/locales/
├── index.ts # createI18n、supportedLocales、组件库语言映射
├── useLocale.ts # changeLocale / locale / getComponentsLocale
└── lang/
├── zh_CN.json # 真源
├── zh_TW.json # 生成
└── en_US.json # 人工翻译三份文件按点分路径嵌套,key 集合必须完全一致(测试会校验)。
key 命名:小驼峰点分,以功能模块开头,模块名从文件路径推断:
| 模块 | 例子 |
|---|---|
layout | layout.header.aboutUs、layout.nav.productCatalog |
product | product.detail.basePrice、product.specSelection |
content | content.notice.empty、content.help.searchPlaceholder |
seo / seoLanding | seoLanding.usServer.hero.title |
common | common.osPicker.version |
规则:
- 全小写字母开头、驼峰;不用中文、不用空格
- 同一语义全站同名(
viewAllProducts不要又写一个seeAll) - 语义化而非按位置命名(
product.detail.totalFee好过product.detail.label3)
三、翻译边界(最容易出事的一节)
必须翻译
- 模板文本节点:
<div>暂无数据</div> - 文案属性:
title/placeholder/alt/aria-label/label/description/content/empty-text/confirm/cancel - 脚本里纯粹给人看的字符串:
MessagePlugin.error('保存失败')、label: '状态'
绝对不能翻译
| 类型 | 真实例子 | 原因 |
|---|---|---|
| 匹配用关键词常量 | CPU_KEYWORDS = ['cpu','core','处理器','核'] | 拿去和商品配置项的 field/name 做字符串匹配。翻译后匹配全部失效,商品规格识别会崩 |
| 协议字段 / API 参数 | configoption、zapfxxg、regdate | 对端按字面解析 |
| 状态字面量 | 'pending'、'active' | 与后端枚举耦合 |
| 品牌名 / 域名 | TuraIDC、25y.cn | |
| 代码标识符 | 变量名、路由 path、组件名 | |
| 占位符 | {count}、{size}、%s | 结构不能动 |
这条是血的教训。 曾经有脚本把
CPU_KEYWORDS里的处理器、核一并替换成$t(), 结果官网的 CPU/内存分类判断失效。凡是不能用眼睛确认「这是给人看的」字符串,一律不碰。
判断方法
问自己一个问题:这个字符串被改动后,系统行为会不会变?
- 会变(匹配、协议、枚举)→ 不翻译
- 不会变(纯展示)→ 翻译
四、接入方式
模板里用 $t()
createI18n({ globalInjection: true }) 已开启,模板直接用,不需要 import:
<h1>{{ $t('product.list.breadcrumbRoot') }}</h1>
<input :placeholder="$t('product.detail.systemPlaceholder')" />脚本里用 t()
import { t } from '@/locales'
MessagePlugin.error(t('common.saveFailed'))文本节点含混合内容时
只替换命中片段,保留前后文:
<!-- 原文:产品中心 · 选择一级分组 -->
<h1>{{ $t('product.list.breadcrumbRoot') }}</h1>
<!-- 原文:共 {{ n }} 条数据 -->
<p>共 {{ $t('common.total') }} {{ n }} {{ $t('common.records') }}</p>组件库语言联动
- TDesign(admin / console):
tdesignLocaleMap挂到词条的componentsLocale, 由getComponentsLocale()取出传给useLocale()。 - Element Plus(官网):由
App.vue的<el-config-provider :locale="elementLocale">承载,useLocale()的getComponentsLocale提供该值。不要再用provideGlobalConfig传 locale, 它的类型不接受Ref,运行期也不响应。
五、新增文案的流程
- 在组件里写
zh_CN文案 - 同步补
en_US词条(key 与zh_CN对齐) zh_TW不手写,跑生成脚本- 跑测试
# 重新生成三端的 zh_TW(zh_CN 改动后执行)
node backend/scripts/i18n/build-zh-trad.mjs
# 护栏测试
cd backend && php artisan test --filter=I18nLocaleConsistency繁体转换(台湾 / 香港)
统一用 OpenCC,不要手写简繁对照表——手写表覆盖不全,实测 447 个字会漏, 产出「關於我们」这种简繁混排。
import OpenCC from 'opencc-js'
const twp = OpenCC.Converter({ from: 'cn', to: 'twp' }) // 台湾
const hkp = OpenCC.Converter({ from: 'cn', to: 'hkp' }) // 香港两个转换器都覆盖不到的词,在 backend/scripts/i18n/build-zh-trad.mjs 里补, 且必须区分替换时机:
HK_PRE_TERMS—— opencc 之前替换。适用于「内存→記憶體」这类 opencc 不会做词汇映射的词(hkp会把「内存」留成「內存」)。HK_POST_TERMS—— opencc 之后替换。港式字典会把部分字反向还原成简体 形态(户、税、温),必须在转换后再改回来,否则输出根本不是繁体。
六、测试护栏
backend/tests/Unit/I18nLocaleConsistencyTest.php 校验:
- 四份词条的 key 集合完全一致
zh_TW/zh_HK都不含简体残留- 各端
supportedLocales与实际存在的词条文件一致,且覆盖四种语言 - 组件库语言映射覆盖全部 locale(漏一项,分页/日期等组件文案就不跟随切换)
- 代码里
$t('key')/t('key')引用的 key 必须存在于词条,否则界面直接显示裸 key - 模板层不残留硬编码中文
TDesign 没有独立的香港语言包(只有
zh_TW),所以zh_HK的组件语言映射到zh_TW;Element Plus 自带zh-hk,直接挂载。
新增文案时忘了补 en_US,或忘了重跑生成脚本出 zh_TW/zh_HK,测试会直接失败。
七、后端业务消息
7.1 语言协商
SetLocale 中间件(注册在 api 栈最前)决定当前 locale,优先级:
X-Locale请求头 —— 前端 i18n 的当前语言,由三端 axios 拦截器带上,最准确;Accept-Language—— 浏览器语言偏好(识别zh-HK/zh-TW/en-GB等,含 q 值排序);- 默认
zh_CN。
前端切换语言后无需刷新,即刻生效。
7.2 消息表以中文原文为 key
backend/lang/{locale}/messages.php 的 key 就是抛异常时写的中文原文:
// zh_TW/messages.php
'商品分组不存在' => '商品分組不存在',
// en_US/messages.php
'商品分组不存在' => 'The product group does not exist',这样做的三个好处:
- 零调用点改动 —— 600 多处
throw new BusinessException('中文')一行都不用改, 翻译只发生在BusinessException构造时; - 天然回落 —— 新增业务消息没翻译时,查表 miss 直接返回中文原文,不会出现空消息;
- 可增量 —— 不必为每种语言同时维护一份完整词表。
查表必须带 group 前缀:
Lang::get('messages.' . $message)。 Laravel 会把不含点号的 key 当成 group 名(去查消息文本[null]),永远命中不了。
7.3 生成脚本
node backend/scripts/i18n/build-biz-messages.mjs扫描 backend/app 里所有 BusinessException('...'),重新生成 zh_CN / zh_TW / zh_HK。 en_US 是人工翻译的产物,脚本不覆盖它 —— 缺翻译会在测试里报出来。
7.4 日志不受语言影响
各调用层记录的是抛异常之前的中文原文,排障不受界面语言影响。
八、切换器放哪
| 端 | 位置 | 原因 |
|---|---|---|
| admin / v4-console | 设置面板(layouts/setting.vue) | TDesign starter 惯例,与主题、布局设置同处 |
| 官网 | 页脚(WebsiteLayout.vue) | 官网无设置面板;页脚不干扰首屏 |
语言名称用各语言自身文字显示(简体中文 / 繁體中文 / English),不做翻译—— 取自各词条顶层 lang 字段。