BCP 47 语言标签与 locale 标准
核心问题:
zh-CN、zh-TW、zh-Hant-HK这些代码到底怎么读懂?如何在代码中正确使用 locale 标签?
真实场景
你的产品要支持繁体中文,但收到反馈:台湾用户的"繁体中文"和香港用户的"繁体中文"界面文字习惯不同("軟體" vs "軟件","資料" vs "數據")。这时候你需要用精确的 BCP 47 语言标签来区分。
什么是 BCP 47
BCP 47(Best Current Practice 47)是 IETF 发布的语言标签规范,合并了 RFC 5646 和 RFC 4647,定义了语言标签的语法和匹配规则。
BCP 47 语言标签的基本结构:
language[-script][-region][-variant][-extension][-privateuse]
例子:
zh-CN = 简体中文(中国大陆)
zh-TW = 繁体中文(台湾)
zh-Hant-HK = 繁体中文(香港)
en-US = 英文(美国)
en-GB = 英文(英国)
ar-SA = 阿拉伯语(沙特阿拉伯)
pt-BR = 葡萄牙语(巴西)
es-419 = 西班牙语(拉丁美洲)
标签结构详解
1. 语言子标签(Language)
ISO 639-1 两字母代码或 ISO 639-2/3 三字母代码。
| 代码 | 语言 |
|---|---|
zh | 中文 |
en | 英语 |
ar | 阿拉伯语 |
ja | 日语 |
ko | 韩语 |
fr | 法语 |
de | 德语 |
es | 西班牙语 |
pt | 葡萄牙语 |
ru | 俄语 |
hi | 印地语 |
th | 泰语 |
2. 文字子标签(Script)
ISO 15924 四字母代码(首字母大写)。只在必要时使用,避免冗余。
| 代码 | 文字系统 |
|---|---|
Hans | 简体汉字 |
Hant | 繁体汉字 |
Latn | 拉丁字母 |
Arab | 阿拉伯字母 |
Cyrl | 西里尔字母 |
Deva | 天城文(印地语) |
zh-Hans = 简体中文(不指定地区)
zh-Hant = 繁体中文(不指定地区)
zh-Hans-CN = 简体中文(中国大陆)—— 冗余,通常简写为 zh-CN
zh-Hant-TW = 繁体中文(台湾)—— 通常简写为 zh-TW
zh-Hant-HK = 繁体中文(香港)
3. 地区子标签(Region)
ISO 3166-1 两字母国家代码(大写)或 UN M.49 数字代码。
| 代码 | 地区 |
|---|---|
CN | 中国大陆 |
TW | 台湾 |
HK | 香港 |
US | 美国 |
GB | 英国 |
AU | 澳大利亚 |
CA | 加拿大 |
SA | 沙特阿拉伯 |
BR | 巴西 |
419 | 拉丁美洲(数字区域代码) |
4. 变体子标签(Variant)
用于区分同一语言/文字的特殊变体,较少使用。
sl-rozaj = 斯洛文尼亚语(Resia 方言)
ca-valencia = 加泰罗尼亚语(瓦伦西亚语)
5. 扩展子标签(Extension)
u- 扩展用于指定 Unicode locale 选项:
zh-CN-u-nu-hanidec = 中文,使用中文数字(一二三)
en-US-u-ca-buddhist = 英文,使用佛历
en-US-u-hc-h23 = 英文,24小时制
ar-u-nu-arab = 阿拉伯语,使用阿拉伯-印度数字(٠١٢٣)
完整语言标签示例
语言标签的大小写规范
BCP 47 规范虽然不区分大小写,但推荐约定:
语言子标签:小写 zh, en, ar
文字子标签:首字母大写 Hans, Hant, Latn
地区子标签:大写 CN, US, GB
// 推荐写法
const locale = 'zh-CN'; // ✅
const locale2 = 'ZH-cn'; // ❌ 不规范(虽然技术上有效)
在 JavaScript / Node.js 中使用
Intl.Locale API
// 创建 Locale 对象
const locale = new Intl.Locale('zh-Hant-HK');
console.log(locale.language); // 'zh'
console.log(locale.script); // 'Hant'
console.log(locale.region); // 'HK'
console.log(locale.baseName); // 'zh-Hant-HK'
// 带扩展的 locale
const localeExt = new Intl.Locale('en-US', {
calendar: 'gregory',
numberingSystem: 'latn',
hourCycle: 'h23'
});
console.log(localeExt.calendar); // 'gregory'
console.log(localeExt.numberingSystem); // 'latn'
console.log(localeExt.hourCycle); // 'h23'
语言标签验证
// 验证 locale 是否合法
function isValidLocale(locale) {
try {
new Intl.Locale(locale);
return true;
} catch {
return false;
}
}
isValidLocale('zh-CN'); // true
isValidLocale('zh_CN'); // false(下划线不合法!)
isValidLocale('chinese'); // false
⚠️ 常见错误:很多旧系统使用下划线(
zh_CN),这在 BCP 47 中是无效的。JavaScript 的IntlAPI 会报错或静默降级。永远使用连字符。
从 HTTP 请求获取用户语言
// Express.js 示例
const acceptLanguage = require('accept-language-parser');
app.use((req, res, next) => {
const languages = acceptLanguage.parse(req.headers['accept-language']);
// Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
// 解析结果:[{code:'zh', region:'CN', quality:1}, ...]
req.locale = languages[0]?.code + (languages[0]?.region ? `-${languages[0].region}` : '');
next();
});
CLDR(Unicode Common Locale Data Repository)
CLDR 是 Unicode 维护的最大的 locale 数据库,包含:
- 所有 locale 的日期格式、数字格式、货币符号
- 复数规则(每种语言的复数逻辑)
- 语言和地区的本地化名称
- 时区名称
- 排序规则
Node.js 与 ICU 数据
默认安装的 Node.js 可能只包含小型 ICU(small-icu),不支持所有 locale:
# 检查当前 ICU 数据集
node -e "console.log(process.versions.icu)"
# 输出:73.1
# 检查支持的 locale 数量
node -e "console.log(Intl.DateTimeFormat.supportedLocalesOf(['zh-CN', 'ar-SA']))"
# 安装完整 ICU 数据
npm install full-icu
node --icu-data-dir=node_modules/full-icu index.js
# 或通过环境变量
NODE_ICU_DATA=node_modules/full-icu node index.js
locale 标识符的冲突与对应关系
不同系统对 locale 的命名规范不同:
| 平台 | 格式 | 例子 |
|---|---|---|
| BCP 47 / IETF | language-REGION | zh-CN |
| Java / ICU | language_REGION | zh_CN |
| POSIX / Linux | language_REGION.encoding | zh_CN.UTF-8 |
| Windows LCID | 数字 | 2052 |
| macOS | BCP 47 | zh-Hans-CN |
// 在代码中统一转换为 BCP 47 格式
function normalizeToBCP47(locale) {
// Java/Python 风格 zh_CN → zh-CN
return locale.replace(/_/g, '-').replace(/\..*$/, '');
}
normalizeToBCP47('zh_CN.UTF-8'); // 'zh-CN'
normalizeToBCP47('en_US'); // 'en-US'
主流语言的标准标签速查
| 语言 | 推荐标签 | 备注 |
|---|---|---|
| 简体中文(中国) | zh-CN | 等同于 zh-Hans-CN |
| 繁体中文(台湾) | zh-TW | 等同于 zh-Hant-TW |
| 繁体中文(香港) | zh-HK 或 zh-Hant-HK | 词汇有差异 |
| 英语(美国) | en-US | |
| 英语(英国) | en-GB | 拼写、日期格式不同 |
| 日语 | ja 或 ja-JP | |
| 韩语 | ko 或 ko-KR | |
| 阿拉伯语(沙特) | ar-SA | RTL |
| 阿拉伯语(埃及) | ar-EG | 口语差异大 |
| 法语(法国) | fr-FR | |
| 法语(加拿大) | fr-CA | 词汇、日期格式不同 |
| 西班牙语(西班牙) | es-ES | |
| 西班牙语(拉丁美洲) | es-419 | 多国共用 |
| 葡萄牙语(巴西) | pt-BR | 与葡萄牙语差异显著 |
| 德语 | de-DE | |
| 俄语 | ru-RU | 西里尔字母 |
| 印地语 | hi-IN | |
| 泰语 | th-TH |
常见问题
Q:zh 和 zh-CN 有什么区别?
A:zh 不指定地区,系统会根据上下文决定显示简体还是繁体。建议总是指定地区以消除歧义,用 zh-CN(简体)或 zh-TW/zh-HK(繁体)。
Q:en 和 en-US 哪个更好?
A:对于大多数 Web 应用,建议使用 en-US 作为默认英语,日期格式(MM/DD/YYYY vs DD/MM/YYYY)会有区别。
Q:一个产品需要同时支持 zh-HK 和 zh-TW 吗?
A:取决于业务规模。初期可以用同一套繁体中文(zh-TW),后续根据用户反馈决定是否拆分。词汇差异主要集中在技术术语和部分口语习惯。
下一节:知道了 locale 标签的语法,下面看看 locale 到底会改变程序的哪些行为——排序、数字、日期、货币,每个领域都有陷阱。