如何从 locale 中提取语言、国家和书写系统

使用 JavaScript 解析 locale 标识符并获取其各个组成部分

简介

en-USfr-CAzh-Hans-CN 这样的 locale 标识符,在一个字符串中编码了多种信息。这些组成部分可以告诉你所用的语言、使用该语言的地区,有时还包括书写系统。

在开发国际化应用时,通常需要提取这些单独的组成部分。例如,你可能只想向用户显示语言名称,按地区对 locale 进行分组,或检查 locale 使用的书写系统。与其手动用正则表达式解析字符串,不如使用 JavaScript 的 Intl.Locale API 来可靠地提取各个组成部分。

本指南将介绍 locale 标识符中包含哪些组成部分,如何使用 Intl.Locale API 提取它们,以及在实际开发中何时需要用到这些组成部分。

locale 标识符包含哪些组成部分

locale 标识符遵循 BCP 47 标准,该标准定义了描述语言及其区域变体的结构。完整的 locale 标识符可以包含多个用连字符分隔的组成部分。

最常见的三个组成部分是:

  • 语言:主要使用的语言,如英语、西班牙语或中文
  • 地区:该语言使用的地理区域,如美国、加拿大或中国
  • 书写系统:用于表示该语言的书写系统,如拉丁字母、西里尔字母或汉字

一个简单的 locale 标识符只包含语言代码:

en

大多数 locale 标识符包含语言和地区:

en-US
fr-CA
es-MX

当一种语言可以用多种书写系统书写时,一些本地标识符会包含脚本信息:

zh-Hans-CN
zh-Hant-TW
sr-Cyrl-RS
sr-Latn-RS

理解这些组成部分有助于你在语言回退、内容选择和用户界面定制方面做出决策。

使用 Intl.Locale 提取组成部分

Intl.Locale API 可将本地标识符字符串转换为结构化对象。创建本地对象后,可以通过属性读取其各个组成部分。

通过将标识符传递给构造函数来创建本地对象:

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

console.log(locale.language); // "en"
console.log(locale.region); // "US"

本地对象公开了与标识符每个组成部分对应的属性。这些属性提供了结构化访问,无需进行字符串解析。

提取语言代码

language 属性返回本地标识符中的语言部分。这是用于标识主要语言的两位或三位字母代码。

const english = new Intl.Locale("en-US");
console.log(english.language); // "en"

const french = new Intl.Locale("fr-CA");
console.log(french.language); // "fr"

const chinese = new Intl.Locale("zh-Hans-CN");
console.log(chinese.language); // "zh"

语言代码遵循 ISO 639 标准。常见代码包括 en(英语)、es(西班牙语)、fr(法语)、de(德语)、ja(日语)和 zh(中文)。

在有效的本地标识符中,语言代码始终存在,是唯一必需的组成部分。

const languageOnly = new Intl.Locale("ja");
console.log(languageOnly.language); // "ja"
console.log(languageOnly.region); // undefined

提取语言代码后,可以用它来选择翻译、确定文本处理规则或为用户构建语言选择器。

提取地区代码

region 属性返回本地标识符中的地区部分。这是用于标识语言使用地理区域的两位字母代码。

const americanEnglish = new Intl.Locale("en-US");
console.log(americanEnglish.region); // "US"

const britishEnglish = new Intl.Locale("en-GB");
console.log(britishEnglish.region); // "GB"

const canadianFrench = new Intl.Locale("fr-CA");
console.log(canadianFrench.region); // "CA"

区域代码遵循 ISO 3166-1 标准。它们使用两个大写字母来表示国家和地区。常见的代码包括 US(美国)、GB(英国)、CA(加拿大)、MX(墨西哥)、FR(法国)以及 CN(中国)。

区域代码会影响日期、数字和货币的格式。例如,美式英语使用月-日-年日期格式和小数点作为小数分隔符;英式英语使用日-月-年日期格式和逗号作为千位分隔符。

在区域标识符中,区域代码是可选的。如果没有区域,region 属性会返回 undefined

const genericSpanish = new Intl.Locale("es");
console.log(genericSpanish.region); // undefined

提取区域代码后,可以用它来自定义区域格式、选择特定区域内容或向用户显示位置信息。

提取书写系统代码

script 属性返回 locale 标识符中的书写系统部分。该部分是一个四字母代码,用于标识用于表示该语言的书写系统。

const simplifiedChinese = new Intl.Locale("zh-Hans-CN");
console.log(simplifiedChinese.script); // "Hans"

const traditionalChinese = new Intl.Locale("zh-Hant-TW");
console.log(traditionalChinese.script); // "Hant"

const serbianCyrillic = new Intl.Locale("sr-Cyrl-RS");
console.log(serbianCyrillic.script); // "Cyrl"

const serbianLatin = new Intl.Locale("sr-Latn-RS");
console.log(serbianLatin.script); // "Latn"

书写系统代码遵循 ISO 15924 标准。它们由四个字母组成,首字母大写。常见的代码包括 Latn(拉丁字母)、Cyrl(西里尔字母)、Hans(简体汉字)、Hant(繁体汉字)以及 Arab(阿拉伯字母)。

大多数 locale 都省略了书写系统代码,因为每种语言都有默认的书写系统。例如,英语默认使用拉丁字母,因此你只需写 en,而不是 en-Latn。俄语默认使用西里尔字母,所以你只需写 ru,而不是 ru-Cyrl

