如何将区域标识符标准化为标准形式

将区域标识符转换为规范格式,确保正确的大小写和组件顺序

介绍

区域标识符可以用多种不同的方式书写,同时指代相同的语言和地区。用户可能会写成 EN-usen-USen-us,这三种形式都表示美式英语。在存储、比较或显示区域标识符时,这些变体会导致不一致。

规范化将区域标识符转换为标准的规范形式。此过程调整组件的大小写,将扩展关键字按字母顺序排列,并生成一致的表示形式,您可以在整个应用程序中依赖它。

JavaScript 提供了内置方法,可以自动规范化区域标识符。本指南解释了规范化的含义、如何在代码中应用它,以及规范化标识符何时能改进您的国际化逻辑。

区域标识符规范化的含义

规范化根据 BCP 47 标准和 Unicode 规范将区域标识符转换为其规范形式。规范形式对大小写、排序和结构有特定规则。

规范化的区域标识符遵循以下约定:

  • 语言代码为小写
  • 脚本代码首字母大写,其余小写
  • 地区代码为大写
  • 变体代码为小写
  • 扩展关键字按字母顺序排列
  • 扩展属性按字母顺序排列

这些规则为每个区域创建了一个统一的标准表示形式。无论用户如何书写区域标识符,规范化形式始终保持一致。

理解规范化规则

区域标识符的每个组件在规范形式中都有特定的大小写约定。

语言大小写

语言代码始终使用小写字母:

en(正确)
EN(不正确,但会规范化为 en)
eN(不正确,但会规范化为 en)

这适用于两字母和三字母的语言代码。

脚本大小写

脚本代码使用首字母大写,其余三个字母小写的形式:

Hans(正确)
hans(错误,但会规范化为 Hans)
HANS(错误,但会规范化为 Hans)

常见的脚本代码包括 Latn 表示拉丁字母,Cyrl 表示西里尔字母,Hans 表示简体汉字,Hant 表示繁体汉字。

区域大小写

区域代码始终使用大写字母:

US(正确)
us(错误,但会规范化为 US)
Us(错误,但会规范化为 US)

这适用于大多数语言环境标识符中使用的两位字母国家代码。

扩展顺序

Unicode 扩展标签包含指定格式偏好的关键字。在规范形式中,这些关键字按其键的字母顺序排列:

en-US-u-ca-gregory-nu-latn(正确)
en-US-u-nu-latn-ca-gregory(错误,但会规范化为第一种形式)

日历键 ca 在字母顺序上位于数字系统键 nu 之前,因此在规范化形式中 ca-gregory 先出现。

使用 Intl.getCanonicalLocales 进行规范化

Intl.getCanonicalLocales() 方法规范化语言环境标识符,并以规范形式返回。这是 JavaScript 中用于规范化的主要方法。

const normalized = Intl.getCanonicalLocales("EN-us");
console.log(normalized);
// ["en-US"]

该方法接受任何大小写形式的语言环境标识符,并返回正确大小写的规范形式。

规范化语言代码

该方法将语言代码转换为小写:

const result = Intl.getCanonicalLocales("FR-fr");
console.log(result);
// ["fr-FR"]

语言代码 FR 在输出中变为 fr

规范化脚本代码

该方法将脚本代码转换为首字母大写形式:

const result = Intl.getCanonicalLocales("zh-HANS-cn");
console.log(result);
// ["zh-Hans-CN"]

脚本代码 HANS 变为 Hans,区域代码 cn 变为 CN

规范化区域代码

该方法将区域代码转换为大写:

const result = Intl.getCanonicalLocales("en-gb");
console.log(result);
// ["en-GB"]

区域代码 gb 在输出中变为 GB

规范化扩展关键字

该方法按字母顺序对扩展关键字进行排序:

const result = Intl.getCanonicalLocales("en-US-u-nu-latn-hc-h12-ca-gregory");
console.log(result);
// ["en-US-u-ca-gregory-hc-h12-nu-latn"]

关键字从 nu-latn-hc-h12-ca-gregory 重新排序为 ca-gregory-hc-h12-nu-latn,因为按字母顺序 cahc 之前,hcnu 之前。

规范化多个语言环境标识符

Intl.getCanonicalLocales() 方法接受一个语言环境标识符数组,并对它们全部进行规范化:

const locales = ["EN-us", "fr-FR", "ZH-hans-cn"];
const normalized = Intl.getCanonicalLocales(locales);
console.log(normalized);
// ["en-US", "fr-FR", "zh-Hans-CN"]

数组中的每个语言环境都会被转换为其规范形式。

删除重复项

该方法在规范化后删除重复的语言环境标识符。如果多个输入值规范化为相同的规范形式,结果中只包含一个副本:

const locales = ["en-US", "EN-us", "en-us"];
const normalized = Intl.getCanonicalLocales(locales);
console.log(normalized);
// ["en-US"]

这三个输入都表示相同的语言环境,因此输出中只包含一个规范化的标识符。

在处理用户输入或合并来自多个来源的语言环境列表时,这种去重功能非常有用。

