resolve-accept-language
Advanced tools
Comparing version 1.0.59 to 1.1.0
@@ -5,3 +5,3 @@ import Locale from './locale'; | ||
readonly objects: Locale[]; | ||
/** A set of locale identifiers using the `language`-`country` format. */ | ||
/** A set of locale identifiers using the BCP 47 `language`-`country` case-normalized format. */ | ||
readonly locales: Set<string>; | ||
@@ -15,3 +15,3 @@ /** A set of ISO 639-1 alpha-2 language codes. */ | ||
* | ||
* @param locales - An array of locale identifiers using the `language`-`country` format. | ||
* @param locales - An array of locale identifiers using the BCP 47 `language`-`country` format. | ||
* | ||
@@ -18,0 +18,0 @@ * @throws Will throw an error if one of the locale's format is invalid. |
@@ -8,3 +8,3 @@ "use strict"; | ||
* | ||
* @param locales - An array of locale identifiers using the `language`-`country` format. | ||
* @param locales - An array of locale identifiers using the BCP 47 `language`-`country` format. | ||
* | ||
@@ -17,3 +17,3 @@ * @throws Will throw an error if one of the locale's format is invalid. | ||
this.objects = []; | ||
/** A set of locale identifiers using the `language`-`country` format. */ | ||
/** A set of locale identifiers using the BCP 47 `language`-`country` case-normalized format. */ | ||
this.locales = new Set(); | ||
@@ -20,0 +20,0 @@ /** A set of ISO 639-1 alpha-2 language codes. */ |
@@ -0,3 +1,4 @@ | ||
/** Class to manage a locale identifier using the BCP 47 `language`-`country` format. */ | ||
export default class Locale { | ||
/** The locale identifier using the `language`-`country` format. */ | ||
/** The locale identifier using the BCP 47 `language`-`country` case-normalized format. */ | ||
readonly identifier: string; | ||
@@ -9,8 +10,8 @@ /** The ISO 639-1 alpha-2 language code. */ | ||
/** | ||
* Is a given string a locale identifier using the `language`-`country` format. | ||
* Is a given string a locale identifier following the BCP 47 `language`-`country` format. | ||
* | ||
* @param identifier - A locale identifier using the `language`-`country` format. | ||
* @param caseSensitive - Is the case of the string sensitive? (`true` by default) | ||
* @param identifier - A potential locale identify to verify. | ||
* @param caseNormalized - Should we verify if the identifier is using the case-normalized format? | ||
*/ | ||
static isLocale(identifier: string, caseSensitive?: boolean): boolean; | ||
static isLocale(identifier: string, caseNormalized?: boolean): boolean; | ||
/** | ||
@@ -20,5 +21,5 @@ * Is a given string an ISO 639-1 alpha-2 language code. | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
* @param caseSensitive - Is the case of the string sensitive? (`true` by default) | ||
* @param caseNormalized - Should we verify if the identifier is using the case-normalized format? | ||
*/ | ||
static isLanguageCode(languageCode: string, caseSensitive?: boolean): boolean; | ||
static isLanguageCode(languageCode: string, caseNormalized?: boolean): boolean; | ||
/** | ||
@@ -28,13 +29,13 @@ * Is a given string an ISO 3166-1 alpha-2 country code. | ||
* @param countryCode - An ISO 3166-1 alpha-2 country code. | ||
* @param caseSensitive - Is the case of the string sensitive? (`true` by default) | ||
* @param caseNormalized - Should we verify if the identifier is using the case-normalized format? | ||
*/ | ||
static isCountryCode(countryCode: string, caseSensitive?: boolean): boolean; | ||
static isCountryCode(countryCode: string, caseNormalized?: boolean): boolean; | ||
/** | ||
* Class to manage a locale identifer using the `language`-`country` format. | ||
* Create a new `Locale` object. | ||
* | ||
* @param identifier - A locale identifier using the `language`-`country` format. | ||
* @param identifier - A locale identifier using the BCP 47 `language`-`country` format (case insensitive). | ||
* | ||
* @throws Will throw an error if the locale format is invalid. | ||
* @throws An error if the `identifier` format is invalid. | ||
*/ | ||
constructor(identifier: string); | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
/** Class to manage a locale identifier using the BCP 47 `language`-`country` format. */ | ||
var Locale = /** @class */ (function () { | ||
/** | ||
* Class to manage a locale identifer using the `language`-`country` format. | ||
* Create a new `Locale` object. | ||
* | ||
* @param identifier - A locale identifier using the `language`-`country` format. | ||
* @param identifier - A locale identifier using the BCP 47 `language`-`country` format (case insensitive). | ||
* | ||
* @throws Will throw an error if the locale format is invalid. | ||
* @throws An error if the `identifier` format is invalid. | ||
*/ | ||
@@ -21,10 +22,10 @@ function Locale(identifier) { | ||
/** | ||
* Is a given string a locale identifier using the `language`-`country` format. | ||
* Is a given string a locale identifier following the BCP 47 `language`-`country` format. | ||
* | ||
* @param identifier - A locale identifier using the `language`-`country` format. | ||
* @param caseSensitive - Is the case of the string sensitive? (`true` by default) | ||
* @param identifier - A potential locale identify to verify. | ||
* @param caseNormalized - Should we verify if the identifier is using the case-normalized format? | ||
*/ | ||
Locale.isLocale = function (identifier, caseSensitive) { | ||
if (caseSensitive === void 0) { caseSensitive = true; } | ||
var regExp = new RegExp(/^[a-z]{2}-[A-Z]{2}$/, caseSensitive ? undefined : 'i'); | ||
Locale.isLocale = function (identifier, caseNormalized) { | ||
if (caseNormalized === void 0) { caseNormalized = true; } | ||
var regExp = new RegExp(/^[a-z]{2}-[A-Z]{2}$/, caseNormalized ? undefined : 'i'); | ||
return regExp.test(identifier); | ||
@@ -36,7 +37,7 @@ }; | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
* @param caseSensitive - Is the case of the string sensitive? (`true` by default) | ||
* @param caseNormalized - Should we verify if the identifier is using the case-normalized format? | ||
*/ | ||
Locale.isLanguageCode = function (languageCode, caseSensitive) { | ||
if (caseSensitive === void 0) { caseSensitive = true; } | ||
var regExp = new RegExp(/^[a-z]{2}$/, caseSensitive ? undefined : 'i'); | ||
Locale.isLanguageCode = function (languageCode, caseNormalized) { | ||
if (caseNormalized === void 0) { caseNormalized = true; } | ||
var regExp = new RegExp(/^[a-z]{2}$/, caseNormalized ? undefined : 'i'); | ||
return regExp.test(languageCode); | ||
@@ -48,7 +49,7 @@ }; | ||
* @param countryCode - An ISO 3166-1 alpha-2 country code. | ||
* @param caseSensitive - Is the case of the string sensitive? (`true` by default) | ||
* @param caseNormalized - Should we verify if the identifier is using the case-normalized format? | ||
*/ | ||
Locale.isCountryCode = function (countryCode, caseSensitive) { | ||
if (caseSensitive === void 0) { caseSensitive = true; } | ||
var regExp = new RegExp(/^[A-Z]{2}$/, caseSensitive ? undefined : 'i'); | ||
Locale.isCountryCode = function (countryCode, caseNormalized) { | ||
if (caseNormalized === void 0) { caseNormalized = true; } | ||
var regExp = new RegExp(/^[A-Z]{2}$/, caseNormalized ? undefined : 'i'); | ||
return regExp.test(countryCode); | ||
@@ -55,0 +56,0 @@ }; |
@@ -1,4 +0,5 @@ | ||
import Locale from './locale'; | ||
import type LocaleList from './locale-list'; | ||
/** Lookup list used to match the preferred locale based on the value of an `Accept-Language` HTTP header. */ | ||
export default class LookupList { | ||
/** The list of locales used to get the match during the lookup. */ | ||
private localeList; | ||
/** Data object where the properties are quality (in string format) and their values a set containing locale | ||
@@ -9,33 +10,68 @@ * identifiers using the `language`-`country` format and ISO 639-1 alpha-2 language code. */ | ||
* language code. */ | ||
private unsupportedLocaleLanguagesByQuality; | ||
private relatedLocaleLanguagesByQuality; | ||
/** | ||
* Create a new `LookupList` object. | ||
* | ||
* @param acceptLanguageHeader - The value of an HTTP request `Accept-Language` header (also known as a "language priority list"). | ||
* @param locales - An array of locale identifiers. The order will be used for matching where the first identifier will be more | ||
* likely to be matched than the last identifier. | ||
*/ | ||
constructor(acceptLanguageHeader: string, locales: string[]); | ||
/** | ||
* Get a directive object from a directive string. | ||
* | ||
* @param directiveString - The string representing a directive, extracted from the HTTP header. | ||
* | ||
* @returns A `Directive` object or `undefined` if the string's format is invalid. | ||
*/ | ||
private getDirective; | ||
/** | ||
* Add a locale in the data object matching its quality. | ||
* | ||
* @param quality - The HTTP quality factor associated with a locale. | ||
* @param identifier - A locale identifier using the `language`-`country` format. | ||
* @param quality - The HTTP header's quality factor associated with a locale. | ||
* @param identifier - A locale identifier using the BCP 47 `language`-`country` case-normalized format. | ||
*/ | ||
addLocale(quality: string, identifier: string): void; | ||
private addLocale; | ||
/** | ||
* Add a language in the data object matching its quality. | ||
* | ||
* @param quality - The HTTP quality factor associated with a language. | ||
* @param quality - The HTTP header's quality factor associated with a language. | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
*/ | ||
addLanguage(quality: string, languageCode: string): void; | ||
private addLanguage; | ||
/** | ||
* Add an unsupported locale's language in the data object matching its quality. | ||
* Add a related locale's language in the data object matching its quality. | ||
* | ||
* @param quality - The HTTP quality factor associated with an unsupported locale's language. | ||
* @param quality - The HTTP header's quality factor associated with a related locale's language. | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
*/ | ||
addUnsupportedLocaleLanguage(quality: string, languageCode: string): void; | ||
private addRelatedLocaleLanguage; | ||
/** | ||
* Get the best locale match from the lookup list. | ||
* Get the top (highest-ranked) entry from a dataset object entries. | ||
* | ||
* @param localeList - The list of locale from which the top language can be selected. | ||
* @param defaultLocale - The default locale object when no match is found. | ||
* @param dataObjectEntries - The object entries of a dataset object. | ||
* | ||
* @returns The best match when found, otherwise the default locale identifier. | ||
* @returns The top entry from a dataset object entries. | ||
*/ | ||
getBestMatch(localeList: LocaleList, defaultLocale: Locale): string; | ||
private getTop; | ||
/** | ||
* Get the top (highest-ranked) locale or language. | ||
* | ||
* @returns The top match, which can either be a locale or a language. | ||
*/ | ||
getTopLocaleOrLanguage(): string | undefined; | ||
/** | ||
* Get the top (highest-ranked) locale by language. | ||
* | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
* | ||
* @returns The top locale with the specified language. | ||
*/ | ||
getTopByLanguage(languageCode: string): string | undefined; | ||
/** | ||
* Get the top (highest-ranked) related locale. | ||
* | ||
* @returns The top related locale. | ||
*/ | ||
getTopRelatedLocale(): string | undefined; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var locale_1 = require("./locale"); | ||
var locale_list_1 = require("./locale-list"); | ||
/** Lookup list used to match the preferred locale based on the value of an `Accept-Language` HTTP header. */ | ||
var LookupList = /** @class */ (function () { | ||
function LookupList() { | ||
/** | ||
* Create a new `LookupList` object. | ||
* | ||
* @param acceptLanguageHeader - The value of an HTTP request `Accept-Language` header (also known as a "language priority list"). | ||
* @param locales - An array of locale identifiers. The order will be used for matching where the first identifier will be more | ||
* likely to be matched than the last identifier. | ||
*/ | ||
function LookupList(acceptLanguageHeader, locales) { | ||
/** Data object where the properties are quality (in string format) and their values a set containing locale | ||
@@ -11,14 +19,64 @@ * identifiers using the `language`-`country` format and ISO 639-1 alpha-2 language code. */ | ||
* language code. */ | ||
this.unsupportedLocaleLanguagesByQuality = {}; | ||
this.relatedLocaleLanguagesByQuality = {}; | ||
this.localeList = new locale_list_1.default(locales); | ||
var directiveStrings = acceptLanguageHeader | ||
.split(',') | ||
.map(function (directiveString) { return directiveString.trim(); }); | ||
for (var _i = 0, directiveStrings_1 = directiveStrings; _i < directiveStrings_1.length; _i++) { | ||
var directiveString = directiveStrings_1[_i]; | ||
var directive = this.getDirective(directiveString); | ||
if (directive === undefined) | ||
continue; // No match for this directive. | ||
var locale = directive.locale, languageCode = directive.languageCode, quality = directive.quality; | ||
// If the language is not supported, skip to the next match. | ||
if (!this.localeList.languages.has(languageCode)) { | ||
continue; | ||
} | ||
// If there is no country code (while the language is supported), add the language preference. | ||
if (!locale) { | ||
this.addLanguage(quality, languageCode); | ||
continue; | ||
} | ||
// If the locale is not supported, but the locale's language is, add to locale language preference. | ||
if (!this.localeList.locales.has(locale) && this.localeList.languages.has(languageCode)) { | ||
this.addRelatedLocaleLanguage(quality, languageCode); | ||
continue; | ||
} | ||
// If the locale is supported, add the locale preference. | ||
this.addLocale(quality, locale); | ||
} | ||
} | ||
/** | ||
* Get a directive object from a directive string. | ||
* | ||
* @param directiveString - The string representing a directive, extracted from the HTTP header. | ||
* | ||
* @returns A `Directive` object or `undefined` if the string's format is invalid. | ||
*/ | ||
LookupList.prototype.getDirective = function (directiveString) { | ||
/** | ||
* The regular expression is excluding certain directives due to the inability to configure those options in modern | ||
* browsers today (also those options seem unpractical): | ||
* | ||
* - The wildcard character "*", as per RFC 2616 (section 14.4), should match any unmatched language tag. | ||
* - Language tags that starts with a wildcard (e.g. "*-CA") should match the first supported locale of a country. | ||
* - A quality value equivalent to "0", as per RFC 2616 (section 3.9), should be considered as "not acceptable". | ||
*/ | ||
var directiveMatch = directiveString.match(/^((?<matchedLanguageCode>([A-Z]{2}))(-(?<matchedCountryCode>[A-Z]{2}))?)(;q=(?<matchedQuality>1|0.(\d*[1-9]\d*){1,3}))?$/i); | ||
if (!(directiveMatch === null || directiveMatch === void 0 ? void 0 : directiveMatch.groups)) | ||
return undefined; // No regular expression match. | ||
var _a = directiveMatch.groups, matchedLanguageCode = _a.matchedLanguageCode, matchedCountryCode = _a.matchedCountryCode, matchedQuality = _a.matchedQuality; | ||
var languageCode = matchedLanguageCode.toLowerCase(); | ||
var countryCode = matchedCountryCode ? matchedCountryCode.toUpperCase() : undefined; | ||
var quality = matchedQuality === undefined ? '1' : parseFloat(matchedQuality).toString(); // Remove trailing zeros. | ||
var locale = countryCode ? "".concat(languageCode, "-").concat(countryCode) : undefined; | ||
return { locale: locale, languageCode: languageCode, quality: quality }; | ||
}; | ||
/** | ||
* Add a locale in the data object matching its quality. | ||
* | ||
* @param quality - The HTTP quality factor associated with a locale. | ||
* @param identifier - A locale identifier using the `language`-`country` format. | ||
* @param quality - The HTTP header's quality factor associated with a locale. | ||
* @param identifier - A locale identifier using the BCP 47 `language`-`country` case-normalized format. | ||
*/ | ||
LookupList.prototype.addLocale = function (quality, identifier) { | ||
if (!locale_1.default.isLocale(identifier)) { | ||
throw new Error("invalid locale identifier '".concat(identifier, "'")); | ||
} | ||
if (!this.localesAndLanguagesByQuality[quality]) { | ||
@@ -32,9 +90,6 @@ this.localesAndLanguagesByQuality[quality] = new Set(); | ||
* | ||
* @param quality - The HTTP quality factor associated with a language. | ||
* @param quality - The HTTP header's quality factor associated with a language. | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
*/ | ||
LookupList.prototype.addLanguage = function (quality, languageCode) { | ||
if (!locale_1.default.isLanguageCode(languageCode)) { | ||
throw new Error("invalid ISO 639-1 alpha-2 language code '".concat(languageCode, "'")); | ||
} | ||
if (!this.localesAndLanguagesByQuality[quality]) { | ||
@@ -46,65 +101,58 @@ this.localesAndLanguagesByQuality[quality] = new Set(); | ||
/** | ||
* Add an unsupported locale's language in the data object matching its quality. | ||
* Add a related locale's language in the data object matching its quality. | ||
* | ||
* @param quality - The HTTP quality factor associated with an unsupported locale's language. | ||
* @param quality - The HTTP header's quality factor associated with a related locale's language. | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
*/ | ||
LookupList.prototype.addUnsupportedLocaleLanguage = function (quality, languageCode) { | ||
if (!locale_1.default.isLanguageCode(languageCode)) { | ||
throw new Error("invalid ISO 639-1 alpha-2 language code '".concat(languageCode, "'")); | ||
LookupList.prototype.addRelatedLocaleLanguage = function (quality, languageCode) { | ||
if (!this.relatedLocaleLanguagesByQuality[quality]) { | ||
this.relatedLocaleLanguagesByQuality[quality] = new Set(); | ||
} | ||
if (!this.unsupportedLocaleLanguagesByQuality[quality]) { | ||
this.unsupportedLocaleLanguagesByQuality[quality] = new Set(); | ||
this.relatedLocaleLanguagesByQuality[quality].add(languageCode); | ||
}; | ||
/** | ||
* Get the top (highest-ranked) entry from a dataset object entries. | ||
* | ||
* @param dataObjectEntries - The object entries of a dataset object. | ||
* | ||
* @returns The top entry from a dataset object entries. | ||
*/ | ||
LookupList.prototype.getTop = function (dataObjectEntries) { | ||
return dataObjectEntries.sort().reverse()[0][1].values().next().value; | ||
}; | ||
/** | ||
* Get the top (highest-ranked) locale or language. | ||
* | ||
* @returns The top match, which can either be a locale or a language. | ||
*/ | ||
LookupList.prototype.getTopLocaleOrLanguage = function () { | ||
var localesAndLanguagesByQuality = Object.entries(this.localesAndLanguagesByQuality); | ||
if (!localesAndLanguagesByQuality.length) { | ||
return undefined; | ||
} | ||
this.unsupportedLocaleLanguagesByQuality[quality].add(languageCode); | ||
return this.getTop(localesAndLanguagesByQuality); | ||
}; | ||
/** | ||
* Get the best locale match from the lookup list. | ||
* Get the top (highest-ranked) locale by language. | ||
* | ||
* @param localeList - The list of locale from which the top language can be selected. | ||
* @param defaultLocale - The default locale object when no match is found. | ||
* @param languageCode - An ISO 639-1 alpha-2 language code. | ||
* | ||
* @returns The best match when found, otherwise the default locale identifier. | ||
* @returns The top locale with the specified language. | ||
*/ | ||
LookupList.prototype.getBestMatch = function (localeList, defaultLocale) { | ||
var _a, _b; | ||
var bestMatch; | ||
// Check if there is any matching locale identifiers or language code. | ||
if (Object.entries(this.localesAndLanguagesByQuality).length) { | ||
var localeOrLanguage_1 = Object.entries(this.localesAndLanguagesByQuality) | ||
.sort() | ||
.reverse()[0][1] | ||
.values() | ||
.next().value; | ||
if (locale_1.default.isLocale(localeOrLanguage_1)) { | ||
bestMatch = localeOrLanguage_1; | ||
} | ||
else { | ||
// The value is a language code. | ||
if (localeOrLanguage_1 !== defaultLocale.languageCode) { | ||
// Only search for a match if the language does not match the default locale's. | ||
bestMatch = (_a = localeList.objects.find(function (_a) { | ||
var languageCode = _a.languageCode; | ||
return languageCode === localeOrLanguage_1; | ||
})) === null || _a === void 0 ? void 0 : _a.identifier; | ||
} | ||
} | ||
LookupList.prototype.getTopByLanguage = function (languageCode) { | ||
var _a; | ||
return (_a = this.localeList.objects.find(function (locale) { return locale.languageCode === languageCode; })) === null || _a === void 0 ? void 0 : _a.identifier; | ||
}; | ||
/** | ||
* Get the top (highest-ranked) related locale. | ||
* | ||
* @returns The top related locale. | ||
*/ | ||
LookupList.prototype.getTopRelatedLocale = function () { | ||
var relatedLocaleLanguagesByQuality = Object.entries(this.relatedLocaleLanguagesByQuality); | ||
if (!relatedLocaleLanguagesByQuality.length) { | ||
return undefined; | ||
} | ||
else if (Object.entries(this.unsupportedLocaleLanguagesByQuality).length) { | ||
// Before using the default locale, check if one of the unsupported locale's language can be found. | ||
var unsupportedLocaleLanguage_1 = Object.entries(this.unsupportedLocaleLanguagesByQuality) | ||
.sort() | ||
.reverse()[0][1] | ||
.values() | ||
.next().value; | ||
if (unsupportedLocaleLanguage_1 !== defaultLocale.languageCode) { | ||
// Only search for a match if the language does not match the default locale's. | ||
bestMatch = (_b = localeList.objects.find(function (_a) { | ||
var languageCode = _a.languageCode; | ||
return languageCode === unsupportedLocaleLanguage_1; | ||
})) === null || _b === void 0 ? void 0 : _b.identifier; | ||
} | ||
} | ||
// Return the best match or the default locale. | ||
return bestMatch ? bestMatch : defaultLocale.identifier; | ||
var topRelatedLocaleLanguage = this.getTop(relatedLocaleLanguagesByQuality); | ||
return this.getTopByLanguage(topRelatedLocaleLanguage); | ||
}; | ||
@@ -111,0 +159,0 @@ return LookupList; |
@@ -0,9 +1,67 @@ | ||
/** Resolve the preferred locale from an HTTP `Accept-Language` header. */ | ||
export declare class ResolveAcceptLanguage { | ||
/** The locale-based match, if applicable. */ | ||
private localeBasedMatch; | ||
/** The language-based match, if applicable. */ | ||
private languageBasedMatch; | ||
/** The related-locale-based match, if applicable. */ | ||
private relatedLocaleBasedMatch; | ||
/** | ||
* Create a new `ResolveAcceptLanguage` object. | ||
* | ||
* All locale identifiers provided as parameters must following the BCP 47 `language`-`country` (case insensitive). | ||
* | ||
* @param acceptLanguageHeader - The value of an HTTP request `Accept-Language` header (also known as a "language priority list"). | ||
* @param locales - An array of locale identifiers. The order will be used for matching where the first identifier will be more | ||
* likely to be matched than the last identifier. | ||
*/ | ||
constructor(acceptLanguageHeader: string, locales: string[]); | ||
/** | ||
* Was a match found when resolving the preferred locale? | ||
* | ||
* @returns True when a match is found, otherwise false. | ||
*/ | ||
hasMatch(): boolean; | ||
/** | ||
* Did the resolution of the preferred locale find no match? | ||
* | ||
* @returns True when there is no match, otherwise false. | ||
*/ | ||
hasNoMatch(): boolean; | ||
/** | ||
* Is the best match locale-based? | ||
* | ||
* @returns True if the best match locale-based, otherwise false. | ||
*/ | ||
bestMatchIsLocaleBased(): boolean; | ||
/** | ||
* Is the best match language-based? | ||
* | ||
* @returns True if the best match language-based, otherwise false. | ||
*/ | ||
bestMatchIsLanguageBased(): boolean; | ||
/** | ||
* Is the best match related-locale-based? | ||
* | ||
* @returns True if the best match related-locale-based, otherwise false. | ||
*/ | ||
bestMatchIsRelatedLocaleBased(): boolean; | ||
/** | ||
* Get the locale which was the best match. | ||
* | ||
* @returns The locale which was the best match. | ||
*/ | ||
getBestMatch(): string | undefined; | ||
} | ||
/** | ||
* Resolve the preferred locale from an HTTP `Accept-Language` header. | ||
* | ||
* All locale identifiers provided as parameters must following the BCP 47 `language`-`country` (case insensitive). | ||
* | ||
* @param acceptLanguageHeader - The value of an HTTP request `Accept-Language` header (also known as a "language priority list"). | ||
* @param supportedLocales - An array of locale identifiers (`language`-`country`). It must include the default locale. | ||
* @param defaultLocale - The default locale (`language`-`country`) when no match is found. | ||
* @param locales - An array of locale identifiers that must include the default locale. The order will be used for matching where | ||
* the first identifier will be more likely to be matched than the last identifier. | ||
* @param defaultLocale - The default locale identifier when no match is found. | ||
* | ||
* @returns The preferred locale identifier following the BCP 47 `language`-`country` (case-normalized) format. | ||
* @returns The locale identifier which was the best match, in case-normalized format. | ||
* | ||
@@ -18,2 +76,2 @@ * @example | ||
*/ | ||
export default function resolveAcceptLanguage(acceptLanguageHeader: string, supportedLocales: string[], defaultLocale: string): string; | ||
export default function resolveAcceptLanguage(acceptLanguageHeader: string, locales: string[], defaultLocale: string): string; |
"use strict"; | ||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { | ||
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { | ||
if (ar || !(i in from)) { | ||
if (!ar) ar = Array.prototype.slice.call(from, 0, i); | ||
ar[i] = from[i]; | ||
} | ||
} | ||
return to.concat(ar || Array.prototype.slice.call(from)); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ResolveAcceptLanguage = void 0; | ||
var locale_1 = require("./locale"); | ||
var locale_list_1 = require("./locale-list"); | ||
var lookup_list_1 = require("./lookup-list"); | ||
/** Resolve the preferred locale from an HTTP `Accept-Language` header. */ | ||
var ResolveAcceptLanguage = /** @class */ (function () { | ||
/** | ||
* Create a new `ResolveAcceptLanguage` object. | ||
* | ||
* All locale identifiers provided as parameters must following the BCP 47 `language`-`country` (case insensitive). | ||
* | ||
* @param acceptLanguageHeader - The value of an HTTP request `Accept-Language` header (also known as a "language priority list"). | ||
* @param locales - An array of locale identifiers. The order will be used for matching where the first identifier will be more | ||
* likely to be matched than the last identifier. | ||
*/ | ||
function ResolveAcceptLanguage(acceptLanguageHeader, locales) { | ||
var lookupList = new lookup_list_1.default(acceptLanguageHeader, locales); | ||
var topLocaleOrLanguage = lookupList.getTopLocaleOrLanguage(); | ||
if (topLocaleOrLanguage !== undefined) { | ||
if (locale_1.default.isLocale(topLocaleOrLanguage)) { | ||
this.localeBasedMatch = topLocaleOrLanguage; | ||
} | ||
else { | ||
this.languageBasedMatch = lookupList.getTopByLanguage(topLocaleOrLanguage); | ||
} | ||
} | ||
else { | ||
this.relatedLocaleBasedMatch = lookupList.getTopRelatedLocale(); | ||
} | ||
} | ||
/** | ||
* Was a match found when resolving the preferred locale? | ||
* | ||
* @returns True when a match is found, otherwise false. | ||
*/ | ||
ResolveAcceptLanguage.prototype.hasMatch = function () { | ||
if (this.localeBasedMatch !== undefined || | ||
this.languageBasedMatch !== undefined || | ||
this.relatedLocaleBasedMatch !== undefined) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
/** | ||
* Did the resolution of the preferred locale find no match? | ||
* | ||
* @returns True when there is no match, otherwise false. | ||
*/ | ||
ResolveAcceptLanguage.prototype.hasNoMatch = function () { | ||
return !this.hasMatch(); | ||
}; | ||
/** | ||
* Is the best match locale-based? | ||
* | ||
* @returns True if the best match locale-based, otherwise false. | ||
*/ | ||
ResolveAcceptLanguage.prototype.bestMatchIsLocaleBased = function () { | ||
return this.localeBasedMatch !== undefined; | ||
}; | ||
/** | ||
* Is the best match language-based? | ||
* | ||
* @returns True if the best match language-based, otherwise false. | ||
*/ | ||
ResolveAcceptLanguage.prototype.bestMatchIsLanguageBased = function () { | ||
return this.languageBasedMatch !== undefined; | ||
}; | ||
/** | ||
* Is the best match related-locale-based? | ||
* | ||
* @returns True if the best match related-locale-based, otherwise false. | ||
*/ | ||
ResolveAcceptLanguage.prototype.bestMatchIsRelatedLocaleBased = function () { | ||
return this.relatedLocaleBasedMatch !== undefined; | ||
}; | ||
/** | ||
* Get the locale which was the best match. | ||
* | ||
* @returns The locale which was the best match. | ||
*/ | ||
ResolveAcceptLanguage.prototype.getBestMatch = function () { | ||
if (this.localeBasedMatch !== undefined) { | ||
return this.localeBasedMatch; | ||
} | ||
else if (this.languageBasedMatch !== undefined) { | ||
return this.languageBasedMatch; | ||
} | ||
else if (this.relatedLocaleBasedMatch !== undefined) { | ||
return this.relatedLocaleBasedMatch; | ||
} | ||
return undefined; | ||
}; | ||
return ResolveAcceptLanguage; | ||
}()); | ||
exports.ResolveAcceptLanguage = ResolveAcceptLanguage; | ||
/** | ||
* Resolve the preferred locale from an HTTP `Accept-Language` header. | ||
* | ||
* All locale identifiers provided as parameters must following the BCP 47 `language`-`country` (case insensitive). | ||
* | ||
* @param acceptLanguageHeader - The value of an HTTP request `Accept-Language` header (also known as a "language priority list"). | ||
* @param supportedLocales - An array of locale identifiers (`language`-`country`). It must include the default locale. | ||
* @param defaultLocale - The default locale (`language`-`country`) when no match is found. | ||
* @param locales - An array of locale identifiers that must include the default locale. The order will be used for matching where | ||
* the first identifier will be more likely to be matched than the last identifier. | ||
* @param defaultLocale - The default locale identifier when no match is found. | ||
* | ||
* @returns The preferred locale identifier following the BCP 47 `language`-`country` (case-normalized) format. | ||
* @returns The locale identifier which was the best match, in case-normalized format. | ||
* | ||
@@ -23,67 +126,25 @@ * @example | ||
*/ | ||
function resolveAcceptLanguage(acceptLanguageHeader, supportedLocales, defaultLocale) { | ||
var _a; | ||
var localeList = new locale_list_1.default(supportedLocales); | ||
var defaultLocaleObject = new locale_1.default(defaultLocale); | ||
if (!localeList.locales.has(defaultLocaleObject.identifier)) { | ||
throw new Error('default locale must be part of the supported locales'); | ||
function resolveAcceptLanguage(acceptLanguageHeader, locales, defaultLocale) { | ||
var localesIncludeDefault = false; | ||
locales.forEach(function (locale) { | ||
if (!locale_1.default.isLocale(locale, false)) { | ||
throw new Error("invalid locale identifier '".concat(locale, "'")); | ||
} | ||
if (locale.toLowerCase() === defaultLocale.toLocaleLowerCase()) { | ||
localesIncludeDefault = true; | ||
} | ||
}); | ||
if (!locale_1.default.isLocale(defaultLocale, false)) { | ||
throw new Error("invalid default locale identifier '".concat(defaultLocale, "'")); | ||
} | ||
if (!acceptLanguageHeader) { | ||
return defaultLocaleObject.identifier; | ||
if (!localesIncludeDefault) { | ||
throw new Error('the default locale must be included in the locales'); | ||
} | ||
var lookupList = new lookup_list_1.default(); | ||
var directives = acceptLanguageHeader.split(',').map(function (directive) { return directive.trim(); }); | ||
for (var _i = 0, directives_1 = directives; _i < directives_1.length; _i++) { | ||
var directive = directives_1[_i]; | ||
/** | ||
* The regular expression is excluding certain directives due to the inability to configure those options in modern | ||
* browsers today (also those options seem unpractical): | ||
* | ||
* - The wildcard character "*", as per RFC 2616 (section 14.4), should match any unmatched language tag. | ||
* - Language tags that starts with a wildcard (e.g. "*-CA") should match the first supported locale of a country. | ||
* - A quality value equivalent to "0", as per RFC 2616 (section 3.9), should be considered as "not acceptable". | ||
*/ | ||
var directiveRegex = RegExp(/^((?<matchedLanguageCode>([A-Z]{2}))(-(?<matchedCountryCode>[A-Z]{2}))?)(;q=(?<matchedQuality>1|0.(\d*[1-9]\d*){1,3}))?$/i); | ||
var directiveDetails = getDirectiveDetails((_a = directiveRegex.exec(directive)) === null || _a === void 0 ? void 0 : _a.groups); | ||
if (directiveDetails) { | ||
var locale = directiveDetails.locale, languageCode = directiveDetails.languageCode, quality = directiveDetails.quality; | ||
// If the language is not supported, skip to the next match. | ||
if (!localeList.languages.has(languageCode)) { | ||
continue; | ||
} | ||
// If there is no country code (while the language is supported), add the language preference. | ||
if (!locale) { | ||
lookupList.addLanguage(quality, languageCode); | ||
continue; | ||
} | ||
// If the locale is not supported, but the locale's language is, add to locale language preference. | ||
if (!localeList.locales.has(locale) && localeList.languages.has(languageCode)) { | ||
lookupList.addUnsupportedLocaleLanguage(quality, languageCode); | ||
continue; | ||
} | ||
// If the locale is supported, add the locale preference. | ||
lookupList.addLocale(quality, locale); | ||
} | ||
var rankedLocales = __spreadArray([defaultLocale], locales.filter(function (locale) { return locale !== defaultLocale; }), true); | ||
var resolveAcceptLanguage = new ResolveAcceptLanguage(acceptLanguageHeader, rankedLocales); | ||
if (resolveAcceptLanguage.hasMatch()) { | ||
return resolveAcceptLanguage.getBestMatch(); | ||
} | ||
return lookupList.getBestMatch(localeList, defaultLocaleObject); | ||
return new locale_1.default(defaultLocale).identifier; | ||
} | ||
exports.default = resolveAcceptLanguage; | ||
/** | ||
* Get directive details from a regex match. | ||
* | ||
* @param directiveMatch - Regex match result for an `Accept-Language` header directive. | ||
* @param defaultLocaleObject - The default locale object used to normalize the result. | ||
* | ||
* @returns Parsed results from a matched `Accept-Language` header directive or `null` when there is no match. | ||
*/ | ||
function getDirectiveDetails(directiveMatch) { | ||
if (!directiveMatch) { | ||
return null; // No regular expression match. | ||
} | ||
var matchedLanguageCode = directiveMatch.matchedLanguageCode, matchedCountryCode = directiveMatch.matchedCountryCode, matchedQuality = directiveMatch.matchedQuality; | ||
var languageCode = matchedLanguageCode.toLowerCase(); | ||
var countryCode = matchedCountryCode ? matchedCountryCode.toUpperCase() : undefined; | ||
var quality = matchedQuality === undefined ? '1' : parseFloat(matchedQuality).toString(); // Remove trailing zeros. | ||
var locale = countryCode ? "".concat(languageCode, "-").concat(countryCode) : undefined; | ||
return { locale: locale, languageCode: languageCode, quality: quality }; | ||
} |
{ | ||
"name": "resolve-accept-language", | ||
"version": "1.0.59", | ||
"version": "1.1.0", | ||
"description": "Resolve the preferred locale based on the value of an `Accept-Language` HTTP header.", | ||
@@ -39,6 +39,6 @@ "author": "Avansai (https://avansai.com)", | ||
"devDependencies": { | ||
"@release-it/conventional-changelog": "^4.3.0", | ||
"@release-it/conventional-changelog": "^5.0.0", | ||
"@types/jest": "^27.4.1", | ||
"@typescript-eslint/eslint-plugin": "^5.20.0", | ||
"@typescript-eslint/parser": "^5.20.0", | ||
"@typescript-eslint/eslint-plugin": "^5.21.0", | ||
"@typescript-eslint/parser": "^5.21.0", | ||
"dotenv-cli": "^5.1.0", | ||
@@ -48,9 +48,9 @@ "eslint": "^8.14.0", | ||
"eslint-plugin-jest": "^26.1.5", | ||
"jest": "^27.5.1", | ||
"jest": "^28.0.3", | ||
"prettier": "2.6.2", | ||
"release-it": "^14.14.2", | ||
"ts-jest": "^27.1.4", | ||
"release-it": "^15.0.0", | ||
"ts-jest": "^28.0.0-next.3", | ||
"ts-node": "^10.7.0", | ||
"typescript": "^4.6.3" | ||
"typescript": "^4.6.4" | ||
} | ||
} |
@@ -35,28 +35,70 @@ # resolve-accept-language | ||
## Why another `Accept-Language` package? | ||
## Advanced use cases | ||
The `Accept-Language` header has been around since 1999. Like many other standards that deal with languages, the headers is based | ||
on BCP 47 language tags. Language tags can be as simple as `fr` (non country specific French) or more complex, for example | ||
`sr-Latn-RS` would represent latin script Serbian. | ||
You may want to control exactly the behavior depending on the type of match. For example, you could want to display a language picker on your home page if the match is not satisfactory. In those cases, you will need to use the `ResolveAcceptLanguage` class instead. It offers more visibility into the selection process while matching a locale: | ||
One of the main challenge is that BCP 47 language tags can be either overly simple or too complex. This is one of the problem this | ||
library will try to address by focusing on locales identifier using the `language`-`country` format instead of trying to provide | ||
full BCP 47 language tags support. The main reasons for this: | ||
```ts | ||
import { ResolveAcceptLanguage } from 'resolve-accept-language'; | ||
- Using 2 letter language codes is rarely sufficient. Without being explicit about the targeted country for a given language, it is impossible to provide the right format for some content such as dates and numbers. Also, while languages are similar across countries, there are different ways to say the same thing. Our hypothesis is that by better targeting the audience, the user experience will improve. | ||
- About 99% of all cases can be covered using the `language`-`country` format. We could possibly extend script support in the future given a valid use case, but in the meantime our goal is to keep this library as simple as possible, while providing the best matches. | ||
/** | ||
* If you are planning to have a "default locale", make sure to add it first in the provided locale list. | ||
* By doing this, your match result will be identical to `resolveAcceptLanguage` as it always checks the | ||
* default locale first. | ||
*/ | ||
const resolveAcceptLanguage = new ResolveAcceptLanguage('fr-CA;q=0.01,en-CA;q=0.1,en-US;q=0.001', [ | ||
'en-US', | ||
'fr-CA', | ||
]); | ||
if (resolveAcceptLanguage.hasMatch()) { | ||
const locale = resolveAcceptLanguage.getBestMatch() as string; | ||
console.log(`A locale was matched: ${locale}`); | ||
if (resolveAcceptLanguage.bestMatchIsLocaleBased()) { | ||
console.log('The match is locale-based'); | ||
} else if (resolveAcceptLanguage.bestMatchIsLanguageBased()) { | ||
console.log('The match is language-based'); | ||
} else if (resolveAcceptLanguage.bestMatchIsRelatedLocaleBased()) { | ||
console.log('The match is related-locale-based'); | ||
} | ||
} | ||
if (resolveAcceptLanguage.hasNoMatch()) { | ||
console.log('No match found :('); | ||
} | ||
``` | ||
## How does the resolver work? | ||
As per RFC 4647, this solution uses the "lookup" matching scheme. This means that it will always produce exactly one match for a | ||
As per RFC 4647, this package uses the "lookup" matching scheme. This means that it will always produce exactly one match for a | ||
given request. | ||
The matching strategy will use the following rules: | ||
The matching strategy will use the following logic: | ||
1. Extract all **supported** locales and languages and sort them by quality factor. | ||
2. If the first result with the highest quality is a locale, return this as the best match. | ||
3. Otherwise, if that result is a language, find the first supported locale with that language (the default locale is always checked first). | ||
4. Otherwise, if no locale or language matches, check if there is a match with an unsupported locale that had a supported language (the default locale is always checked first). | ||
5. When all fails, return the default locale. | ||
1. The default locale (when provided) will always be put as the first locale being evaluated since it is considered the highest quality content available. Otherwise, the locales will be evaluated in the order provided, where the first is the highest quality and the last the lowest. | ||
2. All locales and languages are extracted from the HTTP header and sorted by quality factor. Locales and languages that are in the HTTP header but not in scope are discarded. | ||
3. Three different matching patterns (based on the HTTP header's quality factor and order of the provided locales): | ||
1. If there were any matches, get the highest-ranked (quality factor) locale or language code: | ||
1. **Locale-based match**: Is the highest-ranked a locale? If yes, this is the best match. | ||
2. **Language-based match**: Otherwise, find the first locale that matches the highest-ranked language. | ||
2. **Related-locale-based match**: If there is no match, find the first locale with a language that matches the highest-ranked language of locales that were not in scope. This is a bit of a "fuzzy match", but the presumption is that it's better to show content in a language that can be understood even if the country is wrong. | ||
4. When using `resolveAcceptLanguage` return the default locale as a last resort option. | ||
## Why another `Accept-Language` package? | ||
The `Accept-Language` header has been around since 1999. Like many other standards that deal with languages, the header is based | ||
on BCP 47 language tags. Language tags can be as simple as `fr` (non-country specific French) or more complex, for example, | ||
`sr-Latn-RS` would represent Latin script Serbian. | ||
One of the main challenges is that BCP 47 language tags can be either overly simple or too complex. This is one of the problems this | ||
library will try to address by focusing on locales identifiers using the `language`-`country` format instead of trying to provide | ||
full BCP 47 language tags support. The main reasons for this: | ||
- Using 2 letter language codes is rarely sufficient. Without being explicit about the targeted country for a given language, it is impossible to provide the right format for some content such as dates and numbers. Also, while languages are similar across countries, there are different ways to say the same thing. Our hypothesis is that by better targeting the audience, the user experience will improve. | ||
- About 99% of all cases can be covered using the `language`-`country` format. We could possibly extend script support in the future given a valid use case, but in the meantime, our goal is to keep this library as simple as possible, while providing the best matches. | ||
## Additional references | ||
@@ -63,0 +105,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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
35764
606
109
1