CLDR locale 继承:en-GB 的父级是 en-001,不是 en
CLDR 的 locale 不是一组扁平的字符串 key。tag 之间有继承链,而这条链不是简单地把最后一段子标签删掉。cldr-core 里的 parentLocales.json 只有 5.8 KB,却包含 200 多条显式父级映射,覆盖了常见语言的实际行为。搞错这一层,症状通常很具体:词条加错位置、日期格式改了不生效、繁体永远看不到新增内容。
数据里的实际映射
以下取自 cldr-json 的 cldr-core/supplemental/parentLocales.json(CLDR 48):
en-GB → en-001。en-AU、en-NZ、en-IN、en-ZA、en-SG、en-HK、en-IE等 80 多个地区同样指向en-001。en-150 → en-001;en-AT、en-DE、en-NL、en-SE、en-CH、en-PL等欧洲英语指向en-150。es-MX、es-AR、es-CO、es-US等 →es-419(拉美西语);es-ES才是 →es。zh-Hant → und;zh-Hant-MO → zh-Hant-HK。繁体不继承zh,因为zh默认是 Hans。pt-AO、pt-MZ、pt-CV等 →pt-PT;而pt默认是巴西。nb、nn→no;no-NO → no。ht(海地克里奥尔语)→fr-HT。az-Arab、sr-Latn、pa-Arab、uz-Cyrl、yue-Hans、zh-Hant等 →und,脚本不匹配,直接回 root。规则写在_localeRules.parentLocale.nonlikelyScript = root。
parentLocales.collations 区块还有一套独立父级:yue → zh-Hant,yue-CN、yue-Hans → zh-Hans,sr-Cyrl-ME → sr-ME。排序用的父级和普通数据不是同一条链。
aliases.json(137 KB)管的是另一件事:弃用 ID。iw→he、in→id、ji→yi、mo→ro、sh→sr-Latn,时区 Asia/Calcutta→Asia/Kolkata。老数据、老 URL 里的 tag 要先过 alias,再谈继承。
截断式 fallback 跳过了中间层
自己写 tag.split('-').slice(0, -1) 会得到这样一条路径:
en-GB直接跳到en,跳过en-001es-MX直接跳到es,跳过es-419en-AT直接跳到en,跳过en-150和en-001
结果就是「我在 en-001 里补的词条,英国用户看不到,但我本地测试(用 en)是好的」。测试环境和线上环境的 locale 不同,问题不会在本地暴露。
按 CLDR 规则算继承链
规则很短:先查显式父级表,命中就用表里的值;没命中才截断最后一段子标签。下面是可直接跑的版本。
// npm i cldr-core
const core = require('cldr-core/supplemental/parentLocales.json');
// 普通数据的父级表在 parentLocales.parentLocale;
// 排序的父级表在 parentLocales.collations,与 parentLocale 平级,不在它下面。
const parentLocale = core.supplemental.parentLocales.parentLocale;
const collations = core.supplemental.parentLocales.collations;
function makeChain(table) {
return function chain(tag) {
const out = [tag];
const seen = new Set([tag]);
let cur = tag;
for (;;) {
let next = table[cur];
if (!next) {
const parts = cur.split('-');
if (parts.length === 1) {
out.push('root');
break;
}
next = parts.slice(0, -1).join('-');
}
out.push(next);
if (next === 'root' || next === 'und' || seen.has(next)) break;
seen.add(next);
cur = next;
}
return out;
};
}
const chain = makeChain(parentLocale);
const collationChain = makeChain(collations);
console.log(chain('en-GB'));
// ['en-GB', 'en-001', 'en', 'root']
console.log(chain('en-AT'));
// ['en-AT', 'en-150', 'en-001', 'en', 'root']
console.log(chain('es-MX'));
// ['es-MX', 'es-419', 'es', 'root']
console.log(chain('zh-Hant-MO'));
// ['zh-Hant-MO', 'zh-Hant-HK', 'zh-Hant', 'und']
console.log(chain('pt-AO'));
// ['pt-AO', 'pt-PT', 'pt', 'root']
console.log(chain('ht'));
// ['ht', 'fr-HT', 'fr', 'root']
console.log(collationChain('yue'));
// ['yue', 'zh-Hant', 'und']
seen 用来防环,und 和 root 都作为终点。zh-Hant 命中 → und 后停止,不会继续落到 zh。
三个直接后果
词条放在哪一层,决定影响面。 放 en-001 覆盖所有英式/非美英语地区(80+);放 en 覆盖全部英语,包括美式;放 en-GB 只有英国。往 en-001 补一条日期格式,美国不受影响,但澳洲、印度、南非会同时变。这类改动要看一眼链,而不是看 key 名像不像。
繁体用户看不到 zh 的补充。 zh 默认 Hans,zh-Hant 的父级是 und。要覆盖繁体,必须写 zh-Hant,或者落到 zh-HK / zh-TW,不能只写 zh 然后期待被继承。
排序和搜索规则跟展示数据不是一条链。 yue 的排序父级是 zh-Hant,不是 zh-Hans。给 zh-Hans 调排序不会传导到粤语,两套 map 要分别查。
验证方法
这些映射不需要背。请求进来时把 resolved 链打出来,或者直接断言行为差异:
const fmt = (loc) =>
new Intl.DateTimeFormat(loc, { timeZone: 'UTC' })
.format(new Date(Date.UTC(2026, 9, 20)));
console.log(fmt('en-GB')); // 20/10/2026
console.log(fmt('en')); // 10/20/2026
console.log(new Intl.DateTimeFormat('iw').resolvedOptions().locale); // he,alias 已生效
真正的差异点用 golden file 固定住:把各 locale 的 resolved 链和几个关键格式化结果写成快照,CLDR 升级时 diff 会直接告诉你哪条链变了。想确认某一条,读 parentLocales.json 里的键,5.8 KB 的文件比任何二手描述都准。
资料
- parentLocales.json:https://github.com/unicode-cldr/cldr-json/blob/main/cldr-json/cldr-core/supplemental/parentLocales.json
- aliases.json:https://github.com/unicode-cldr/cldr-json/blob/main/cldr-json/cldr-core/supplemental/aliases.json
- 规范 UTS #35 的 Locale Inheritance / Lookup:https://www.unicode.org/reports/tr35/
- CLDR 数据仓库:https://github.com/unicode-org/cldr
- cldr-json 总入口:https://github.com/unicode-cldr/cldr-json