@formatjs/intl-getcanonicallocales
Version:
Intl.getCanonicalLocales polyfill
418 lines (417 loc) • 14.6 kB
JavaScript
import { languageAlias, scriptAlias, territoryAlias, variantAlias } from "@formatjs_generated/cldr.core/aliases.js";
import { likelySubtags } from "@formatjs_generated/cldr.core/likelySubtags.js";
import { defineProperty, ensureIntl } from "#packages/ecma402-abstract/utils.js";
//#region packages/intl-getcanonicallocales/emitter.ts
function emitUnicodeLanguageId(lang) {
if (!lang) return "";
return [
lang.lang,
lang.script,
lang.region,
...lang.variants || []
].filter(Boolean).join("-");
}
function emitUnicodeLocaleId({ lang, extensions }) {
const chunks = [emitUnicodeLanguageId(lang)];
for (const ext of extensions) {
chunks.push(ext.type);
switch (ext.type) {
case "u":
chunks.push(...ext.attributes, ...ext.keywords.reduce((all, kv) => all.concat(kv), []));
break;
case "t":
chunks.push(emitUnicodeLanguageId(ext.lang), ...ext.fields.reduce((all, kv) => all.concat(kv), []));
break;
default:
chunks.push(ext.value);
break;
}
}
return chunks.filter(Boolean).join("-");
}
//#endregion
//#region packages/intl-getcanonicallocales/parser.ts
const ALPHANUM_1_8 = /^[a-z0-9]{1,8}$/i;
const ALPHANUM_2_8 = /^[a-z0-9]{2,8}$/i;
const ALPHANUM_3_8 = /^[a-z0-9]{3,8}$/i;
const KEY_REGEX = /^[a-z0-9][a-z]$/i;
const TYPE_REGEX = /^[a-z0-9]{3,8}$/i;
const ALPHA_4 = /^[a-z]{4}$/i;
const OTHER_EXTENSION_TYPE = /^[0-9a-svwyz]$/i;
const UNICODE_REGION_SUBTAG_REGEX = /^([a-z]{2}|[0-9]{3})$/i;
const UNICODE_VARIANT_SUBTAG_REGEX = /^([a-z0-9]{5,8}|[0-9][a-z0-9]{3})$/i;
const UNICODE_LANGUAGE_SUBTAG_REGEX = /^([a-z]{2,3}|[a-z]{5,8})$/i;
const TKEY_REGEX = /^[a-z][0-9]$/i;
function isUnicodeLanguageSubtag(lang) {
return UNICODE_LANGUAGE_SUBTAG_REGEX.test(lang);
}
function isUnicodeRegionSubtag(region) {
return UNICODE_REGION_SUBTAG_REGEX.test(region);
}
function isUnicodeScriptSubtag(script) {
return ALPHA_4.test(script);
}
function isUnicodeVariantSubtag(variant) {
return UNICODE_VARIANT_SUBTAG_REGEX.test(variant);
}
function parseUnicodeLanguageId(chunks) {
if (typeof chunks === "string") chunks = chunks.split("-");
const lang = chunks.shift();
if (!lang) throw new RangeError("Missing unicode_language_subtag");
if (lang === "root") return {
lang: "root",
variants: []
};
if (!isUnicodeLanguageSubtag(lang)) throw new RangeError("Malformed unicode_language_subtag");
let script;
if (chunks.length && isUnicodeScriptSubtag(chunks[0])) script = chunks.shift();
let region;
if (chunks.length && isUnicodeRegionSubtag(chunks[0])) region = chunks.shift();
const variants = {};
while (chunks.length && isUnicodeVariantSubtag(chunks[0])) {
const variant = chunks.shift();
if (variant in variants) throw new RangeError(`Duplicate variant "${variant}"`);
variants[variant] = 1;
}
return {
lang,
script,
region,
variants: Object.keys(variants)
};
}
function parseUnicodeExtension(chunks) {
const keywords = [];
let keyword;
while (chunks.length && (keyword = parseKeyword(chunks))) keywords.push(keyword);
if (keywords.length) return {
type: "u",
keywords,
attributes: []
};
const attributes = [];
while (chunks.length && ALPHANUM_3_8.test(chunks[0])) attributes.push(chunks.shift());
while (chunks.length && (keyword = parseKeyword(chunks))) keywords.push(keyword);
if (keywords.length || attributes.length) return {
type: "u",
attributes,
keywords
};
throw new RangeError("Malformed unicode_extension");
}
function parseKeyword(chunks) {
let key;
if (!KEY_REGEX.test(chunks[0])) return;
key = chunks.shift();
const type = [];
while (chunks.length && TYPE_REGEX.test(chunks[0])) type.push(chunks.shift());
let value = "";
if (type.length) value = type.join("-");
return [key, value];
}
function parseTransformedExtension(chunks) {
let lang;
try {
lang = parseUnicodeLanguageId(chunks);
} catch {}
const fields = [];
while (chunks.length && TKEY_REGEX.test(chunks[0])) {
const key = chunks.shift();
const value = [];
while (chunks.length && ALPHANUM_3_8.test(chunks[0])) value.push(chunks.shift());
if (!value.length) throw new RangeError(`Missing tvalue for tkey "${key}"`);
fields.push([key, value.join("-")]);
}
if (fields.length) return {
type: "t",
fields,
lang
};
throw new RangeError("Malformed transformed_extension");
}
function parsePuExtension(chunks) {
const exts = [];
while (chunks.length && ALPHANUM_1_8.test(chunks[0])) exts.push(chunks.shift());
if (exts.length) return {
type: "x",
value: exts.join("-")
};
throw new RangeError("Malformed private_use_extension");
}
function parseOtherExtensionValue(chunks) {
const exts = [];
while (chunks.length && ALPHANUM_2_8.test(chunks[0])) exts.push(chunks.shift());
if (exts.length) return exts.join("-");
return "";
}
function parseExtensions(chunks) {
if (!chunks.length) return { extensions: [] };
const extensions = [];
let unicodeExtension;
let transformedExtension;
let puExtension;
const otherExtensionMap = {};
do {
const type = chunks.shift();
switch (type) {
case "u":
case "U":
if (unicodeExtension) throw new RangeError("There can only be 1 -u- extension");
unicodeExtension = parseUnicodeExtension(chunks);
extensions.push(unicodeExtension);
break;
case "t":
case "T":
if (transformedExtension) throw new RangeError("There can only be 1 -t- extension");
transformedExtension = parseTransformedExtension(chunks);
extensions.push(transformedExtension);
break;
case "x":
case "X":
if (puExtension) throw new RangeError("There can only be 1 -x- extension");
puExtension = parsePuExtension(chunks);
extensions.push(puExtension);
break;
default:
if (!OTHER_EXTENSION_TYPE.test(type)) throw new RangeError("Malformed extension type");
if (type in otherExtensionMap) throw new RangeError(`There can only be 1 -${type}- extension`);
const extension = {
type,
value: parseOtherExtensionValue(chunks)
};
otherExtensionMap[extension.type] = extension;
extensions.push(extension);
break;
}
} while (chunks.length);
return { extensions };
}
function parseUnicodeLocaleId(locale) {
const chunks = locale.split("-");
return {
lang: parseUnicodeLanguageId(chunks),
...parseExtensions(chunks)
};
}
//#endregion
//#region packages/intl-getcanonicallocales/canonicalizer.ts
function canonicalizeAttrs(strs) {
return Object.keys(strs.reduce((all, str) => {
all[str.toLowerCase()] = 1;
return all;
}, {})).sort();
}
function canonicalizeKVs(arr) {
const all = {};
const result = [];
for (const kv of arr) {
if (kv[0] in all) continue;
all[kv[0]] = 1;
if (!kv[1] || kv[1] === "true") result.push([kv[0].toLowerCase()]);
else result.push([kv[0].toLowerCase(), kv[1].toLowerCase()]);
}
return result.sort(compareKV);
}
function compareKV(t1, t2) {
return t1[0] < t2[0] ? -1 : t1[0] > t2[0] ? 1 : 0;
}
function compareExtension(e1, e2) {
return e1.type < e2.type ? -1 : e1.type > e2.type ? 1 : 0;
}
function mergeVariants(v1, v2) {
const result = [...v1];
for (const v of v2) if (v1.indexOf(v) < 0) result.push(v);
return result;
}
/**
* CAVEAT: We don't do this section in the spec bc they have no JSON data
* Use the bcp47 data to replace keys, types, tfields, and tvalues by their canonical forms. See Section 3.6.4 U Extension Data Files) and Section 3.7.1 T Extension Data Files. The aliases are in the alias attribute value, while the canonical is in the name attribute value. For example,
Because of the following bcp47 data:
<key name="ms"…>…<type name="uksystem" … alias="imperial" … />…</key>
We get the following transformation:
en-u-ms-imperial ⇒ en-u-ms-uksystem
* @param lang
*/
function canonicalizeUnicodeLanguageId(unicodeLanguageId) {
/**
* If the language subtag matches the type attribute of a languageAlias element in Supplemental Data, replace the language subtag with the replacement value.
* 1. If there are additional subtags in the replacement value, add them to the result, but only if there is no corresponding subtag already in the tag.
* 2. Five special deprecated grandfathered codes (such as i-default) are in type attributes, and are also replaced.
*/
let finalLangAst = unicodeLanguageId;
if (unicodeLanguageId.variants.length) {
let replacedLang = "";
for (const variant of unicodeLanguageId.variants) if (replacedLang = languageAlias[emitUnicodeLanguageId({
lang: unicodeLanguageId.lang,
variants: [variant]
})]) {
const replacedLangAst = parseUnicodeLanguageId(replacedLang.split("-"));
finalLangAst = {
lang: replacedLangAst.lang,
script: finalLangAst.script || replacedLangAst.script,
region: finalLangAst.region || replacedLangAst.region,
variants: mergeVariants(finalLangAst.variants, replacedLangAst.variants)
};
break;
}
}
if (finalLangAst.script && finalLangAst.region) {
const replacedLang = languageAlias[emitUnicodeLanguageId({
lang: finalLangAst.lang,
script: finalLangAst.script,
region: finalLangAst.region,
variants: []
})];
if (replacedLang) {
const replacedLangAst = parseUnicodeLanguageId(replacedLang.split("-"));
finalLangAst = {
lang: replacedLangAst.lang,
script: replacedLangAst.script,
region: replacedLangAst.region,
variants: finalLangAst.variants
};
}
}
if (finalLangAst.region) {
const replacedLang = languageAlias[emitUnicodeLanguageId({
lang: finalLangAst.lang,
region: finalLangAst.region,
variants: []
})];
if (replacedLang) {
const replacedLangAst = parseUnicodeLanguageId(replacedLang.split("-"));
finalLangAst = {
lang: replacedLangAst.lang,
script: finalLangAst.script || replacedLangAst.script,
region: replacedLangAst.region,
variants: finalLangAst.variants
};
}
}
const replacedLang = languageAlias[emitUnicodeLanguageId({
lang: finalLangAst.lang,
variants: []
})];
if (replacedLang) {
const replacedLangAst = parseUnicodeLanguageId(replacedLang.split("-"));
finalLangAst = {
lang: replacedLangAst.lang,
script: finalLangAst.script || replacedLangAst.script,
region: finalLangAst.region || replacedLangAst.region,
variants: finalLangAst.variants
};
}
if (finalLangAst.region) {
const regionAlias = territoryAlias[finalLangAst.region.toUpperCase()];
let replacedRegion;
if (regionAlias) {
const regions = regionAlias.split(" ");
replacedRegion = regions[0];
const likelySubtag = likelySubtags[emitUnicodeLanguageId({
lang: finalLangAst.lang,
script: finalLangAst.script,
variants: []
})];
if (likelySubtag) {
const { region: likelyRegion } = parseUnicodeLanguageId(likelySubtag.split("-"));
if (likelyRegion && regions.indexOf(likelyRegion) > -1) replacedRegion = likelyRegion;
}
}
if (replacedRegion) finalLangAst.region = replacedRegion;
finalLangAst.region = finalLangAst.region.toUpperCase();
}
if (finalLangAst.script) {
finalLangAst.script = finalLangAst.script[0].toUpperCase() + finalLangAst.script.slice(1).toLowerCase();
if (scriptAlias[finalLangAst.script]) finalLangAst.script = scriptAlias[finalLangAst.script];
}
if (finalLangAst.variants.length) {
for (let i = 0; i < finalLangAst.variants.length; i++) {
let variant = finalLangAst.variants[i].toLowerCase();
if (variantAlias[variant]) {
const alias = variantAlias[variant];
if (isUnicodeVariantSubtag(alias)) finalLangAst.variants[i] = alias;
else if (isUnicodeLanguageSubtag(alias)) finalLangAst.lang = alias;
}
}
finalLangAst.variants.sort();
}
return finalLangAst;
}
/**
* Canonicalize based on
* https://www.unicode.org/reports/tr35/tr35.html#Canonical_Unicode_Locale_Identifiers
* https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid
* IMPORTANT: This modifies the object inline
* @param locale
*/
function CanonicalizeUnicodeLocaleId(locale) {
locale.lang = canonicalizeUnicodeLanguageId(locale.lang);
if (locale.extensions) {
for (const extension of locale.extensions) switch (extension.type) {
case "u":
extension.keywords = canonicalizeKVs(extension.keywords);
if (extension.attributes) extension.attributes = canonicalizeAttrs(extension.attributes);
break;
case "t":
if (extension.lang) extension.lang = canonicalizeUnicodeLanguageId(extension.lang);
extension.fields = canonicalizeKVs(extension.fields);
break;
default:
extension.value = extension.value.toLowerCase();
break;
}
locale.extensions.sort(compareExtension);
}
return locale;
}
//#endregion
//#region packages/intl-getcanonicallocales/index.ts
/**
* Check if value is an Intl.Locale object by checking for [[InitializedLocale]] internal slot
* We detect this by checking if it's an Intl.Locale instance
*
* Per ECMA-402 #sec-canonicalizelocalelist step 7.c:
* "If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then
* Let tag be kValue.[[Locale]]"
*
* https://tc39.es/ecma402/#sec-canonicalizelocalelist
*/
function isLocaleObject(value) {
return typeof value === "object" && value !== null && typeof value.toString === "function" && typeof value.baseName === "string" && typeof value.language === "string";
}
/**
* https://tc39.es/ecma402/#sec-canonicalizelocalelist
* @param locales
*/
function CanonicalizeLocaleList(locales) {
if (locales === void 0) return [];
const seen = [];
if (typeof locales === "string" || isLocaleObject(locales)) locales = [locales];
const O = Object(locales);
const len = typeof O.length === "number" ? O.length : 0;
for (let k = 0; k < len; k++) {
if (!(k in O)) continue;
const kValue = O[k];
let tag;
if (typeof kValue === "string") tag = kValue;
else if (isLocaleObject(kValue)) tag = kValue.toString();
else throw new TypeError(`Invalid locale type: expected string or Intl.Locale, got ${typeof kValue}`);
const canonicalizedTag = emitUnicodeLocaleId(CanonicalizeUnicodeLocaleId(parseUnicodeLocaleId(tag)));
if (seen.indexOf(canonicalizedTag) < 0) seen.push(canonicalizedTag);
}
return seen;
}
function getCanonicalLocales(locales) {
return CanonicalizeLocaleList(locales);
}
//#endregion
//#region packages/intl-getcanonicallocales/should-polyfill.ts
function shouldPolyfill() {
return typeof Intl === "undefined" || !("getCanonicalLocales" in Intl) || Intl.getCanonicalLocales("und-x-private")[0] === "x-private";
}
//#endregion
//#region packages/intl-getcanonicallocales/polyfill.ts
const intl = ensureIntl();
if (shouldPolyfill()) defineProperty(intl, "getCanonicalLocales", { value: getCanonicalLocales });
//#endregion
//# sourceMappingURL=polyfill.iife.js.map