如何使用 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 AM
  • h11 使用 0-11 小时,午夜为 0:00 AM
  • h23 使用 0-23 小时,午夜为 0:00
  • h24 使用 1-24 小时,午夜为 24:00

h12h11 表示 12 小时制,而 h23h24 表示 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。

大多数应用程序采用 h12h23h11 格式主要用于日本,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 属性分别对应 cokfkn 扩展键:

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,以及小时制的 h12h23。像传统中文历法或少数民族数字系统等不常见的值,可能并非所有浏览器都支持。

在使用不常见扩展值时,请在目标浏览器中测试你的 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 国际化代码中自定义格式化行为提供了标准方式。