UNPKG

@formatjs/intl-getcanonicallocales

Version:

Intl.getCanonicalLocales polyfill

418 lines (417 loc) 14.6 kB
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