如何将 locale 标识符标准化为规范形式

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

简介

Locale 标识符可以有多种不同的写法,但都指向同一种语言和地区。例如,用户可能会写 EN-usen-USen-us,这三种都表示美式英语。在存储、比较或显示 locale 标识符时,这些不同写法会导致不一致。

标准化会将 locale 标识符转换为统一的规范形式。此过程会调整各组件的大小写、按字母顺序排列扩展关键字,并生成可在整个应用中可靠使用的一致表示。

JavaScript 提供了内置方法,可自动标准化 locale 标识符。本文将介绍标准化的含义、如何在代码中应用标准化,以及在何种情况下标准化标识符能提升国际化逻辑。

locale 标识符标准化的含义

标准化会根据 BCP 47 标准和 Unicode 规范,将 locale 标识符转换为其规范形式。规范形式对大小写、顺序和结构有明确要求。

标准化后的 locale 标识符遵循以下约定:

  • 语言代码为小写
  • 脚本代码首字母大写(标题式)
  • 地区代码为大写
  • 变体代码为小写
  • 扩展关键字按字母顺序排列
  • 扩展属性按字母顺序排列

这些规则为每个 locale 创建了唯一的标准表示形式。无论用户如何书写 locale 标识符,标准化后的形式始终一致。

理解标准化规则

locale 标识符的每个组成部分在规范形式下都有特定的大小写约定。

语言代码大小写

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

en (correct)
EN (incorrect, but normalizes to en)
eN (incorrect, but normalizes to en)

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

字母书写系统代码大小写

书写系统代码采用首字母大写,其余三个字母小写的格式:

Hans (correct)
hans (incorrect, but normalizes to Hans)
HANS (incorrect, but normalizes to Hans)

常见的书写系统代码包括表示 Latin 的 Latn,表示 Cyrillic 的 Cyrl,表示简体汉字的 Hans,以及表示繁体汉字的 Hant

区域代码大小写

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

US (correct)
us (incorrect, but normalizes to US)
Us (incorrect, but normalizes to US)

这适用于大多数本地标识符中使用的两位国家代码。

扩展顺序

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

en-US-u-ca-gregory-nu-latn (correct)
en-US-u-nu-latn-ca-gregory (incorrect, but normalizes to first form)

日历关键字 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,因为 ca 在字母顺序上排在 hc 前面,hc 又排在 nu 前面。

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

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 构造函数在创建 locale 对象时也会规范化 locale 标识符。你可以通过 toString() 方法访问规范化后的形式。

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

该构造函数接受任意合法大小写的标识符,并生成规范化的 locale 对象。

访问规范化组件

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"

即使 options 对象未指定顺序,输出中的扩展关键字也会按字母顺序排列。

为什么规范化很重要

规范化可为你的应用带来一致性。当你存储、显示或比较 locale 标识符时,使用规范形式可以避免潜在的 bug 并提升可靠性。

一致的存储

在数据库、配置文件或本地存储中保存 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

直接对未规范化的标识符进行字符串比较会导致错误结果。

一致的显示

在向用户或调试输出中展示 locale 标识符时,规范化形式可提供一致的格式:

function displayLocale(identifier) {
  try {
    const normalized = Intl.getCanonicalLocales(identifier)[0];
    return `Current locale: ${normalized}`;
  } catch (error) {
    return "Invalid locale identifier";
  }
}

console.log(displayLocale("EN-us"));
// "Current locale: en-US"

console.log(displayLocale("zh-HANS-cn"));
// "Current locale: zh-Hans-CN"

无论输入格式如何,用户都能看到格式规范的 locale 标识符。

实际应用

在真实应用中,规范化可以解决处理 locale 标识符时常见的问题。

规范化用户输入

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

function processLocaleInput(input) {
  try {
    const normalized = Intl.getCanonicalLocales(input)[0];
    return {
      success: true,
      locale: normalized
    };
  } catch (error) {
    return {
      success: false,
      error: "Please enter a valid locale identifier"
    };
  }
}

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

这样可以确保数据库或配置中的格式一致。

构建 locale 查找表

在为翻译或特定 locale 数据创建查找表时,应使用规范化后的键:

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 的两次调用都使用了相同的规范化键,因此翻译被存储在同一个对象中。

合并 locale 列表

当需要合并来自多个来源的 locale 标识符时,应进行规范化并去重:

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"]

该方法会移除重复项,并统一所有来源的大小写格式。

创建 locale 选择界面

在构建下拉菜单或选择界面时,应对 locale 标识符进行规范化以便展示:

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" }
// ]

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

校验配置文件

在加载配置文件中的 locale 标识符时,应在初始化阶段进行规范化:

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

  try {
    validatedConfig.defaultLocale = Intl.getCanonicalLocales(
      config.defaultLocale
    )[0];
  } catch (error) {
    console.error("Invalid default locale:", 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("Skipping invalid locale:", 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"]
// }

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

规范化与 locale 匹配

规范化对于 locale 匹配算法非常重要。在为用户偏好查找最佳 locale 匹配时,应比较规范化后的形式:

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"

规范化可确保匹配逻辑在不同输入大小写下都能正确运行。

规范化不会改变含义

规范化只影响 locale 标识符的表示方式,不会改变该标识符所代表的语言、书写系统或地区。

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。

总结

规范化通过应用标准大小写规则和对扩展关键字排序,将 locale 标识符转换为其规范形式。这可创建一致的表示方式,便于可靠地存储、比较和展示。

关键概念:

  • 规范形式中,语言为小写,书写系统为首字母大写,地区为大写
  • 扩展关键字在规范形式中按字母顺序排序
  • Intl.getCanonicalLocales() 方法可规范化标识符并去除重复项
  • Intl.Locale 构造函数同样会生成规范化输出
  • 规范化不会改变 locale 标识符的含义
  • 建议在存储、比较和展示时使用规范化后的标识符

规范化是所有处理 locale 标识符的应用程序的基础操作。它可防止因大小写不一致导致的 bug,并确保国际化逻辑能够可靠地处理 locale 标识符。