处理无效标识符

如果数组中的任何语言环境标识符无效,该方法会抛出一个 RangeError

try {
  Intl.getCanonicalLocales(["en-US", "invalid", "fr-FR"]);
} catch (error) {
  console.error(error.message);
  // "invalid is not a structurally valid language tag"
}

在规范化用户提供的列表时,可以为每个语言环境单独验证或捕获错误,以识别哪些具体标识符无效。

使用 Intl.Locale 进行规范化

Intl.Locale 构造函数在创建语言环境对象时也会规范化语言环境标识符。您可以通过 toString() 方法访问规范化形式。

const locale = new Intl.Locale("EN-us");
console.log(locale.toString());
// "en-US"

构造函数接受任何有效的大小写形式,并生成一个规范化的语言环境对象。

访问标准化组件

locale 对象的每个属性都会返回该组件的标准化形式:

const locale = new Intl.Locale("ZH-hans-CN");

console.log(locale.language);
// "zh"

console.log(locale.script);
// "Hans"

console.log(locale.region);
// "CN"

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

languagescriptregion 属性都使用了规范形式的正确大小写。

使用选项进行标准化

当您使用选项创建 locale 对象时,构造函数会同时标准化基础标识符和选项:

const locale = new Intl.Locale("EN-us", {
  calendar: "gregory",
  numberingSystem: "latn",
  hourCycle: "h12"
});

console.log(locale.toString());
// "en-US-u-ca-gregory-hc-h12-nu-latn"

扩展关键字在输出中按字母顺序排列,即使选项对象未指定任何特定顺序。

为什么标准化很重要

标准化为您的应用程序提供了一致性。当您存储、显示或比较 locale 标识符时,使用规范形式可以防止细微的错误并提高可靠性。

一致的存储

在数据库、配置文件或本地存储中存储 locale 标识符时,标准化形式可以防止重复:

const userPreferences = new Set();

function saveUserLocale(identifier) {
  const normalized = Intl.getCanonicalLocales(identifier)[0];
  userPreferences.add(normalized);
}

saveUserLocale("en-US");
saveUserLocale("EN-us");
saveUserLocale("en-us");

console.log(userPreferences);
// Set { "en-US" }

如果没有标准化,集合中会包含同一 locale 的三个条目。通过标准化,它正确地只包含一个。

可靠的比较

比较 locale 标识符需要标准化。仅大小写不同的两个标识符表示相同的 locale:

function isSameLocale(locale1, locale2) {
  const normalized1 = Intl.getCanonicalLocales(locale1)[0];
  const normalized2 = Intl.getCanonicalLocales(locale2)[0];
  return normalized1 === normalized2;
}

console.log(isSameLocale("en-US", "EN-us"));
// true

console.log(isSameLocale("en-US", "en-GB"));
// false

直接对未标准化的标识符进行字符串比较会产生错误的结果。

一致的显示

在向用户显示语言环境标识符或调试输出时,规范化形式提供了一致的格式:

function displayLocale(identifier) {
  try {
    const normalized = Intl.getCanonicalLocales(identifier)[0];
    return `当前语言环境:${normalized}`;
  } catch (error) {
    return "无效的语言环境标识符";
  }
}

console.log(displayLocale("EN-us"));
// "当前语言环境:en-US"

console.log(displayLocale("zh-HANS-cn"));
// "当前语言环境:zh-Hans-CN"

无论输入格式如何,用户都能看到格式正确的语言环境标识符。

实际应用

在实际应用中,规范化解决了处理语言环境标识符时的常见问题。

规范化用户输入

当用户在表单或设置中输入语言环境标识符时,应在存储之前对输入进行规范化:

function processLocaleInput(input) {
  try {
    const normalized = Intl.getCanonicalLocales(input)[0];
    return {
      success: true,
      locale: normalized
    };
  } catch (error) {
    return {
      success: false,
      error: "请输入有效的语言环境标识符"
    };
  }
}

const result = processLocaleInput("fr-ca");
console.log(result);
// { success: true, locale: "fr-CA" }

这确保了数据库或配置中的格式一致性。

构建语言环境查找表

在为翻译或特定语言环境的数据创建查找表时,使用规范化的键:

const translations = new Map();

function addTranslation(locale, key, value) {
  const normalized = Intl.getCanonicalLocales(locale)[0];

  if (!translations.has(normalized)) {
    translations.set(normalized, {});
  }

  translations.get(normalized)[key] = value;
}

addTranslation("en-us", "hello", "Hello");
addTranslation("EN-US", "goodbye", "Goodbye");

console.log(translations.get("en-US"));
// { hello: "Hello", goodbye: "Goodbye" }

两次调用 addTranslation 使用相同的规范化键,因此翻译存储在同一个对象中。

合并语言环境列表

当从多个来源合并语言环境标识符时,应对其进行规范化并去重:

function mergeLocales(...sources) {
  const allLocales = sources.flat();
  const normalized = Intl.getCanonicalLocales(allLocales);
  return normalized;
}

