如何为不完整的 locale 添加可能的子标签

使用 JavaScript 为部分 locale 标识符补全最可能的书写系统和地区

简介

在处理 locale 标识符时,有时你会收到不完整的信息。例如,用户可能只指定了语言代码,如 ja,而没有指明书写系统或地区。虽然这种部分标识符是有效的,但在比较 locale 或确定格式规范等操作时,缺乏必要的细节。

JavaScript 提供了一种方法,可以通过补全最可能缺失的部分来完善这些不完整的标识符。该过程利用语言数据推断该语言用户通常使用的书写系统和地区。

本指南将介绍什么是可能的子标签,JavaScript 如何确定它们,以及在你的应用中何时应使用此功能。

什么是可能的子标签

可能的子标签是与特定语言最常见搭配的书写系统和地区代码。这些关联来自 Unicode 联盟维护的真实语言使用数据。

例如,英语通常使用拉丁字母书写,并且最常在美国使用。如果你只有语言代码 en,则最可能的书写系统是 Latn,地区是 US,从而得到完整的标识符 en-Latn-US

这种可能性是基于使用人口和历史使用模式。该算法始终返回统计上最常见的组合。

为什么要添加可能的子标签

部分 locale 标识符适用于大多数格式化操作。Intl.DateTimeFormatIntl.NumberFormat API 可以接受 en 并应用合理的默认值。但在某些场景下,完整的标识符是必需的。

比较 locale 标识符

在比较两个 locale 标识符以判断它们是否指向相同的语言和地区时,部分标识符会带来歧义。例如,enen-US 是否表示相同的内容,还是因为一个指定了地区而另一个没有,所以不同?

添加 likely subtags 可以消除这种歧义。enen-US 都会最大化为 en-Latn-US,从而可以直接进行比较。

const locale1 = new Intl.Locale("en");
const locale2 = new Intl.Locale("en-US");

console.log(locale1.baseName === locale2.baseName);
// false - they look different

const maximized1 = locale1.maximize();
const maximized2 = locale2.maximize();

console.log(maximized1.baseName === maximized2.baseName);
// true - both are "en-Latn-US"

存储规范化形式

在数据库或配置文件中存储 locale 标识符时,使用完整形式可以确保一致性。每个法语 locale 都会变为 fr-Latn-FR,每个日语 locale 都会变为 ja-Jpan-JP,以此类推。

这种一致性使得按 locale 搜索、过滤和分组更加可靠。

判断特定书写系统的行为

有些语言可以使用多种书写系统,而书写系统会影响文本渲染、字体选择和排序。例如,中文可以用简体或繁体字符书写,塞尔维亚语可以用西里尔字母或拉丁字母。

添加 likely subtags 可以明确指定书写系统。如果用户只提供 zh 而未指定书写系统,最大化后会得到 zh-Hans-CN,这表示应使用简体中文字符。

算法的工作原理

Add Likely Subtags 算法利用一份语言使用信息数据库来确定缺失的组成部分。该数据库由 Unicode 联盟维护,是 Common Locale Data Repository 的一部分。

该算法会检查你提供的信息,并补全缺失部分:

  • 如果只提供语言,则会添加该语言最常用的书写系统和地区
  • 如果提供了语言和书写系统,则会添加该组合最常用的地区
  • 如果提供了语言和地区,则会添加该组合最常用的书写系统
  • 如果三者都已提供,则保持不变

这些决策基于全球语言使用的统计数据。

使用 maximize 方法

maximize() 方法可用于 Intl.Locale 对象。它会返回一个新的 locale 对象,并在基础名称上添加可能的子标签。

const locale = new Intl.Locale("ja");
const maximized = locale.maximize();

console.log(locale.baseName);
// "ja"

console.log(maximized.baseName);
// "ja-Jpan-JP"

该方法不会修改原始的 locale 对象,而是创建并返回一个新对象。

不同语言的示例

不同的语言根据其主要使用地区和所用书写系统,会有不同的可能子标签。

欧洲语言

法语最大化后为法国和拉丁字母:

const french = new Intl.Locale("fr");
const maximized = french.maximize();

console.log(maximized.baseName);
// "fr-Latn-FR"

德语最大化后为德国和拉丁字母:

const german = new Intl.Locale("de");
const maximized = german.maximize();

console.log(maximized.baseName);
// "de-Latn-DE"

非拉丁字母语言

日语最大化后为日本和日文书写系统:

const japanese = new Intl.Locale("ja");
const maximized = japanese.maximize();

console.log(maximized.baseName);
// "ja-Jpan-JP"

阿拉伯语最大化后为埃及和阿拉伯字母:

const arabic = new Intl.Locale("ar");
const maximized = arabic.maximize();

console.log(maximized.baseName);
// "ar-Arab-EG"

未指定书写系统的中文最大化后为简体字和中国:

const chinese = new Intl.Locale("zh");
const maximized = chinese.maximize();

console.log(maximized.baseName);
// "zh-Hans-CN"

带地区的部分标识符

当你只提供语言和地区但没有书写系统时,算法会自动补全书写系统:

const britishEnglish = new Intl.Locale("en-GB");
const maximized = britishEnglish.maximize();

console.log(maximized.baseName);
// "en-Latn-GB"

地区保持不变,只会补全缺失的书写系统。

带书写系统的部分标识符

