Skip to content

国际化(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_USEnglish人工翻译

繁体必须分台湾与香港 ​

繁体「字」大多相同,差异在「词」,两者不能互相替代:

简体台灣 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 命名:小驼峰点分,以功能模块开头,模块名从文件路径推断:

模块例子
layoutlayout.header.aboutUs、layout.nav.productCatalog
productproduct.detail.basePrice、product.specSelection
contentcontent.notice.empty、content.help.searchPlaceholder
seo / seoLandingseoLanding.usServer.hero.title
commoncommon.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:

vue
<h1>{{ $t('product.list.breadcrumbRoot') }}</h1>
<input :placeholder="$t('product.detail.systemPlaceholder')" />

脚本里用 t() ​

js
import { t } from '@/locales'
MessagePlugin.error(t('common.saveFailed'))

文本节点含混合内容时 ​

只替换命中片段,保留前后文:

vue
<!-- 原文:产品中心 · 选择一级分组 -->
<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,运行期也不响应。

五、新增文案的流程 ​

  1. 在组件里写 zh_CN 文案
  2. 同步补 en_US 词条(key 与 zh_CN 对齐)
  3. zh_TW 不手写,跑生成脚本
  4. 跑测试
bash
# 重新生成三端的 zh_TW(zh_CN 改动后执行)
node backend/scripts/i18n/build-zh-trad.mjs

# 护栏测试
cd backend && php artisan test --filter=I18nLocaleConsistency

繁体转换(台湾 / 香港) ​

统一用 OpenCC,不要手写简繁对照表——手写表覆盖不全,实测 447 个字会漏, 产出「關於我们」这种简繁混排。

js
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 校验:

  1. 四份词条的 key 集合完全一致
  2. zh_TW / zh_HK 都不含简体残留
  3. 各端 supportedLocales 与实际存在的词条文件一致,且覆盖四种语言
  4. 组件库语言映射覆盖全部 locale(漏一项,分页/日期等组件文案就不跟随切换)
  5. 代码里 $t('key') / t('key') 引用的 key 必须存在于词条,否则界面直接显示裸 key
  6. 模板层不残留硬编码中文

TDesign 没有独立的香港语言包(只有 zh_TW),所以 zh_HK 的组件语言映射到 zh_TW;Element Plus 自带 zh-hk,直接挂载。

新增文案时忘了补 en_US,或忘了重跑生成脚本出 zh_TW/zh_HK,测试会直接失败。


七、后端业务消息 ​

7.1 语言协商 ​

SetLocale 中间件(注册在 api 栈最前)决定当前 locale,优先级:

  1. X-Locale 请求头 —— 前端 i18n 的当前语言,由三端 axios 拦截器带上,最准确;
  2. Accept-Language —— 浏览器语言偏好(识别 zh-HK / zh-TW / en-GB 等,含 q 值排序);
  3. 默认 zh_CN。

前端切换语言后无需刷新,即刻生效。

7.2 消息表以中文原文为 key ​

backend/lang/{locale}/messages.php 的 key 就是抛异常时写的中文原文:

php
// 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 生成脚本 ​

bash
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 字段。

基于 AGPL-3.0-or-later 发布