const userLocales = ["en-us", "fr-FR"];
const appLocales = ["EN-US", "de-de"];
const systemLocales = ["en-US", "es-mx"];

const merged = mergeLocales(userLocales, appLocales, systemLocales);
console.log(merged);
// ["en-US", "fr-FR", "de-DE", "es-MX"]

此方法去除了重复项,并在所有来源中规范了大小写。

创建语言环境选择界面

在构建下拉菜单或选择界面时,规范化语言环境标识符以便显示:

function buildLocaleOptions(locales) {
  const normalized = Intl.getCanonicalLocales(locales);

  return normalized.map(locale => {
    const localeObj = new Intl.Locale(locale);
    const displayNames = new Intl.DisplayNames([locale], {
      type: "language"
    });

    return {
      value: locale,
      label: displayNames.of(localeObj.language)
    };
  });
}

const options = buildLocaleOptions(["EN-us", "fr-FR", "DE-de"]);
console.log(options);
// [
//   { value: "en-US", label: "English" },
//   { value: "fr-FR", label: "French" },
//   { value: "de-DE", label: "German" }
// ]

规范化的值为表单提交提供了一致的标识符。

验证配置文件

在从配置文件加载语言环境标识符时,在初始化期间对其进行规范化:

function loadLocaleConfig(config) {
  const validatedConfig = {
    defaultLocale: null,
    supportedLocales: []
  };

  try {
    validatedConfig.defaultLocale = Intl.getCanonicalLocales(
      config.defaultLocale
    )[0];
  } catch (error) {
    console.error("无效的默认语言环境:", config.defaultLocale);
    validatedConfig.defaultLocale = "en-US";
  }

  config.supportedLocales.forEach(locale => {
    try {
      const normalized = Intl.getCanonicalLocales(locale)[0];
      validatedConfig.supportedLocales.push(normalized);
    } catch (error) {
      console.warn("跳过无效的语言环境:", locale);
    }
  });

  return validatedConfig;
}

const config = {
  defaultLocale: "en-us",
  supportedLocales: ["EN-us", "fr-FR", "invalid", "de-DE"]
};

const validated = loadLocaleConfig(config);
console.log(validated);
// {
//   defaultLocale: "en-US",
//   supportedLocales: ["en-US", "fr-FR", "de-DE"]
// }

这可以及早捕获配置错误,并确保您的应用程序使用有效的规范化标识符。

规范化与语言环境匹配

规范化对于语言环境匹配算法非常重要。在为用户偏好找到最佳语言环境匹配时,比较规范化形式:

function findBestMatch(userPreference, availableLocales) {
  const normalizedPreference = Intl.getCanonicalLocales(userPreference)[0];
  const normalizedAvailable = Intl.getCanonicalLocales(availableLocales);

  if (normalizedAvailable.includes(normalizedPreference)) {
    return normalizedPreference;
  }

  const preferenceLocale = new Intl.Locale(normalizedPreference);

  const languageMatch = normalizedAvailable.find(available => {
    const availableLocale = new Intl.Locale(available);
    return availableLocale.language === preferenceLocale.language;
  });

  if (languageMatch) {
    return languageMatch;
  }

  return normalizedAvailable[0];
}

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

规范化确保匹配逻辑无论输入大小写如何都能正确工作。

规范化不会改变含义

规范化仅影响区域标识符的表示形式。它不会改变标识符所代表的语言、脚本或地区。

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

console.log(locale1.language === locale2.language);
// true

console.log(locale1.region === locale2.region);
// true

console.log(locale1.toString() === locale2.toString());
// true

这两个标识符都指代美式英语。规范化只是确保它们以相同的方式书写。

这与 maximize()minimize() 等操作不同,后者会添加或移除组件,并可能改变标识符的具体性。

浏览器支持

Intl.getCanonicalLocales() 方法在所有现代浏览器中都可以使用。Chrome、Firefox、Safari 和 Edge 提供全面支持。

Node.js 从版本 9 开始支持 Intl.getCanonicalLocales(),在版本 10 及更高版本中提供完整支持。

Intl.Locale 构造函数及其规范化行为在支持 Intl.Locale API 的所有浏览器中都可用。这包括现代版本的 Chrome、Firefox、Safari 和 Edge。

总结

规范化通过应用标准大小写规则和对扩展关键字进行排序,将区域标识符转换为其规范形式。这创建了一种一致的表示形式,您可以可靠地存储、比较和显示。

关键概念:

  • 规范形式对语言使用小写,对脚本使用首字母大写,对地区使用大写
  • 扩展关键字在规范形式中按字母顺序排序
  • Intl.getCanonicalLocales() 方法规范化标识符并移除重复项
  • Intl.Locale 构造函数也会生成规范化的输出
  • 规范化不会改变区域标识符的含义
  • 使用规范化的标识符进行存储、比较和显示

规范化是任何处理区域标识符的应用程序的基础操作。它可以防止因大小写不一致引起的错误,并确保您的国际化逻辑可靠地处理区域标识符。