当一种语言可以用多种书写方式时,会出现脚本代码。例如,中文有简体和繁体两种字符,塞尔维亚语则有西里尔字母和拉丁字母。在这些情况下,脚本代码用于区分应使用哪种书写系统。

当 locale 没有显式脚本代码时,script 属性会返回 undefined

const english = new Intl.Locale("en-US");
console.log(english.script); // undefined

提取脚本代码后,可以用它来选择字体、确定文本渲染方式,或按书写系统筛选内容。

了解组件未定义的情况

并非所有 locale 标识符都包含所有组件。语言代码是必需的,但地区和脚本是可选的。

当标识符中没有某个组件时,相应的属性会返回 undefined

const locale = new Intl.Locale("fr");

console.log(locale.language); // "fr"
console.log(locale.region); // undefined
console.log(locale.script); // undefined

这种行为可以让你在使用地区或脚本值之前,检查 locale 是否指定了这些内容:

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

if (locale.region) {
  console.log(`Region-specific formatting for ${locale.region}`);
} else {
  console.log("Using default formatting");
}

你可以使用空值合并运算符为其提供默认值:

const locale = new Intl.Locale("es");
const region = locale.region ?? "ES";

console.log(region); // "ES"

在构建 locale 回退链时,检查未定义的组件有助于你构建替代方案:

function buildFallbackChain(identifier) {
  const locale = new Intl.Locale(identifier);
  const fallbacks = [identifier];

  if (locale.region) {
    fallbacks.push(locale.language);
  }

  return fallbacks;
}

console.log(buildFallbackChain("fr-CA")); // ["fr-CA", "fr"]
console.log(buildFallbackChain("fr")); // ["fr"]

这样可以创建一个从最具体到最通用排序的 locale 标识符列表。

提取组件的实际应用场景

在构建国际化应用时,提取 locale 组件可以解决多个常见问题。

按语言分组 locale

在显示可用语言列表时,将具有相同语言代码的 locale 分组:

const locales = ["en-US", "en-GB", "fr-FR", "fr-CA", "es-ES", "es-MX"];

const grouped = locales.reduce((groups, identifier) => {
  const locale = new Intl.Locale(identifier);
  const language = locale.language;

  if (!groups[language]) {
    groups[language] = [];
  }

  groups[language].push(identifier);
  return groups;
}, {});

console.log(grouped);
// {
//   en: ["en-US", "en-GB"],
//   fr: ["fr-FR", "fr-CA"],
//   es: ["es-ES", "es-MX"]
// }

这种组织方式有助于用户找到自己偏好的语言区域变体。

构建 locale 选择器

在构建语言选择界面时,提取组件以显示有意义的标签:

function buildLocaleSelector(identifiers) {
  return identifiers.map(identifier => {
    const locale = new Intl.Locale(identifier);

    const languageNames = new Intl.DisplayNames([identifier], {
      type: "language"
    });

    const regionNames = new Intl.DisplayNames([identifier], {
      type: "region"
    });

    return {
      value: identifier,
      language: languageNames.of(locale.language),
      region: locale.region ? regionNames.of(locale.region) : null
    };
  });
}

const options = buildLocaleSelector(["en-US", "en-GB", "fr-FR"]);
console.log(options);
// [
//   { value: "en-US", language: "English", region: "United States" },
//   { value: "en-GB", language: "English", region: "United Kingdom" },
//   { value: "fr-FR", language: "French", region: "France" }
// ]

这为每个 locale 选项提供了易于理解的标签。

按地区筛选

当需要显示特定地区的内容时,可以提取地区代码以筛选 locale:

function filterByRegion(identifiers, targetRegion) {
  return identifiers.filter(identifier => {
    const locale = new Intl.Locale(identifier);
    return locale.region === targetRegion;
  });
}

const allLocales = ["en-US", "es-US", "en-GB", "fr-FR", "zh-CN"];
const usLocales = filterByRegion(allLocales, "US");

console.log(usLocales); // ["en-US", "es-US"]

这样有助于为特定国家/地区的用户选择合适的 locale。

检查脚本兼容性

在选择字体或渲染文本时,请检查脚本以确保兼容性:

function selectFont(identifier) {
  const locale = new Intl.Locale(identifier);
  const script = locale.script;

  if (script === "Hans" || script === "Hant") {
    return "Noto Sans CJK";
  } else if (script === "Arab") {
    return "Noto Sans Arabic";
  } else if (script === "Cyrl") {
    return "Noto Sans";
  } else {
    return "Noto Sans";
  }
}

console.log(selectFont("zh-Hans-CN")); // "Noto Sans CJK"
console.log(selectFont("ar-SA")); // "Noto Sans Arabic"
console.log(selectFont("en-US")); // "Noto Sans"

这样可以确保每种书写系统的文本都能正确显示。

实现语言回退

当用户的首选 locale 不可用时,回退到基础语言:

function selectBestLocale(userPreference, supportedLocales) {
  const user = new Intl.Locale(userPreference);

  if (supportedLocales.includes(userPreference)) {
    return userPreference;
  }

  const languageMatch = supportedLocales.find(supported => {
    const locale = new Intl.Locale(supported);
    return locale.language === user.language;
  });

  if (languageMatch) {
    return languageMatch;
  }

  return supportedLocales[0];
}

const supported = ["en-US", "fr-FR", "es-ES"];

console.log(selectBestLocale("en-GB", supported)); // "en-US"
console.log(selectBestLocale("fr-CA", supported)); // "fr-FR"
console.log(selectBestLocale("de-DE", supported)); // "en-US"

当无法精确匹配时,这样可以实现平滑的回退。