如何使用 Unicode 扩展自定义本地化标识符
为本地化标识符添加日历系统、数字格式和时间显示偏好
简介
像 en-US 这样的本地化标识符告诉 JavaScript 应该使用哪种语言和地区进行格式化。但它并未指定应使用哪种日历系统、显示哪种数字格式,或采用 12 小时制还是 24 小时制显示时间。这些格式化偏好取决于用户选择,而不仅仅是地理位置。
Unicode 扩展解决了这个问题。它允许你将格式化偏好直接添加到本地化标识符中。无需为每个格式化器单独配置选项,而是将所有偏好一次性编码进本地化字符串。
本指南将解释 Unicode 扩展的工作原理、可用的扩展类型,以及在国际化代码中何时使用它们。
什么是 Unicode 扩展
Unicode 扩展是你在本地化标识符中添加的额外标签,用于指定格式化偏好。它们遵循 BCP 47 标准格式,这也是定义本地化标识符的规范。
扩展以 -u- 开头,后面跟着键值对。u 代表 Unicode。每个键为两个字母,值根据键的类型而变化。
const locale = "en-US-u-ca-gregory-hc-h12";
此本地化标识符指定了美式英语、格里高利历和 12 小时制时间显示。
如何向本地化字符串添加扩展
扩展出现在本地化标识符的末尾,位于语言、脚本和地区部分之后。-u- 标记用于分隔核心标识符和扩展部分。
基本结构遵循如下模式:
language-region-u-key-value-key-value
每个键值对都指定一个格式化偏好。你可以在一个 locale 字符串中包含多个键值对。
const japanese = new Intl.Locale("ja-JP-u-ca-japanese-nu-jpan");
console.log(japanese.calendar); // "japanese"
console.log(japanese.numberingSystem); // "jpan"
键值对的顺序无关紧要。"en-u-ca-gregory-nu-latn" 和 "en-u-nu-latn-ca-gregory" 都是有效且等价的。
日历扩展
ca 键用于指定日期格式化时使用哪种日历系统。不同文化采用不同的日历系统,有些用户因宗教或文化原因更喜欢非公历(格里历)日历。
常见的日历值包括:
gregory表示公历(格里历)buddhist表示佛历islamic表示伊斯兰历hebrew表示希伯来历chinese表示中国农历japanese表示日本皇历
const islamicLocale = new Intl.Locale("ar-SA-u-ca-islamic");
const date = new Date("2025-03-15");
const formatter = new Intl.DateTimeFormat(islamicLocale, {
year: "numeric",
month: "long",
day: "numeric"
});
console.log(formatter.format(date));
// Output: "٢٠ رمضان ١٤٤٦ هـ"
这会根据伊斯兰历格式化日期。同一个公历日期在伊斯兰历中会显示为不同的年份、月份和日期。
佛历在泰国广泛使用。其纪年从公元前 543 年释迦牟尼佛诞生算起,因此佛历年份比公历年份早 543 年。
const buddhistLocale = new Intl.Locale("th-TH-u-ca-buddhist");
const formatter = new Intl.DateTimeFormat(buddhistLocale, {
year: "numeric",
month: "long",
day: "numeric"
});
console.log(formatter.format(new Date("2025-03-15")));
// Output: "15 มีนาคม 2568"
公历 2025 年在佛历中为 2568 年。
数字系统扩展
nu 键用于指定数字显示时采用哪种数字系统。虽然大多数 locale 使用西方阿拉伯数字(0-9),但许多地区有自己的传统数字系统。
常见的数字系统值包括:
latn表示西方阿拉伯数字(0-9)arab表示阿拉伯-印度数字hanidec表示中文十进制数字deva表示天城文数字thai表示泰文数字
const arabicLocale = new Intl.Locale("ar-EG-u-nu-arab");
const number = 123456;
const formatter = new Intl.NumberFormat(arabicLocale);
console.log(formatter.format(number));
// Output: "١٢٣٬٤٥٦"
阿拉伯-印度数字与西方数字外观不同,但表示的数值相同。数字 123456 显示为 ١٢٣٬٤٥٦。
泰文数字是另一个例子:
const thaiLocale = new Intl.Locale("th-TH-u-nu-thai");
const formatter = new Intl.NumberFormat(thaiLocale);
console.log(formatter.format(123456));
// Output: "๑๒๓,๔๕๖"
许多阿拉伯地区同时支持阿拉伯-印度数字和拉丁数字。用户可以根据个人偏好或具体场景选择所用的数字系统。
小时制扩展
hc 键用于指定时间的显示方式。部分地区偏好带有 AM 和 PM 指示的 12 小时制,另一些地区则偏好 24 小时制。小时制还决定了午夜的显示方式。
可用的小时制有四种:
h12使用 1-12 小时,午夜为 12:00 AMh11使用 0-11 小时,午夜为 0:00 AMh23使用 0-23 小时,午夜为 0:00h24使用 1-24 小时,午夜为 24:00
h12 和 h11 表示 12 小时制,而 h23 与 h24 表示 24 小时制。区别在于小时范围是从 0 还是从 1 开始。
const us12Hour = new Intl.Locale("en-US-u-hc-h12");
const japan11Hour = new Intl.Locale("ja-JP-u-hc-h11");
const europe23Hour = new Intl.Locale("en-GB-u-hc-h23");
const date = new Date("2025-03-15T00:30:00");
console.log(new Intl.DateTimeFormat(us12Hour, { hour: "numeric", minute: "numeric" }).format(date));
// Output: "12:30 AM"
console.log(new Intl.DateTimeFormat(japan11Hour, { hour: "numeric", minute: "numeric" }).format(date));
// Output: "0:30 AM"
console.log(new Intl.DateTimeFormat(europe23Hour, { hour: "numeric", minute: "numeric" }).format(date));
// Output: "00:30"
h12 格式下,午夜显示为 12:30 AM,而 h11 格式下为 0:30 AM。h23 格式下则为 00:30,不带 AM 或 PM。
大多数应用程序采用 h12 或 h23。h11 格式主要用于日本,h24 在实际中很少使用。
排序扩展
co 键用于指定字符串排序规则。排序决定了文本排序时字符的顺序。不同语言和地区有不同的排序习惯。
常见的排序规则值包括:
standard:标准 Unicode 排序phonebk:电话簿排序(德语)pinyin:拼音排序(中文)stroke:笔画排序(中文)
德语电话簿排序与标准排序不同,对变音符号的处理方式也不同。电话簿排序会将 ä 展开为 ae,ö 展开为 oe,ü 展开为 ue,以便于排序。
const names = ["Müller", "Meyer", "Möller", "Mueller"];
const standard = new Intl.Collator("de-DE");
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");
console.log(names.sort((a, b) => standard.compare(a, b)));
// Output: ["Meyer", "Möller", "Mueller", "Müller"]
console.log(names.sort((a, b) => phonebook.compare(a, b)));
// Output: ["Meyer", "Möller", "Mueller", "Müller"]
中文排序提供多种排序系统。拼音排序按发音顺序排列,笔画排序则根据每个汉字的笔画数进行排序。
const pinyinCollator = new Intl.Collator("zh-CN-u-co-pinyin");
const strokeCollator = new Intl.Collator("zh-CN-u-co-stroke");
排序扩展仅影响 Intl.Collator API 以及与排序器一起使用的 Array.prototype.sort() 等方法。
首字母大小写扩展
kf 键用于确定在排序时大写字母还是小写字母优先。该偏好因语言和使用场景而异。
可用的三个值为:
upper:大写字母优先排序lower:小写字母优先排序false:使用本地默认的大小写排序方式
const words = ["apple", "Apple", "APPLE", "banana"];
const upperFirst = new Intl.Collator("en-US-u-kf-upper");
const lowerFirst = new Intl.Collator("en-US-u-kf-lower");
console.log(words.sort((a, b) => upperFirst.compare(a, b)));
// Output: ["APPLE", "Apple", "apple", "banana"]
console.log(words.sort((a, b) => lowerFirst.compare(a, b)));
// Output: ["apple", "Apple", "APPLE", "banana"]
首字母大小写排序在单词仅大小写不同的情况下影响排序。它决定了在比较基础字符后,次级排序顺序。
数值排序扩展
kn 键用于启用数值排序,按数值大小而非字典顺序对数字序列进行排序。如果未启用数值排序,"10" 会排在 "2" 前面,因为字符顺序中 "1" 在 "2" 之前。
数值排序可接受两个值:
true:启用数值排序false:禁用数值排序(默认)
const items = ["item1", "item10", "item2", "item20"];
const standard = new Intl.Collator("en-US");
const numeric = new Intl.Collator("en-US-u-kn-true");
console.log(items.sort((a, b) => standard.compare(a, b)));
// Output: ["item1", "item10", "item2", "item20"]
console.log(items.sort((a, b) => numeric.compare(a, b)));
// Output: ["item1", "item2", "item10", "item20"]
启用数字排序后,"item2" 会正确地排在 "item10" 之前,因为 2 小于 10。这为包含数字的字符串生成了符合预期的排序顺序。
数字排序对于文件名、版本号、街道地址以及任何包含嵌入数字的文本排序都非常有用。
使用 options 对象替代扩展字符串
与其在 locale 字符串中编码扩展,不如将它们作为 options 传递给 Intl.Locale 构造函数。这种方式将基础 locale 与格式化偏好分离开来。
const locale = new Intl.Locale("ja-JP", {
calendar: "japanese",
numberingSystem: "jpan",
hourCycle: "h11"
});
console.log(locale.toString());
// Output: "ja-JP-u-ca-japanese-hc-h11-nu-jpan"
构造函数会自动将 options 转换为扩展标签。这两种方式生成的 locale 对象是相同的。
使用 options 对象有多种优势。它通过使用完整的属性名而不是两个字母的代码,使代码更易读。同时,也更方便根据配置数据动态构建 locale。
const userPreferences = {
language: "ar",
region: "SA",
calendar: "islamic",
numberingSystem: "arab"
};
const locale = new Intl.Locale(`${userPreferences.language}-${userPreferences.region}`, {
calendar: userPreferences.calendar,
numberingSystem: userPreferences.numberingSystem
});
你也可以直接将 options 传递给格式化器的构造函数:
const formatter = new Intl.DateTimeFormat("th-TH", {
calendar: "buddhist",
numberingSystem: "thai",
year: "numeric",
month: "long",
day: "numeric"
});
这样可以在一次构造函数调用中结合 locale 特定的格式化选项和展示选项。
何时使用扩展与格式化器选项
扩展和格式化器选项用途不同。了解何时使用哪种方式有助于你编写更简洁、易维护的代码。
当格式化偏好是用户 locale 固有属性时,在 locale 字符串中使用扩展。例如,如果泰国用户始终希望看到佛历和泰文数字,应将这些偏好编码到他们的 locale 标识符中。
const userLocale = "th-TH-u-ca-buddhist-nu-thai";
这样你就可以将 locale 传递给任何格式化器,无需重复指定偏好:
const dateFormatter = new Intl.DateTimeFormat(userLocale);
const numberFormatter = new Intl.NumberFormat(userLocale);
两个格式化器都会自动使用佛历和泰文数字。
当格式化偏好仅适用于某个特定用例时,请使用 formatter 选项。例如,如果你希望在应用程序的某一部分显示伊斯兰历,而在其他地方显示公历,可以将 calendar 选项传递给特定的 formatter。
const islamicFormatter = new Intl.DateTimeFormat("ar-SA", {
calendar: "islamic"
});
const gregorianFormatter = new Intl.DateTimeFormat("ar-SA", {
calendar: "gregory"
});
相同的 locale 标识符,根据 calendar 选项的不同,会产生不同的格式化结果。
locale 字符串中的扩展作为默认值。当指定 formatter 选项时,这些选项会覆盖默认值。这样,你可以以用户偏好为基础,同时自定义特定的 formatter。
const locale = "en-US-u-hc-h23";
const formatter12Hour = new Intl.DateTimeFormat(locale, {
hourCycle: "h12"
});
用户偏好为 24 小时制,但此特定 formatter 覆盖了该偏好,显示为 12 小时制。
从 locale 读取扩展值
Intl.Locale 对象将扩展值作为属性暴露。你可以读取这些属性,以检查或验证 locale 的格式化偏好。
const locale = new Intl.Locale("ar-SA-u-ca-islamic-nu-arab-hc-h12");
console.log(locale.calendar); // "islamic"
console.log(locale.numberingSystem); // "arab"
console.log(locale.hourCycle); // "h12"
如果存在扩展,这些属性会返回扩展值;如果未指定扩展,则返回 undefined。
你可以利用这些属性构建配置界面或验证用户偏好:
function describeLocalePreferences(localeString) {
const locale = new Intl.Locale(localeString);
return {
language: locale.language,
region: locale.region,
calendar: locale.calendar || "default",
numberingSystem: locale.numberingSystem || "default",
hourCycle: locale.hourCycle || "default"
};
}
console.log(describeLocalePreferences("th-TH-u-ca-buddhist-nu-thai"));
// Output: { language: "th", region: "TH", calendar: "buddhist", numberingSystem: "thai", hourCycle: "default" }
collation、caseFirst 和 numeric 属性分别对应 co、kf 和 kn 扩展键:
const locale = new Intl.Locale("de-DE-u-co-phonebk-kf-upper-kn-true");
console.log(locale.collation); // "phonebk"
console.log(locale.caseFirst); // "upper"
console.log(locale.numeric); // true
请注意,numeric 属性返回布尔值而非字符串。true 表示已启用数字排序。
组合多个扩展
你可以在单个 locale 标识符中组合多个扩展,从而一次性指定所有格式化偏好。
const locale = new Intl.Locale("ar-SA-u-ca-islamic-nu-arab-hc-h12-co-standard");
const dateFormatter = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric"
});
const date = new Date("2025-03-15T14:30:00");
console.log(dateFormatter.format(date));
// Output uses Islamic calendar, Arabic-Indic numerals, and 12-hour time
每个扩展键在 locale 字符串中只能出现一次。如果多次指定同一键,则以最后一个值为准。
const locale = new Intl.Locale("en-US-u-hc-h23-hc-h12");
console.log(locale.hourCycle); // "h12"
在以编程方式构建 locale 时,请确保每个扩展键只出现一次,以避免歧义。
实际应用场景
Unicode 扩展解决了国际化应用中的实际问题。了解常见用例有助于你更有效地应用扩展。
用户偏好存储
将用户的格式化偏好存储在一个 locale 字符串中,而不是多个配置字段:
function saveUserPreferences(userId, localeString) {
const locale = new Intl.Locale(localeString);
return {
userId,
language: locale.language,
region: locale.region,
localeString: locale.toString(),
preferences: {
calendar: locale.calendar,
numberingSystem: locale.numberingSystem,
hourCycle: locale.hourCycle
}
};
}
const preferences = saveUserPreferences(123, "ar-SA-u-ca-islamic-nu-arab-hc-h12");
这种方法将格式化偏好作为单个字符串存储,同时仍然可以结构化访问各个组件。
构建 locale 选择器
通过 UI 让用户选择格式化偏好,并用扩展构建 locale 字符串:
function buildLocaleFromUserInput(language, region, preferences) {
const options = {};
if (preferences.calendar) {
options.calendar = preferences.calendar;
}
if (preferences.numberingSystem) {
options.numberingSystem = preferences.numberingSystem;
}
if (preferences.hourCycle) {
options.hourCycle = preferences.hourCycle;
}
const locale = new Intl.Locale(`${language}-${region}`, options);
return locale.toString();
}
const userLocale = buildLocaleFromUserInput("th", "TH", {
calendar: "buddhist",
numberingSystem: "thai",
hourCycle: "h23"
});
console.log(userLocale);
// Output: "th-TH-u-ca-buddhist-hc-h23-nu-thai"
支持宗教历法
为宗教社区服务的应用应支持其历法系统:
function createReligiousCalendarFormatter(religion, baseLocale) {
const calendars = {
jewish: "hebrew",
muslim: "islamic",
buddhist: "buddhist"
};
const calendar = calendars[religion];
if (!calendar) {
return new Intl.DateTimeFormat(baseLocale);
}
const locale = new Intl.Locale(baseLocale, { calendar });
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric"
});
}
const jewishFormatter = createReligiousCalendarFormatter("jewish", "en-US");
console.log(jewishFormatter.format(new Date("2025-03-15")));
// Output: "15 Adar II 5785"
自定义排序规则
使用排序扩展实现特定 locale 的排序规则:
function sortNames(names, locale, collationType) {
const localeWithCollation = new Intl.Locale(locale, {
collation: collationType
});
const collator = new Intl.Collator(localeWithCollation);
return names.sort((a, b) => collator.compare(a, b));
}
const germanNames = ["Müller", "Meyer", "Möller", "Mueller"];
const sorted = sortNames(germanNames, "de-DE", "phonebk");
console.log(sorted);
显示传统数字
以传统数字系统显示数字,确保文化适配:
function formatTraditionalNumber(number, locale, numberingSystem) {
const localeWithNumbering = new Intl.Locale(locale, {
numberingSystem
});
return new Intl.NumberFormat(localeWithNumbering).format(number);
}
console.log(formatTraditionalNumber(123456, "ar-EG", "arab"));
// Output: "١٢٣٬٤٥٦"
console.log(formatTraditionalNumber(123456, "th-TH", "thai"));
// Output: "๑๒๓,๔๕๖"
浏览器支持
Unicode 扩展在所有现代浏览器中均可用。Chrome、Firefox、Safari 和 Edge 支持 locale 标识符中的扩展语法,以及 Intl.Locale 对象上的相关属性。
特定扩展值的可用性取决于浏览器实现。所有浏览器都支持常见值,例如日历的 gregory,数字系统的 latn,以及小时制的 h12 或 h23。像传统中文历法或少数民族数字系统等不常见的值,可能并非所有浏览器都支持。
在使用不常见扩展值时,请在目标浏览器中测试你的 locale 标识符。可通过 Intl.Locale 属性检查浏览器是否识别你的扩展值:
const locale = new Intl.Locale("zh-CN-u-ca-chinese");
console.log(locale.calendar);
// If browser supports Chinese calendar: "chinese"
// If browser does not support it: undefined
Node.js 从 12 版本开始支持 Unicode 扩展,在 18 及更高版本中对所有属性提供了完整支持。
总结
Unicode 扩展允许你将格式化偏好添加到 locale 标识符中。你无需为每个格式化器单独配置,而是可以在 locale 字符串中一次性编码所有偏好设置。
关键概念:
- 扩展以
-u-开头,后跟键值对 ca键指定日历系统nu键指定数字系统hc键指定小时制格式co键指定排序规则kf键指定首字母大小写排序kn键启用数字排序- 可以使用扩展字符串或 options 对象
- 扩展作为默认值,格式化器选项可以覆盖
Intl.Locale对象将扩展作为属性暴露
使用 Unicode 扩展可以存储用户偏好、支持文化日历、显示传统数字以及实现特定语言环境的排序。它们为在 JavaScript 国际化代码中自定义格式化行为提供了标准方式。