如何使用 Unicode 扩展自定义语言环境

为语言环境标识符添加日历系统、数字格式和时间显示偏好

介绍

en-US 这样的区域标识符告诉 JavaScript 使用哪种语言和地区进行格式化。然而,它并未指定使用哪种日历系统、显示哪种数字格式,或者是以 12 小时制还是 24 小时制显示时间。这些格式化偏好因用户选择而异,而不仅仅是由地理位置决定。

Unicode 扩展解决了这个问题。它们允许您将格式化偏好直接添加到区域标识符中。与其为每个格式化器使用单独的配置选项,不如将偏好一次性编码到区域字符串中。

本指南解释了 Unicode 扩展的工作原理、可用的扩展类型以及在国际化代码中何时使用它们。

什么是 Unicode 扩展

Unicode 扩展是您添加到区域标识符中的附加标签,用于指定格式化偏好。它们遵循 BCP 47 中定义的标准格式,BCP 47 也是定义区域标识符的规范。

扩展以 -u- 开头,后跟键值对。u 代表 Unicode。每个键由两个字母组成,值根据键的类型而变化。

const locale = "en-US-u-ca-gregory-hc-h12";

此区域标识符指定了使用美式英语、格里高利历和 12 小时制时间显示。

如何将扩展添加到区域字符串

扩展出现在区域标识符的末尾,在语言、脚本和地区组件之后。-u- 标记将核心标识符与扩展部分分隔开。

基本结构遵循以下模式:

language-region-u-key-value-key-value

每个键值对指定一个格式化偏好。您可以在单个区域字符串中包含多个键值对。

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));
// 输出: "٢٠ رمضان ١٤٤٦ هـ"

这会根据伊斯兰历格式化日期。同一公历日期在伊斯兰历系统中会显示为不同的年份、月份和日期。

佛历在泰国常用。它从公元前 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")));
// 输出: "15 มีนาคม 2568"

公历 2025 年在佛历中是 2568 年。

数字系统扩展

nu 键指定用于显示数字的数字系统。虽然大多数语言环境使用西方阿拉伯数字(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));
// 输出: "١٢٣٬٤٥٦"

阿拉伯-印度数字看起来与西方数字不同,但表示相同的数值。数字 123456 显示为 ١٢٣٬٤٥٦。

泰文数字是另一个例子:

const thaiLocale = new Intl.Locale("th-TH-u-nu-thai");
const formatter = new Intl.NumberFormat(thaiLocale);

console.log(formatter.format(123456));
// 输出: "๑๒๓,๔๕๖"

许多阿拉伯语言环境同时支持阿拉伯-印度数字和拉丁数字。用户可以根据个人偏好或上下文选择其首选系统。

小时周期扩展

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));
// 输出: "12:30 AM"

console.log(new Intl.DateTimeFormat(japan11Hour, { hour: "numeric", minute: "numeric" }).format(date));
// 输出: "0:30 AM"

console.log(new Intl.DateTimeFormat(europe23Hour, { hour: "numeric", minute: "numeric" }).format(date));
// 输出: "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)));
// 输出: ["Meyer", "Möller", "Mueller", "Müller"]

console.log(names.sort((a, b) => phonebook.compare(a, b)));
// 输出: ["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)));
// 输出: ["APPLE", "Apple", "apple", "banana"]

console.log(words.sort((a, b) => lowerFirst.compare(a, b)));
// 输出: ["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)));
// 输出: ["item1", "item10", "item2", "item20"]

console.log(items.sort((a, b) => numeric.compare(a, b)));
// 输出: ["item1", "item2", "item10", "item20"]

启用数字排序后,"item2" 会正确地排在 "item10" 之前,因为 2 小于 10。这为包含数字的字符串生成了预期的排序顺序。

数字排序对于排序文件名、版本号、街道地址以及任何包含嵌入数字的文本非常有用。

使用选项对象替代扩展字符串

与其在语言环境字符串中编码扩展,不如将它们作为选项传递给 Intl.Locale 构造函数。这种方法将基础语言环境与格式化偏好分开。

const locale = new Intl.Locale("ja-JP", {
  calendar: "japanese",
  numberingSystem: "jpan",
  hourCycle: "h11"
});

console.log(locale.toString());
// 输出: "ja-JP-u-ca-japanese-hc-h11-nu-jpan"

构造函数会自动将选项转换为扩展标签。这两种方法生成的语言环境对象是相同的。

使用选项对象的方法有几个好处。它通过使用完整的属性名称而不是两个字母的代码使代码更具可读性。此外,它还使从配置数据动态构建语言环境变得更容易。

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
});

您还可以直接将选项传递给格式化器构造函数:

const formatter = new Intl.DateTimeFormat("th-TH", {
  calendar: "buddhist",
  numberingSystem: "thai",
  year: "numeric",
  month: "long",
  day: "numeric"
});

这将语言环境特定的格式化选项与展示选项结合在一个构造函数调用中。

何时使用扩展与格式化器选项

