如何将 locale 标识符标准化为规范形式
将 locale 标识符转换为正确大小写和组件顺序的规范格式
简介
Locale 标识符可以有多种不同的写法,但都指向同一种语言和地区。例如,用户可能会写 EN-us、en-US 或 en-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"
language、script 和 region 属性都采用规范形式的正确大小写。
使用选项进行规范化
当你使用选项创建 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 标识符。