@fluent/langneg
Advanced tools
Comparing version 0.6.2 to 0.7.0
# Changelog | ||
## [0.7.0](https://github.com/projectfluent/fluent.js/compare/@fluent/langneg@0.6.2...@fluent/langneg@0.7.0) (2023-03-13) | ||
- Drop Node.js v12 support, add v18 & latest to CI tests | ||
([#607](https://github.com/projectfluent/fluent.js/pull/607)) | ||
## [0.6.2](https://github.com/projectfluent/fluent.js/compare/@fluent/langneg@0.6.1...@fluent/langneg@0.6.2) (2022-05-03) | ||
@@ -4,0 +9,0 @@ |
export function acceptedLanguages(str = "") { | ||
if (typeof str !== "string") { | ||
throw new TypeError("Argument must be a string"); | ||
} | ||
const tokens = str.split(",").map((t) => t.trim()); | ||
return tokens.filter((t) => t !== "").map((t) => t.split(";")[0]); | ||
if (typeof str !== "string") { | ||
throw new TypeError("Argument must be a string"); | ||
} | ||
const tokens = str.split(",").map(t => t.trim()); | ||
return tokens.filter(t => t !== "").map(t => t.split(";")[0]); | ||
} |
@@ -1,6 +0,3 @@ | ||
export { | ||
negotiateLanguages, | ||
NegotiateLanguagesOptions, | ||
} from "./negotiate_languages.js"; | ||
export { negotiateLanguages, NegotiateLanguagesOptions, } from "./negotiate_languages.js"; | ||
export { acceptedLanguages } from "./accepted_languages.js"; | ||
export { filterMatches } from "./matches.js"; |
@@ -9,4 +9,4 @@ /* | ||
*/ | ||
export { negotiateLanguages } from "./negotiate_languages.js"; | ||
export { negotiateLanguages, } from "./negotiate_languages.js"; | ||
export { acceptedLanguages } from "./accepted_languages.js"; | ||
export { filterMatches } from "./matches.js"; |
export declare class Locale { | ||
isWellFormed: boolean; | ||
language?: string; | ||
script?: string; | ||
region?: string; | ||
variant?: string; | ||
/** | ||
* Parses a locale id using the localeRe into an array with four elements. | ||
* | ||
* If the second argument `range` is set to true, it places range `*` char | ||
* in place of any missing piece. | ||
* | ||
* It also allows skipping the script section of the id, so `en-US` is | ||
* properly parsed as `en-*-US-*`. | ||
*/ | ||
constructor(locale: string); | ||
isEqual(other: Locale): boolean; | ||
matches(other: Locale, thisRange?: boolean, otherRange?: boolean): boolean; | ||
toString(): string; | ||
clearVariants(): void; | ||
clearRegion(): void; | ||
addLikelySubtags(): boolean; | ||
isWellFormed: boolean; | ||
language?: string; | ||
script?: string; | ||
region?: string; | ||
variant?: string; | ||
/** | ||
* Parses a locale id using the localeRe into an array with four elements. | ||
* | ||
* If the second argument `range` is set to true, it places range `*` char | ||
* in place of any missing piece. | ||
* | ||
* It also allows skipping the script section of the id, so `en-US` is | ||
* properly parsed as `en-*-US-*`. | ||
*/ | ||
constructor(locale: string); | ||
isEqual(other: Locale): boolean; | ||
matches(other: Locale, thisRange?: boolean, otherRange?: boolean): boolean; | ||
toString(): string; | ||
clearVariants(): void; | ||
clearRegion(): void; | ||
addLikelySubtags(): boolean; | ||
} |
@@ -18,81 +18,74 @@ /* eslint no-magic-numbers: 0 */ | ||
*/ | ||
const localeRe = new RegExp( | ||
`^${languageCodeRe}${scriptCodeRe}?${regionCodeRe}?${variantCodeRe}?$`, | ||
"i" | ||
); | ||
const localeRe = new RegExp(`^${languageCodeRe}${scriptCodeRe}?${regionCodeRe}?${variantCodeRe}?$`, "i"); | ||
export class Locale { | ||
/** | ||
* Parses a locale id using the localeRe into an array with four elements. | ||
* | ||
* If the second argument `range` is set to true, it places range `*` char | ||
* in place of any missing piece. | ||
* | ||
* It also allows skipping the script section of the id, so `en-US` is | ||
* properly parsed as `en-*-US-*`. | ||
*/ | ||
constructor(locale) { | ||
const result = localeRe.exec(locale.replace(/_/g, "-")); | ||
if (!result) { | ||
this.isWellFormed = false; | ||
return; | ||
/** | ||
* Parses a locale id using the localeRe into an array with four elements. | ||
* | ||
* If the second argument `range` is set to true, it places range `*` char | ||
* in place of any missing piece. | ||
* | ||
* It also allows skipping the script section of the id, so `en-US` is | ||
* properly parsed as `en-*-US-*`. | ||
*/ | ||
constructor(locale) { | ||
const result = localeRe.exec(locale.replace(/_/g, "-")); | ||
if (!result) { | ||
this.isWellFormed = false; | ||
return; | ||
} | ||
let [, language, script, region, variant] = result; | ||
if (language) { | ||
this.language = language.toLowerCase(); | ||
} | ||
if (script) { | ||
this.script = script[0].toUpperCase() + script.slice(1); | ||
} | ||
if (region) { | ||
this.region = region.toUpperCase(); | ||
} | ||
this.variant = variant; | ||
this.isWellFormed = true; | ||
} | ||
let [, language, script, region, variant] = result; | ||
if (language) { | ||
this.language = language.toLowerCase(); | ||
isEqual(other) { | ||
return (this.language === other.language && | ||
this.script === other.script && | ||
this.region === other.region && | ||
this.variant === other.variant); | ||
} | ||
if (script) { | ||
this.script = script[0].toUpperCase() + script.slice(1); | ||
matches(other, thisRange = false, otherRange = false) { | ||
return ((this.language === other.language || | ||
(thisRange && this.language === undefined) || | ||
(otherRange && other.language === undefined)) && | ||
(this.script === other.script || | ||
(thisRange && this.script === undefined) || | ||
(otherRange && other.script === undefined)) && | ||
(this.region === other.region || | ||
(thisRange && this.region === undefined) || | ||
(otherRange && other.region === undefined)) && | ||
(this.variant === other.variant || | ||
(thisRange && this.variant === undefined) || | ||
(otherRange && other.variant === undefined))); | ||
} | ||
if (region) { | ||
this.region = region.toUpperCase(); | ||
toString() { | ||
return [this.language, this.script, this.region, this.variant] | ||
.filter(part => part !== undefined) | ||
.join("-"); | ||
} | ||
this.variant = variant; | ||
this.isWellFormed = true; | ||
} | ||
isEqual(other) { | ||
return ( | ||
this.language === other.language && | ||
this.script === other.script && | ||
this.region === other.region && | ||
this.variant === other.variant | ||
); | ||
} | ||
matches(other, thisRange = false, otherRange = false) { | ||
return ( | ||
(this.language === other.language || | ||
(thisRange && this.language === undefined) || | ||
(otherRange && other.language === undefined)) && | ||
(this.script === other.script || | ||
(thisRange && this.script === undefined) || | ||
(otherRange && other.script === undefined)) && | ||
(this.region === other.region || | ||
(thisRange && this.region === undefined) || | ||
(otherRange && other.region === undefined)) && | ||
(this.variant === other.variant || | ||
(thisRange && this.variant === undefined) || | ||
(otherRange && other.variant === undefined)) | ||
); | ||
} | ||
toString() { | ||
return [this.language, this.script, this.region, this.variant] | ||
.filter((part) => part !== undefined) | ||
.join("-"); | ||
} | ||
clearVariants() { | ||
this.variant = undefined; | ||
} | ||
clearRegion() { | ||
this.region = undefined; | ||
} | ||
addLikelySubtags() { | ||
const newLocale = getLikelySubtagsMin(this.toString().toLowerCase()); | ||
if (newLocale) { | ||
this.language = newLocale.language; | ||
this.script = newLocale.script; | ||
this.region = newLocale.region; | ||
this.variant = newLocale.variant; | ||
return true; | ||
clearVariants() { | ||
this.variant = undefined; | ||
} | ||
return false; | ||
} | ||
clearRegion() { | ||
this.region = undefined; | ||
} | ||
addLikelySubtags() { | ||
const newLocale = getLikelySubtagsMin(this.toString().toLowerCase()); | ||
if (newLocale) { | ||
this.language = newLocale.language; | ||
this.script = newLocale.script; | ||
this.region = newLocale.region; | ||
this.variant = newLocale.variant; | ||
return true; | ||
} | ||
return false; | ||
} | ||
} | ||
@@ -109,53 +102,53 @@ /** | ||
const likelySubtagsMin = { | ||
ar: "ar-arab-eg", | ||
"az-arab": "az-arab-ir", | ||
"az-ir": "az-arab-ir", | ||
be: "be-cyrl-by", | ||
da: "da-latn-dk", | ||
el: "el-grek-gr", | ||
en: "en-latn-us", | ||
fa: "fa-arab-ir", | ||
ja: "ja-jpan-jp", | ||
ko: "ko-kore-kr", | ||
pt: "pt-latn-br", | ||
sr: "sr-cyrl-rs", | ||
"sr-ru": "sr-latn-ru", | ||
sv: "sv-latn-se", | ||
ta: "ta-taml-in", | ||
uk: "uk-cyrl-ua", | ||
zh: "zh-hans-cn", | ||
"zh-hant": "zh-hant-tw", | ||
"zh-hk": "zh-hant-hk", | ||
"zh-mo": "zh-hant-mo", | ||
"zh-tw": "zh-hant-tw", | ||
"zh-gb": "zh-hant-gb", | ||
"zh-us": "zh-hant-us", | ||
ar: "ar-arab-eg", | ||
"az-arab": "az-arab-ir", | ||
"az-ir": "az-arab-ir", | ||
be: "be-cyrl-by", | ||
da: "da-latn-dk", | ||
el: "el-grek-gr", | ||
en: "en-latn-us", | ||
fa: "fa-arab-ir", | ||
ja: "ja-jpan-jp", | ||
ko: "ko-kore-kr", | ||
pt: "pt-latn-br", | ||
sr: "sr-cyrl-rs", | ||
"sr-ru": "sr-latn-ru", | ||
sv: "sv-latn-se", | ||
ta: "ta-taml-in", | ||
uk: "uk-cyrl-ua", | ||
zh: "zh-hans-cn", | ||
"zh-hant": "zh-hant-tw", | ||
"zh-hk": "zh-hant-hk", | ||
"zh-mo": "zh-hant-mo", | ||
"zh-tw": "zh-hant-tw", | ||
"zh-gb": "zh-hant-gb", | ||
"zh-us": "zh-hant-us", | ||
}; | ||
const regionMatchingLangs = [ | ||
"az", | ||
"bg", | ||
"cs", | ||
"de", | ||
"es", | ||
"fi", | ||
"fr", | ||
"hu", | ||
"it", | ||
"lt", | ||
"lv", | ||
"nl", | ||
"pl", | ||
"ro", | ||
"ru", | ||
"az", | ||
"bg", | ||
"cs", | ||
"de", | ||
"es", | ||
"fi", | ||
"fr", | ||
"hu", | ||
"it", | ||
"lt", | ||
"lv", | ||
"nl", | ||
"pl", | ||
"ro", | ||
"ru", | ||
]; | ||
function getLikelySubtagsMin(loc) { | ||
if (Object.prototype.hasOwnProperty.call(likelySubtagsMin, loc)) { | ||
return new Locale(likelySubtagsMin[loc]); | ||
} | ||
const locale = new Locale(loc); | ||
if (locale.language && regionMatchingLangs.includes(locale.language)) { | ||
locale.region = locale.language.toUpperCase(); | ||
return locale; | ||
} | ||
return null; | ||
if (Object.prototype.hasOwnProperty.call(likelySubtagsMin, loc)) { | ||
return new Locale(likelySubtagsMin[loc]); | ||
} | ||
const locale = new Locale(loc); | ||
if (locale.language && regionMatchingLangs.includes(locale.language)) { | ||
locale.region = locale.language.toUpperCase(); | ||
return locale; | ||
} | ||
return null; | ||
} |
@@ -71,6 +71,2 @@ /** | ||
*/ | ||
export declare function filterMatches( | ||
requestedLocales: Array<string>, | ||
availableLocales: Array<string>, | ||
strategy: string | ||
): Array<string>; | ||
export declare function filterMatches(requestedLocales: Array<string>, availableLocales: Array<string>, strategy: string): Array<string>; |
@@ -74,122 +74,134 @@ /* eslint no-magic-numbers: 0 */ | ||
export function filterMatches(requestedLocales, availableLocales, strategy) { | ||
const supportedLocales = new Set(); | ||
const availableLocalesMap = new Map(); | ||
for (let locale of availableLocales) { | ||
let newLocale = new Locale(locale); | ||
if (newLocale.isWellFormed) { | ||
availableLocalesMap.set(locale, new Locale(locale)); | ||
} | ||
} | ||
outer: for (const reqLocStr of requestedLocales) { | ||
const reqLocStrLC = reqLocStr.toLowerCase(); | ||
const requestedLocale = new Locale(reqLocStrLC); | ||
if (requestedLocale.language === undefined) { | ||
continue; | ||
} | ||
// 1) Attempt to make an exact match | ||
// Example: `en-US` === `en-US` | ||
for (const key of availableLocalesMap.keys()) { | ||
if (reqLocStrLC === key.toLowerCase()) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
const supportedLocales = new Set(); | ||
const availableLocalesMap = new Map(); | ||
for (let locale of availableLocales) { | ||
let newLocale = new Locale(locale); | ||
if (newLocale.isWellFormed) { | ||
availableLocalesMap.set(locale, new Locale(locale)); | ||
} | ||
} | ||
} | ||
// 2) Attempt to match against the available range | ||
// This turns `en` into `en-*-*-*` and `en-US` into `en-*-US-*` | ||
// Example: ['en-US'] * ['en'] = ['en'] | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
// 3) Attempt to retrieve a maximal version of the requested locale ID | ||
// If data is available, it'll expand `en` into `en-Latn-US` and | ||
// `zh` into `zh-Hans-CN`. | ||
// Example: ['en'] * ['en-GB', 'en-US'] = ['en-US'] | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
outer: for (const reqLocStr of requestedLocales) { | ||
const reqLocStrLC = reqLocStr.toLowerCase(); | ||
const requestedLocale = new Locale(reqLocStrLC); | ||
if (requestedLocale.language === undefined) { | ||
continue; | ||
} else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
// 4) Attempt to look up for a different variant for the same locale ID | ||
// Example: ['en-US-mac'] * ['en-US-win'] = ['en-US-win'] | ||
requestedLocale.clearVariants(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
// 1) Attempt to make an exact match | ||
// Example: `en-US` === `en-US` | ||
for (const key of availableLocalesMap.keys()) { | ||
if (reqLocStrLC === key.toLowerCase()) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// 5) Attempt to match against the likely subtag without region | ||
// In the example below, addLikelySubtags will turn | ||
// `zh-Hant` into `zh-Hant-TW` giving `zh-TW` priority match | ||
// over `zh-CN`. | ||
// | ||
// Example: ['zh-Hant-HK'] * ['zh-TW', 'zh-CN'] = ['zh-TW'] | ||
requestedLocale.clearRegion(); | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
} | ||
// 2) Attempt to match against the available range | ||
// This turns `en` into `en-*-*-*` and `en-US` into `en-*-US-*` | ||
// Example: ['en-US'] * ['en'] = ['en'] | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// 6) Attempt to look up for a different region for the same locale ID | ||
// Example: ['en-US'] * ['en-AU'] = ['en-AU'] | ||
requestedLocale.clearRegion(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
// 3) Attempt to retrieve a maximal version of the requested locale ID | ||
// If data is available, it'll expand `en` into `en-Latn-US` and | ||
// `zh` into `zh-Hans-CN`. | ||
// Example: ['en'] * ['en-GB', 'en-US'] = ['en-US'] | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// 4) Attempt to look up for a different variant for the same locale ID | ||
// Example: ['en-US-mac'] * ['en-US-win'] = ['en-US-win'] | ||
requestedLocale.clearVariants(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
// 5) Attempt to match against the likely subtag without region | ||
// In the example below, addLikelySubtags will turn | ||
// `zh-Hant` into `zh-Hant-TW` giving `zh-TW` priority match | ||
// over `zh-CN`. | ||
// | ||
// Example: ['zh-Hant-HK'] * ['zh-TW', 'zh-CN'] = ['zh-TW'] | ||
requestedLocale.clearRegion(); | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
// 6) Attempt to look up for a different region for the same locale ID | ||
// Example: ['en-US'] * ['en-AU'] = ['en-AU'] | ||
requestedLocale.clearRegion(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return Array.from(supportedLocales); | ||
return Array.from(supportedLocales); | ||
} |
export interface NegotiateLanguagesOptions { | ||
strategy?: "filtering" | "matching" | "lookup"; | ||
defaultLocale?: string; | ||
strategy?: "filtering" | "matching" | "lookup"; | ||
defaultLocale?: string; | ||
} | ||
@@ -48,6 +48,2 @@ /** | ||
*/ | ||
export declare function negotiateLanguages( | ||
requestedLocales: Readonly<Array<string>>, | ||
availableLocales: Readonly<Array<string>>, | ||
{ strategy, defaultLocale }?: NegotiateLanguagesOptions | ||
): Array<string>; | ||
export declare function negotiateLanguages(requestedLocales: Readonly<Array<string>>, availableLocales: Readonly<Array<string>>, { strategy, defaultLocale }?: NegotiateLanguagesOptions): Array<string>; |
@@ -45,33 +45,16 @@ import { filterMatches } from "./matches.js"; | ||
*/ | ||
export function negotiateLanguages( | ||
requestedLocales, | ||
availableLocales, | ||
{ strategy = "filtering", defaultLocale } = {} | ||
) { | ||
const supportedLocales = filterMatches( | ||
Array.from( | ||
requestedLocales !== null && requestedLocales !== void 0 | ||
? requestedLocales | ||
: [] | ||
).map(String), | ||
Array.from( | ||
availableLocales !== null && availableLocales !== void 0 | ||
? availableLocales | ||
: [] | ||
).map(String), | ||
strategy | ||
); | ||
if (strategy === "lookup") { | ||
if (defaultLocale === undefined) { | ||
throw new Error( | ||
"defaultLocale cannot be undefined for strategy `lookup`" | ||
); | ||
export function negotiateLanguages(requestedLocales, availableLocales, { strategy = "filtering", defaultLocale } = {}) { | ||
const supportedLocales = filterMatches(Array.from(requestedLocales !== null && requestedLocales !== void 0 ? requestedLocales : []).map(String), Array.from(availableLocales !== null && availableLocales !== void 0 ? availableLocales : []).map(String), strategy); | ||
if (strategy === "lookup") { | ||
if (defaultLocale === undefined) { | ||
throw new Error("defaultLocale cannot be undefined for strategy `lookup`"); | ||
} | ||
if (supportedLocales.length === 0) { | ||
supportedLocales.push(defaultLocale); | ||
} | ||
} | ||
if (supportedLocales.length === 0) { | ||
supportedLocales.push(defaultLocale); | ||
else if (defaultLocale && !supportedLocales.includes(defaultLocale)) { | ||
supportedLocales.push(defaultLocale); | ||
} | ||
} else if (defaultLocale && !supportedLocales.includes(defaultLocale)) { | ||
supportedLocales.push(defaultLocale); | ||
} | ||
return supportedLocales; | ||
return supportedLocales; | ||
} |
852
index.js
@@ -1,456 +0,438 @@ | ||
/* @fluent/langneg@0.6.1 */ | ||
/** @fluent/langneg@0.7.0 */ | ||
(function (global, factory) { | ||
typeof exports === "object" && typeof module !== "undefined" | ||
? factory(exports) | ||
: typeof define === "function" && define.amd | ||
? define("@fluent/langneg", ["exports"], factory) | ||
: ((global = | ||
typeof globalThis !== "undefined" ? globalThis : global || self), | ||
factory((global.FluentLangNeg = {}))); | ||
})(this, function (exports) { | ||
"use strict"; | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | ||
typeof define === 'function' && define.amd ? define('@fluent/langneg', ['exports'], factory) : | ||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FluentLangNeg = {})); | ||
})(this, (function (exports) { 'use strict'; | ||
/* eslint no-magic-numbers: 0 */ | ||
const languageCodeRe = "([a-z]{2,3}|\\*)"; | ||
const scriptCodeRe = "(?:-([a-z]{4}|\\*))"; | ||
const regionCodeRe = "(?:-([a-z]{2}|\\*))"; | ||
const variantCodeRe = "(?:-(([0-9][a-z0-9]{3}|[a-z0-9]{5,8})|\\*))"; | ||
/** | ||
* Regular expression splitting locale id into four pieces: | ||
* | ||
* Example: `en-Latn-US-macos` | ||
* | ||
* language: en | ||
* script: Latn | ||
* region: US | ||
* variant: macos | ||
* | ||
* It can also accept a range `*` character on any position. | ||
*/ | ||
const localeRe = new RegExp( | ||
`^${languageCodeRe}${scriptCodeRe}?${regionCodeRe}?${variantCodeRe}?$`, | ||
"i" | ||
); | ||
class Locale { | ||
/* eslint no-magic-numbers: 0 */ | ||
const languageCodeRe = "([a-z]{2,3}|\\*)"; | ||
const scriptCodeRe = "(?:-([a-z]{4}|\\*))"; | ||
const regionCodeRe = "(?:-([a-z]{2}|\\*))"; | ||
const variantCodeRe = "(?:-(([0-9][a-z0-9]{3}|[a-z0-9]{5,8})|\\*))"; | ||
/** | ||
* Parses a locale id using the localeRe into an array with four elements. | ||
* Regular expression splitting locale id into four pieces: | ||
* | ||
* If the second argument `range` is set to true, it places range `*` char | ||
* in place of any missing piece. | ||
* Example: `en-Latn-US-macos` | ||
* | ||
* It also allows skipping the script section of the id, so `en-US` is | ||
* properly parsed as `en-*-US-*`. | ||
* language: en | ||
* script: Latn | ||
* region: US | ||
* variant: macos | ||
* | ||
* It can also accept a range `*` character on any position. | ||
*/ | ||
constructor(locale) { | ||
const result = localeRe.exec(locale.replace(/_/g, "-")); | ||
if (!result) { | ||
this.isWellFormed = false; | ||
return; | ||
} | ||
let [, language, script, region, variant] = result; | ||
if (language) { | ||
this.language = language.toLowerCase(); | ||
} | ||
if (script) { | ||
this.script = script[0].toUpperCase() + script.slice(1); | ||
} | ||
if (region) { | ||
this.region = region.toUpperCase(); | ||
} | ||
this.variant = variant; | ||
this.isWellFormed = true; | ||
} | ||
isEqual(other) { | ||
return ( | ||
this.language === other.language && | ||
this.script === other.script && | ||
this.region === other.region && | ||
this.variant === other.variant | ||
); | ||
} | ||
matches(other, thisRange = false, otherRange = false) { | ||
return ( | ||
(this.language === other.language || | ||
(thisRange && this.language === undefined) || | ||
(otherRange && other.language === undefined)) && | ||
(this.script === other.script || | ||
(thisRange && this.script === undefined) || | ||
(otherRange && other.script === undefined)) && | ||
(this.region === other.region || | ||
(thisRange && this.region === undefined) || | ||
(otherRange && other.region === undefined)) && | ||
(this.variant === other.variant || | ||
(thisRange && this.variant === undefined) || | ||
(otherRange && other.variant === undefined)) | ||
); | ||
} | ||
toString() { | ||
return [this.language, this.script, this.region, this.variant] | ||
.filter((part) => part !== undefined) | ||
.join("-"); | ||
} | ||
clearVariants() { | ||
this.variant = undefined; | ||
} | ||
clearRegion() { | ||
this.region = undefined; | ||
} | ||
addLikelySubtags() { | ||
const newLocale = getLikelySubtagsMin(this.toString().toLowerCase()); | ||
if (newLocale) { | ||
this.language = newLocale.language; | ||
this.script = newLocale.script; | ||
this.region = newLocale.region; | ||
this.variant = newLocale.variant; | ||
return true; | ||
} | ||
return false; | ||
} | ||
} | ||
/** | ||
* Below is a manually a list of likely subtags corresponding to Unicode | ||
* CLDR likelySubtags list. | ||
* This list is curated by the maintainers of Project Fluent and is | ||
* intended to be used in place of the full likelySubtags list in use cases | ||
* where full list cannot be (for example, due to the size). | ||
* | ||
* This version of the list is based on CLDR 30.0.3. | ||
*/ | ||
const likelySubtagsMin = { | ||
ar: "ar-arab-eg", | ||
"az-arab": "az-arab-ir", | ||
"az-ir": "az-arab-ir", | ||
be: "be-cyrl-by", | ||
da: "da-latn-dk", | ||
el: "el-grek-gr", | ||
en: "en-latn-us", | ||
fa: "fa-arab-ir", | ||
ja: "ja-jpan-jp", | ||
ko: "ko-kore-kr", | ||
pt: "pt-latn-br", | ||
sr: "sr-cyrl-rs", | ||
"sr-ru": "sr-latn-ru", | ||
sv: "sv-latn-se", | ||
ta: "ta-taml-in", | ||
uk: "uk-cyrl-ua", | ||
zh: "zh-hans-cn", | ||
"zh-hant": "zh-hant-tw", | ||
"zh-hk": "zh-hant-hk", | ||
"zh-mo": "zh-hant-mo", | ||
"zh-tw": "zh-hant-tw", | ||
"zh-gb": "zh-hant-gb", | ||
"zh-us": "zh-hant-us", | ||
}; | ||
const regionMatchingLangs = [ | ||
"az", | ||
"bg", | ||
"cs", | ||
"de", | ||
"es", | ||
"fi", | ||
"fr", | ||
"hu", | ||
"it", | ||
"lt", | ||
"lv", | ||
"nl", | ||
"pl", | ||
"ro", | ||
"ru", | ||
]; | ||
function getLikelySubtagsMin(loc) { | ||
if (Object.prototype.hasOwnProperty.call(likelySubtagsMin, loc)) { | ||
return new Locale(likelySubtagsMin[loc]); | ||
} | ||
const locale = new Locale(loc); | ||
if (locale.language && regionMatchingLangs.includes(locale.language)) { | ||
locale.region = locale.language.toUpperCase(); | ||
return locale; | ||
} | ||
return null; | ||
} | ||
/* eslint no-magic-numbers: 0 */ | ||
/** | ||
* Negotiates the languages between the list of requested locales against | ||
* a list of available locales. | ||
* | ||
* The algorithm is based on the BCP4647 3.3.2 Extended Filtering algorithm, | ||
* with several modifications: | ||
* | ||
* 1) available locales are treated as ranges | ||
* | ||
* This change allows us to match a more specific request against | ||
* more generic available locale. | ||
* | ||
* For example, if the available locale list provides locale `en`, | ||
* and the requested locale is `en-US`, we treat the available locale as | ||
* a locale that matches all possible english requests. | ||
* | ||
* This means that we expect available locale ID to be as precize as | ||
* the matches they want to cover. | ||
* | ||
* For example, if there is only `sr` available, it's ok to list | ||
* it in available locales. But once the available locales has both, | ||
* Cyrl and Latn variants, the locale IDs should be `sr-Cyrl` and `sr-Latn` | ||
* to avoid any `sr-*` request to match against whole `sr` range. | ||
* | ||
* What it does ([requested] * [available] = [supported]): | ||
* | ||
* ['en-US'] * ['en'] = ['en'] | ||
* | ||
* 2) likely subtags from LDML 4.3 Likely Subtags has been added | ||
* | ||
* The most obvious likely subtag that can be computed is a duplication | ||
* of the language field onto region field (`fr` => `fr-FR`). | ||
* | ||
* On top of that, likely subtags may use a list of mappings, that | ||
* allow the algorithm to handle non-obvious matches. | ||
* For example, making sure that we match `en` to `en-US` or `sr` to | ||
* `sr-Cyrl`, while `sr-RU` to `sr-Latn-RU`. | ||
* | ||
* This list can be taken directly from CLDR Supplemental Data. | ||
* | ||
* What it does ([requested] * [available] = [supported]): | ||
* | ||
* ['fr'] * ['fr-FR'] = ['fr-FR'] | ||
* ['en'] * ['en-US'] = ['en-US'] | ||
* ['sr'] * ['sr-Latn', 'sr-Cyrl'] = ['sr-Cyrl'] | ||
* | ||
* 3) variant/region range check has been added | ||
* | ||
* Lastly, the last form of check is against the requested locale ID | ||
* but with the variant/region field replaced with a `*` range. | ||
* | ||
* The rationale here laid out in LDML 4.4 Language Matching: | ||
* "(...) normally the fall-off between the user's languages is | ||
* substantially greated than regional variants." | ||
* | ||
* In other words, if we can't match for the given region, maybe | ||
* we can match for the same language/script but other region, and | ||
* it will in most cases be preferred over falling back on the next | ||
* language. | ||
* | ||
* What it does ([requested] * [available] = [supported]): | ||
* | ||
* ['en-AU'] * ['en-US'] = ['en-US'] | ||
* ['sr-RU'] * ['sr-Latn-RO'] = ['sr-Latn-RO'] // sr-RU -> sr-Latn-RU | ||
* | ||
* It works similarly to getParentLocales algo, except that we stop | ||
* after matching against variant/region ranges and don't try to match | ||
* ignoring script ranges. That means that `sr-Cyrl` will never match | ||
* against `sr-Latn`. | ||
*/ | ||
function filterMatches(requestedLocales, availableLocales, strategy) { | ||
const supportedLocales = new Set(); | ||
const availableLocalesMap = new Map(); | ||
for (let locale of availableLocales) { | ||
let newLocale = new Locale(locale); | ||
if (newLocale.isWellFormed) { | ||
availableLocalesMap.set(locale, new Locale(locale)); | ||
} | ||
} | ||
outer: for (const reqLocStr of requestedLocales) { | ||
const reqLocStrLC = reqLocStr.toLowerCase(); | ||
const requestedLocale = new Locale(reqLocStrLC); | ||
if (requestedLocale.language === undefined) { | ||
continue; | ||
} | ||
// 1) Attempt to make an exact match | ||
// Example: `en-US` === `en-US` | ||
for (const key of availableLocalesMap.keys()) { | ||
if (reqLocStrLC === key.toLowerCase()) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
} | ||
const localeRe = new RegExp(`^${languageCodeRe}${scriptCodeRe}?${regionCodeRe}?${variantCodeRe}?$`, "i"); | ||
class Locale { | ||
/** | ||
* Parses a locale id using the localeRe into an array with four elements. | ||
* | ||
* If the second argument `range` is set to true, it places range `*` char | ||
* in place of any missing piece. | ||
* | ||
* It also allows skipping the script section of the id, so `en-US` is | ||
* properly parsed as `en-*-US-*`. | ||
*/ | ||
constructor(locale) { | ||
const result = localeRe.exec(locale.replace(/_/g, "-")); | ||
if (!result) { | ||
this.isWellFormed = false; | ||
return; | ||
} | ||
let [, language, script, region, variant] = result; | ||
if (language) { | ||
this.language = language.toLowerCase(); | ||
} | ||
if (script) { | ||
this.script = script[0].toUpperCase() + script.slice(1); | ||
} | ||
if (region) { | ||
this.region = region.toUpperCase(); | ||
} | ||
this.variant = variant; | ||
this.isWellFormed = true; | ||
} | ||
} | ||
// 2) Attempt to match against the available range | ||
// This turns `en` into `en-*-*-*` and `en-US` into `en-*-US-*` | ||
// Example: ['en-US'] * ['en'] = ['en'] | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
} | ||
isEqual(other) { | ||
return (this.language === other.language && | ||
this.script === other.script && | ||
this.region === other.region && | ||
this.variant === other.variant); | ||
} | ||
} | ||
// 3) Attempt to retrieve a maximal version of the requested locale ID | ||
// If data is available, it'll expand `en` into `en-Latn-US` and | ||
// `zh` into `zh-Hans-CN`. | ||
// Example: ['en'] * ['en-GB', 'en-US'] = ['en-US'] | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
matches(other, thisRange = false, otherRange = false) { | ||
return ((this.language === other.language || | ||
(thisRange && this.language === undefined) || | ||
(otherRange && other.language === undefined)) && | ||
(this.script === other.script || | ||
(thisRange && this.script === undefined) || | ||
(otherRange && other.script === undefined)) && | ||
(this.region === other.region || | ||
(thisRange && this.region === undefined) || | ||
(otherRange && other.region === undefined)) && | ||
(this.variant === other.variant || | ||
(thisRange && this.variant === undefined) || | ||
(otherRange && other.variant === undefined))); | ||
} | ||
toString() { | ||
return [this.language, this.script, this.region, this.variant] | ||
.filter(part => part !== undefined) | ||
.join("-"); | ||
} | ||
clearVariants() { | ||
this.variant = undefined; | ||
} | ||
clearRegion() { | ||
this.region = undefined; | ||
} | ||
addLikelySubtags() { | ||
const newLocale = getLikelySubtagsMin(this.toString().toLowerCase()); | ||
if (newLocale) { | ||
this.language = newLocale.language; | ||
this.script = newLocale.script; | ||
this.region = newLocale.region; | ||
this.variant = newLocale.variant; | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
// 4) Attempt to look up for a different variant for the same locale ID | ||
// Example: ['en-US-mac'] * ['en-US-win'] = ['en-US-win'] | ||
requestedLocale.clearVariants(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
} | ||
} | ||
/** | ||
* Below is a manually a list of likely subtags corresponding to Unicode | ||
* CLDR likelySubtags list. | ||
* This list is curated by the maintainers of Project Fluent and is | ||
* intended to be used in place of the full likelySubtags list in use cases | ||
* where full list cannot be (for example, due to the size). | ||
* | ||
* This version of the list is based on CLDR 30.0.3. | ||
*/ | ||
const likelySubtagsMin = { | ||
ar: "ar-arab-eg", | ||
"az-arab": "az-arab-ir", | ||
"az-ir": "az-arab-ir", | ||
be: "be-cyrl-by", | ||
da: "da-latn-dk", | ||
el: "el-grek-gr", | ||
en: "en-latn-us", | ||
fa: "fa-arab-ir", | ||
ja: "ja-jpan-jp", | ||
ko: "ko-kore-kr", | ||
pt: "pt-latn-br", | ||
sr: "sr-cyrl-rs", | ||
"sr-ru": "sr-latn-ru", | ||
sv: "sv-latn-se", | ||
ta: "ta-taml-in", | ||
uk: "uk-cyrl-ua", | ||
zh: "zh-hans-cn", | ||
"zh-hant": "zh-hant-tw", | ||
"zh-hk": "zh-hant-hk", | ||
"zh-mo": "zh-hant-mo", | ||
"zh-tw": "zh-hant-tw", | ||
"zh-gb": "zh-hant-gb", | ||
"zh-us": "zh-hant-us", | ||
}; | ||
const regionMatchingLangs = [ | ||
"az", | ||
"bg", | ||
"cs", | ||
"de", | ||
"es", | ||
"fi", | ||
"fr", | ||
"hu", | ||
"it", | ||
"lt", | ||
"lv", | ||
"nl", | ||
"pl", | ||
"ro", | ||
"ru", | ||
]; | ||
function getLikelySubtagsMin(loc) { | ||
if (Object.prototype.hasOwnProperty.call(likelySubtagsMin, loc)) { | ||
return new Locale(likelySubtagsMin[loc]); | ||
} | ||
} | ||
// 5) Attempt to match against the likely subtag without region | ||
// In the example below, addLikelySubtags will turn | ||
// `zh-Hant` into `zh-Hant-TW` giving `zh-TW` priority match | ||
// over `zh-CN`. | ||
// | ||
// Example: ['zh-Hant-HK'] * ['zh-TW', 'zh-CN'] = ['zh-TW'] | ||
requestedLocale.clearRegion(); | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
const locale = new Locale(loc); | ||
if (locale.language && regionMatchingLangs.includes(locale.language)) { | ||
locale.region = locale.language.toUpperCase(); | ||
return locale; | ||
} | ||
return null; | ||
} | ||
/* eslint no-magic-numbers: 0 */ | ||
/** | ||
* Negotiates the languages between the list of requested locales against | ||
* a list of available locales. | ||
* | ||
* The algorithm is based on the BCP4647 3.3.2 Extended Filtering algorithm, | ||
* with several modifications: | ||
* | ||
* 1) available locales are treated as ranges | ||
* | ||
* This change allows us to match a more specific request against | ||
* more generic available locale. | ||
* | ||
* For example, if the available locale list provides locale `en`, | ||
* and the requested locale is `en-US`, we treat the available locale as | ||
* a locale that matches all possible english requests. | ||
* | ||
* This means that we expect available locale ID to be as precize as | ||
* the matches they want to cover. | ||
* | ||
* For example, if there is only `sr` available, it's ok to list | ||
* it in available locales. But once the available locales has both, | ||
* Cyrl and Latn variants, the locale IDs should be `sr-Cyrl` and `sr-Latn` | ||
* to avoid any `sr-*` request to match against whole `sr` range. | ||
* | ||
* What it does ([requested] * [available] = [supported]): | ||
* | ||
* ['en-US'] * ['en'] = ['en'] | ||
* | ||
* 2) likely subtags from LDML 4.3 Likely Subtags has been added | ||
* | ||
* The most obvious likely subtag that can be computed is a duplication | ||
* of the language field onto region field (`fr` => `fr-FR`). | ||
* | ||
* On top of that, likely subtags may use a list of mappings, that | ||
* allow the algorithm to handle non-obvious matches. | ||
* For example, making sure that we match `en` to `en-US` or `sr` to | ||
* `sr-Cyrl`, while `sr-RU` to `sr-Latn-RU`. | ||
* | ||
* This list can be taken directly from CLDR Supplemental Data. | ||
* | ||
* What it does ([requested] * [available] = [supported]): | ||
* | ||
* ['fr'] * ['fr-FR'] = ['fr-FR'] | ||
* ['en'] * ['en-US'] = ['en-US'] | ||
* ['sr'] * ['sr-Latn', 'sr-Cyrl'] = ['sr-Cyrl'] | ||
* | ||
* 3) variant/region range check has been added | ||
* | ||
* Lastly, the last form of check is against the requested locale ID | ||
* but with the variant/region field replaced with a `*` range. | ||
* | ||
* The rationale here laid out in LDML 4.4 Language Matching: | ||
* "(...) normally the fall-off between the user's languages is | ||
* substantially greated than regional variants." | ||
* | ||
* In other words, if we can't match for the given region, maybe | ||
* we can match for the same language/script but other region, and | ||
* it will in most cases be preferred over falling back on the next | ||
* language. | ||
* | ||
* What it does ([requested] * [available] = [supported]): | ||
* | ||
* ['en-AU'] * ['en-US'] = ['en-US'] | ||
* ['sr-RU'] * ['sr-Latn-RO'] = ['sr-Latn-RO'] // sr-RU -> sr-Latn-RU | ||
* | ||
* It works similarly to getParentLocales algo, except that we stop | ||
* after matching against variant/region ranges and don't try to match | ||
* ignoring script ranges. That means that `sr-Cyrl` will never match | ||
* against `sr-Latn`. | ||
*/ | ||
function filterMatches(requestedLocales, availableLocales, strategy) { | ||
const supportedLocales = new Set(); | ||
const availableLocalesMap = new Map(); | ||
for (let locale of availableLocales) { | ||
let newLocale = new Locale(locale); | ||
if (newLocale.isWellFormed) { | ||
availableLocalesMap.set(locale, new Locale(locale)); | ||
} | ||
} | ||
} | ||
} | ||
// 6) Attempt to look up for a different region for the same locale ID | ||
// Example: ['en-US'] * ['en-AU'] = ['en-AU'] | ||
requestedLocale.clearRegion(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} else if (strategy === "filtering") { | ||
continue; | ||
} else { | ||
continue outer; | ||
} | ||
outer: for (const reqLocStr of requestedLocales) { | ||
const reqLocStrLC = reqLocStr.toLowerCase(); | ||
const requestedLocale = new Locale(reqLocStrLC); | ||
if (requestedLocale.language === undefined) { | ||
continue; | ||
} | ||
// 1) Attempt to make an exact match | ||
// Example: `en-US` === `en-US` | ||
for (const key of availableLocalesMap.keys()) { | ||
if (reqLocStrLC === key.toLowerCase()) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
// 2) Attempt to match against the available range | ||
// This turns `en` into `en-*-*-*` and `en-US` into `en-*-US-*` | ||
// Example: ['en-US'] * ['en'] = ['en'] | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
// 3) Attempt to retrieve a maximal version of the requested locale ID | ||
// If data is available, it'll expand `en` into `en-Latn-US` and | ||
// `zh` into `zh-Hans-CN`. | ||
// Example: ['en'] * ['en-GB', 'en-US'] = ['en-US'] | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
// 4) Attempt to look up for a different variant for the same locale ID | ||
// Example: ['en-US-mac'] * ['en-US-win'] = ['en-US-win'] | ||
requestedLocale.clearVariants(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
// 5) Attempt to match against the likely subtag without region | ||
// In the example below, addLikelySubtags will turn | ||
// `zh-Hant` into `zh-Hant-TW` giving `zh-TW` priority match | ||
// over `zh-CN`. | ||
// | ||
// Example: ['zh-Hant-HK'] * ['zh-TW', 'zh-CN'] = ['zh-TW'] | ||
requestedLocale.clearRegion(); | ||
if (requestedLocale.addLikelySubtags()) { | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, false)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
// 6) Attempt to look up for a different region for the same locale ID | ||
// Example: ['en-US'] * ['en-AU'] = ['en-AU'] | ||
requestedLocale.clearRegion(); | ||
for (const [key, availableLocale] of availableLocalesMap.entries()) { | ||
if (availableLocale.matches(requestedLocale, true, true)) { | ||
supportedLocales.add(key); | ||
availableLocalesMap.delete(key); | ||
if (strategy === "lookup") { | ||
return Array.from(supportedLocales); | ||
} | ||
else if (strategy === "filtering") { | ||
continue; | ||
} | ||
else { | ||
continue outer; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return Array.from(supportedLocales); | ||
} | ||
return Array.from(supportedLocales); | ||
} | ||
/** | ||
* Negotiates the languages between the list of requested locales against | ||
* a list of available locales. | ||
* | ||
* It accepts three arguments: | ||
* | ||
* requestedLocales: | ||
* an Array of strings with BCP47 locale IDs sorted | ||
* according to user preferences. | ||
* | ||
* availableLocales: | ||
* an Array of strings with BCP47 locale IDs of locale for which | ||
* resources are available. Unsorted. | ||
* | ||
* options: | ||
* An object with the following, optional keys: | ||
* | ||
* strategy: 'filtering' (default) | 'matching' | 'lookup' | ||
* | ||
* defaultLocale: | ||
* a string with BCP47 locale ID to be used | ||
* as a last resort locale. | ||
* | ||
* | ||
* It returns an Array of strings with BCP47 locale IDs sorted according to the | ||
* user preferences. | ||
* | ||
* The exact list will be selected differently depending on the strategy: | ||
* | ||
* 'filtering': (default) | ||
* In the filtering strategy, the algorithm will attempt to match | ||
* as many keys in the available locales in order of the requested locales. | ||
* | ||
* 'matching': | ||
* In the matching strategy, the algorithm will attempt to find the | ||
* best possible match for each element of the requestedLocales list. | ||
* | ||
* 'lookup': | ||
* In the lookup strategy, the algorithm will attempt to find a single | ||
* best available locale based on the requested locales list. | ||
* | ||
* This strategy requires defaultLocale option to be set. | ||
*/ | ||
function negotiateLanguages( | ||
requestedLocales, | ||
availableLocales, | ||
{ strategy = "filtering", defaultLocale } = {} | ||
) { | ||
const supportedLocales = filterMatches( | ||
Array.from( | ||
requestedLocales !== null && requestedLocales !== void 0 | ||
? requestedLocales | ||
: [] | ||
).map(String), | ||
Array.from( | ||
availableLocales !== null && availableLocales !== void 0 | ||
? availableLocales | ||
: [] | ||
).map(String), | ||
strategy | ||
); | ||
if (strategy === "lookup") { | ||
if (defaultLocale === undefined) { | ||
throw new Error( | ||
"defaultLocale cannot be undefined for strategy `lookup`" | ||
); | ||
} | ||
if (supportedLocales.length === 0) { | ||
supportedLocales.push(defaultLocale); | ||
} | ||
} else if (defaultLocale && !supportedLocales.includes(defaultLocale)) { | ||
supportedLocales.push(defaultLocale); | ||
/** | ||
* Negotiates the languages between the list of requested locales against | ||
* a list of available locales. | ||
* | ||
* It accepts three arguments: | ||
* | ||
* requestedLocales: | ||
* an Array of strings with BCP47 locale IDs sorted | ||
* according to user preferences. | ||
* | ||
* availableLocales: | ||
* an Array of strings with BCP47 locale IDs of locale for which | ||
* resources are available. Unsorted. | ||
* | ||
* options: | ||
* An object with the following, optional keys: | ||
* | ||
* strategy: 'filtering' (default) | 'matching' | 'lookup' | ||
* | ||
* defaultLocale: | ||
* a string with BCP47 locale ID to be used | ||
* as a last resort locale. | ||
* | ||
* | ||
* It returns an Array of strings with BCP47 locale IDs sorted according to the | ||
* user preferences. | ||
* | ||
* The exact list will be selected differently depending on the strategy: | ||
* | ||
* 'filtering': (default) | ||
* In the filtering strategy, the algorithm will attempt to match | ||
* as many keys in the available locales in order of the requested locales. | ||
* | ||
* 'matching': | ||
* In the matching strategy, the algorithm will attempt to find the | ||
* best possible match for each element of the requestedLocales list. | ||
* | ||
* 'lookup': | ||
* In the lookup strategy, the algorithm will attempt to find a single | ||
* best available locale based on the requested locales list. | ||
* | ||
* This strategy requires defaultLocale option to be set. | ||
*/ | ||
function negotiateLanguages(requestedLocales, availableLocales, { strategy = "filtering", defaultLocale } = {}) { | ||
const supportedLocales = filterMatches(Array.from(requestedLocales !== null && requestedLocales !== void 0 ? requestedLocales : []).map(String), Array.from(availableLocales !== null && availableLocales !== void 0 ? availableLocales : []).map(String), strategy); | ||
if (strategy === "lookup") { | ||
if (defaultLocale === undefined) { | ||
throw new Error("defaultLocale cannot be undefined for strategy `lookup`"); | ||
} | ||
if (supportedLocales.length === 0) { | ||
supportedLocales.push(defaultLocale); | ||
} | ||
} | ||
else if (defaultLocale && !supportedLocales.includes(defaultLocale)) { | ||
supportedLocales.push(defaultLocale); | ||
} | ||
return supportedLocales; | ||
} | ||
return supportedLocales; | ||
} | ||
function acceptedLanguages(str = "") { | ||
if (typeof str !== "string") { | ||
throw new TypeError("Argument must be a string"); | ||
function acceptedLanguages(str = "") { | ||
if (typeof str !== "string") { | ||
throw new TypeError("Argument must be a string"); | ||
} | ||
const tokens = str.split(",").map(t => t.trim()); | ||
return tokens.filter(t => t !== "").map(t => t.split(";")[0]); | ||
} | ||
const tokens = str.split(",").map((t) => t.trim()); | ||
return tokens.filter((t) => t !== "").map((t) => t.split(";")[0]); | ||
} | ||
exports.acceptedLanguages = acceptedLanguages; | ||
exports.filterMatches = filterMatches; | ||
exports.negotiateLanguages = negotiateLanguages; | ||
exports.acceptedLanguages = acceptedLanguages; | ||
exports.filterMatches = filterMatches; | ||
exports.negotiateLanguages = negotiateLanguages; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
}); | ||
})); |
{ | ||
"name": "@fluent/langneg", | ||
"description": "Language Negotiation API for Fluent", | ||
"version": "0.6.2", | ||
"version": "0.7.0", | ||
"homepage": "https://projectfluent.org", | ||
@@ -41,5 +41,5 @@ "author": "Mozilla <l10n-drivers@mozilla.org>", | ||
"engines": { | ||
"node": ">=12.0.0", | ||
"node": ">=14.0.0", | ||
"npm": ">=7.0.0" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
46644
1013