扩展和格式化器选项的用途不同。了解何时使用每种方法有助于编写更简洁、更易维护的代码。

当格式化偏好是用户语言环境固有的时,在语言环境字符串中使用扩展。如果泰国用户总是希望看到佛教日历和泰国数字,则在其语言环境标识符中编码这些偏好。

const userLocale = "th-TH-u-ca-buddhist-nu-thai";

这使您可以将语言环境传递给任何格式化器,而无需重复这些偏好:

const dateFormatter = new Intl.DateTimeFormat(userLocale);
const numberFormatter = new Intl.NumberFormat(userLocale);

两个格式化器都会自动使用佛教日历和泰国数字。

当格式化偏好特定于某个用例时,使用格式化器选项。如果您希望在应用程序的某个部分显示伊斯兰日历,而在其他地方显示公历,则将日历选项传递给特定的格式化器。

const islamicFormatter = new Intl.DateTimeFormat("ar-SA", {
  calendar: "islamic"
});

const gregorianFormatter = new Intl.DateTimeFormat("ar-SA", {
  calendar: "gregory"
});

相同的语言环境标识符会根据日历选项生成不同的格式。

语言环境字符串中的扩展充当默认值。当指定时,格式化器选项会覆盖这些默认值。这使您可以将用户偏好用作基线,同时自定义特定的格式化器。

const locale = "en-US-u-hc-h23";
const formatter12Hour = new Intl.DateTimeFormat(locale, {
  hourCycle: "h12"
});

用户偏好是 24 小时制,但此特定格式化器覆盖了该偏好以显示 12 小时制。

从语言环境中读取扩展值

Intl.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"));
// 输出: { language: "th", region: "TH", calendar: "buddhist", numberingSystem: "thai", hourCycle: "default" }

collationcaseFirstnumeric 属性分别对应 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 表示启用了数字排序。

组合多个扩展

您可以在单个语言环境标识符中组合多个扩展。这使您可以一次性指定所有格式偏好。

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));
// 输出使用伊斯兰历法、阿拉伯-印度数字和 12 小时制时间

每个扩展键在语言环境字符串中只能出现一次。如果您多次指定相同的键,则最后一个值优先。

const locale = new Intl.Locale("en-US-u-hc-h23-hc-h12");
console.log(locale.hourCycle); // "h12"

在以编程方式构造语言环境时,请确保每个扩展键只出现一次,以避免歧义。

实际使用案例

Unicode 扩展解决了国际化应用中的实际问题。了解常见的使用案例有助于您有效地应用扩展。

用户偏好存储

将用户的格式偏好存储在一个单一的语言环境字符串中,而不是多个配置字段中:

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");

这种方法将格式偏好存储为一个字符串,同时仍然可以结构化地访问各个组件。

构建语言环境选择器

通过构建带有扩展的语言环境字符串,让用户通过 UI 选择格式偏好:

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);
// 输出: "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")));
// 输出: "15 Adar II 5785"

使用自定义规则排序

使用排序扩展实现特定语言环境的排序:

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"));
// 输出: "١٢٣٬٤٥٦"

console.log(formatTraditionalNumber(123456, "th-TH", "thai"));
// 输出: "๑๒๓,๔๕๖"

浏览器支持

Unicode 扩展在所有现代浏览器中均可使用。Chrome、Firefox、Safari 和 Edge 支持区域标识符中的扩展语法以及 Intl.Locale 对象上的相应属性。

特定扩展值的可用性取决于浏览器的实现。所有浏览器都支持常见值,例如日历的 gregory,数字系统的 latn,以及小时制的 h12h23。像传统的中国日历或少数民族语言数字系统等不常见的值可能无法在所有浏览器中使用。

在使用不常见的扩展值时,请在目标浏览器中测试您的区域标识符。使用 Intl.Locale 属性检查浏览器是否识别您的扩展值:

const locale = new Intl.Locale("zh-CN-u-ca-chinese");
console.log(locale.calendar);
// 如果浏览器支持中国日历: "chinese"
// 如果浏览器不支持: undefined

Node.js 从版本 12 开始支持 Unicode 扩展,从版本 18 开始全面支持所有属性。

总结

Unicode 扩展允许您将格式化偏好添加到区域标识符中。您无需单独配置每个格式化器,而是可以在区域字符串中一次性编码偏好。

关键概念:

  • 扩展以 -u- 开头,后跟键值对
  • ca 键指定日历系统
  • nu 键指定数字系统
  • hc 键指定小时制格式
  • co 键指定排序规则
  • kf 键指定首字母大小写排序
  • kn 键启用数字排序
  • 您可以使用扩展字符串或选项对象
  • 扩展作为默认值,格式化器选项可以覆盖
  • Intl.Locale 对象将扩展作为属性暴露

使用 Unicode 扩展存储用户偏好,尊重文化日历,显示传统数字,并实现区域特定的排序。它们为在 JavaScript 国际化代码中自定义格式化行为提供了标准方法。