当你只提供语言和书写系统但没有地区时,算法会补全该书写系统最常用的地区:

const traditionalChinese = new Intl.Locale("zh-Hant");
const maximized = traditionalChinese.maximize();

console.log(maximized.baseName);
// "zh-Hant-TW"

繁体中文字符主要在台湾使用,因此会将 TW 作为地区添加。

扩展标签会被保留

Unicode 扩展标签用于指定格式偏好,例如日历系统、数字系统和小时制。这些标签出现在语言环境标识符中的 -u- 之后。

maximize() 方法不会更改扩展标签。它只影响语言、脚本和地区部分。

const locale = new Intl.Locale("fr", {
  calendar: "gregory",
  numberingSystem: "latn",
  hourCycle: "h23"
});

console.log(locale.toString());
// "fr-u-ca-gregory-hc-h23-nu-latn"

const maximized = locale.maximize();

console.log(maximized.toString());
// "fr-Latn-FR-u-ca-gregory-hc-h23-nu-latn"

基础名称会从 fr 变为 fr-Latn-FR,但扩展标签保持不变。

何时使用 maximize

当你需要完整的语言环境标识符以实现一致性或比较时,请使用 maximize() 方法。

规范化用户输入

用户可能会以不同形式输入 locale。有些人会输入 en,有些人会输入 en-US,还有些人会输入 en-Latn-US。对所有输入进行 maximize,可以创建一致的格式:

function normalizeLocale(input) {
  try {
    const locale = new Intl.Locale(input);
    const maximized = locale.maximize();
    return maximized.baseName;
  } catch (error) {
    return null;
  }
}

console.log(normalizeLocale("en"));
// "en-Latn-US"

console.log(normalizeLocale("en-US"));
// "en-Latn-US"

console.log(normalizeLocale("en-Latn-US"));
// "en-Latn-US"

这三种输入都会生成相同的规范化形式。

构建语言环境回退链

当某个特定的 locale 不可用时,应用会回退到更通用的 locale。使用 maximize 可以正确构建这些回退链:

function buildFallbackChain(localeString) {
  const locale = new Intl.Locale(localeString);
  const maximized = locale.maximize();

  const chain = [maximized.toString()];

  if (maximized.script && maximized.region) {
    const withoutRegion = new Intl.Locale(
      `${maximized.language}-${maximized.script}`
    );
    chain.push(withoutRegion.toString());
  }

  if (maximized.region) {
    chain.push(maximized.language);
  }

  chain.push("en");

  return chain;
}

console.log(buildFallbackChain("zh-TW"));
// ["zh-Hant-TW", "zh-Hant", "zh", "en"]

这样可以实现从最具体到最通用 locale 的正确回退。

将用户偏好与可用 locale 匹配

当你有一组可用翻译,需要根据用户偏好找到最佳匹配时,同时对两者进行 maximize 可以实现准确比较:

function findBestMatch(userPreference, availableLocales) {
  const userMaximized = new Intl.Locale(userPreference).maximize();

  const matches = availableLocales.map(available => {
    const availableMaximized = new Intl.Locale(available).maximize();

    let score = 0;
    if (userMaximized.language === availableMaximized.language) score += 1;
    if (userMaximized.script === availableMaximized.script) score += 1;
    if (userMaximized.region === availableMaximized.region) score += 1;

    return { locale: available, score };
  });

  matches.sort((a, b) => b.score - a.score);

  return matches[0].locale;
}

const available = ["en-US", "en-GB", "fr-FR", "de-DE"];
console.log(findBestMatch("en", available));
// "en-US"

该函数会将用户偏好 en 扩展为 en-Latn-US,并找到最接近的匹配项。

何时不应使用 maximize

在将 locale 传递给格式化 API 之前,无需先进行 maximize。Intl.DateTimeFormatIntl.NumberFormat 以及其他格式化构造函数都能正确处理部分标识符。

const date = new Date("2025-03-15");

const partial = new Intl.DateTimeFormat("fr").format(date);
const maximized = new Intl.DateTimeFormat("fr-Latn-FR").format(date);

console.log(partial);
// "15/03/2025"

console.log(maximized);
// "15/03/2025"

两者输出完全相同。在这种情况下,增加额外的特异性不会改变格式化行为。

仅当你需要为自己的逻辑获取明确的信息时,才使用 maximize(),而不是在将 locale 传递给内置格式化器时使用。

浏览器支持

maximize() 方法在所有现代浏览器中均可用。Chrome、Firefox、Safari 和 Edge 都支持该方法,作为 Intl.Locale API 的一部分。

Node.js 从 12 版本开始支持 maximize(),在 14 及更高版本中提供完整支持。

总结

Likely subtags 通过为给定语言添加最常用的书写系统和地区,来补全部分 locale 标识符。Intl.Locale.maximize() 方法实现了 Unicode Add Likely Subtags 算法以完成此扩展。

要点:

  • likely subtags 基于真实的语言使用数据
  • maximize() 方法会补全缺失的书写系统和地区代码
  • 日历和数字系统的扩展标签保持不变
  • 使用 maximize 可用于规范化用户输入和比较 locale
  • 格式化 API 不需要 maximized 的 locale

maximize() 方法为需要明确书写系统和地区信息的应用逻辑,提供了标准化的完整 locale 标识符处理方式。