@intlayer/core
Version:
Includes core Intlayer functions like translation, dictionary, and utility functions shared across multiple packages.
132 lines (130 loc) • 5.59 kB
JavaScript
const require_localization_localeResolver = require('./localeResolver.cjs');
//#region src/localization/localeDetector.ts
/**
* Constants
*/
const LANGUAGE_FORMAT_REGULAR_EXPRESSION = /^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$/;
const DEFAULT_QUALITY_SCORE = 1;
/**
* Enumeration for specificity weights.
* Higher values indicate a more precise match.
*/
var SpecificityWeight = /* @__PURE__ */ function(SpecificityWeight$1) {
SpecificityWeight$1[SpecificityWeight$1["None"] = 0] = "None";
SpecificityWeight$1[SpecificityWeight$1["Broad"] = 1] = "Broad";
SpecificityWeight$1[SpecificityWeight$1["Prefix"] = 2] = "Prefix";
SpecificityWeight$1[SpecificityWeight$1["Exact"] = 4] = "Exact";
return SpecificityWeight$1;
}(SpecificityWeight || {});
/**
* Parses a single language tag string from the Accept-Language header.
* Example input: "en-US;q=0.8"
*/
const parseLanguageTag = (tagString, index) => {
const match = LANGUAGE_FORMAT_REGULAR_EXPRESSION.exec(tagString);
if (!match) return null;
const languageCode = match[1];
const regionCode = match[2];
const parameters = match[3];
const fullLocale = regionCode ? `${languageCode}-${regionCode}` : languageCode;
let qualityScore = DEFAULT_QUALITY_SCORE;
if (parameters) {
const parameterList = parameters.split(";");
for (const parameter of parameterList) {
const [key, value] = parameter.split("=");
if (key === "q") qualityScore = parseFloat(value);
}
}
return {
languageCode,
regionCode,
qualityScore,
originalIndex: index,
fullLocale
};
};
/**
* Parses the entire Accept-Language header string into a list of preferences.
*/
const parseAcceptLanguageHeader = (headerValue) => {
const rawTags = headerValue.split(",");
const preferences = [];
for (let index = 0; index < rawTags.length; index++) {
const parsedLanguage = parseLanguageTag(rawTags[index].trim(), index);
if (parsedLanguage) preferences.push(parsedLanguage);
}
return preferences;
};
/**
* Calculates the specificity of a match between a provided language and a requested preference.
*/
const calculateMatchSpecificity = (providedLanguage, preference, providedIndex) => {
const parsedProvided = parseLanguageTag(providedLanguage, providedIndex);
if (!parsedProvided) return null;
let specificityScore = SpecificityWeight.None;
const preferenceFullLower = preference.fullLocale.toLowerCase();
const preferencePrefixLower = preference.languageCode.toLowerCase();
const providedFullLower = parsedProvided.fullLocale.toLowerCase();
const providedPrefixLower = parsedProvided.languageCode.toLowerCase();
if (preferenceFullLower === providedFullLower) specificityScore |= SpecificityWeight.Exact;
else if (preferencePrefixLower === providedFullLower) specificityScore |= SpecificityWeight.Prefix;
else if (preferenceFullLower === providedPrefixLower) specificityScore |= SpecificityWeight.Broad;
else if (preference.fullLocale !== "*") return null;
return {
providedIndex,
headerIndex: preference.originalIndex,
qualityScore: preference.qualityScore,
specificityScore
};
};
/**
* Determines the best match for a specific available language against the list of user accepted languages.
*/
const getBestMatchForLanguage = (providedLanguage, acceptedPreferences, providedIndex) => {
let bestMatch = {
headerIndex: -1,
qualityScore: 0,
specificityScore: 0,
providedIndex
};
for (const preference of acceptedPreferences) {
const matchSpec = calculateMatchSpecificity(providedLanguage, preference, providedIndex);
if (matchSpec) {
if ((bestMatch.specificityScore - matchSpec.specificityScore || bestMatch.qualityScore - matchSpec.qualityScore || bestMatch.headerIndex - matchSpec.headerIndex) < 0) bestMatch = matchSpec;
}
}
return bestMatch;
};
/**
* Comparator function to sort language matches.
* Sorting order:
* 1. Quality Score (Descending)
* 2. Specificity Score (Descending)
* 3. Order in Header (Ascending - lower index is better)
* 4. Order in Provided List (Ascending)
*/
const compareMatchResults = (a, b) => {
return b.qualityScore - a.qualityScore || b.specificityScore - a.specificityScore || a.headerIndex - b.headerIndex || a.providedIndex - b.providedIndex || 0;
};
/**
* Derives the list of preferred languages based on the Accept-Language header
* and an optional list of available languages.
*/
const getPreferredLanguages = (acceptHeader, availableLanguages) => {
const acceptedPreferences = parseAcceptLanguageHeader(acceptHeader === void 0 ? "*" : acceptHeader || "");
if (!availableLanguages) return acceptedPreferences.filter((preference) => preference.qualityScore > 0).sort((a, b) => b.qualityScore - a.qualityScore).map((preference) => preference.fullLocale);
return availableLanguages.map((language, index) => getBestMatchForLanguage(language, acceptedPreferences, index)).filter((result) => result.qualityScore > 0).sort(compareMatchResults).map((result) => availableLanguages[result.providedIndex]);
};
/**
* Detects the locale from the request headers.
*
* Headers are provided by the browser/client and can be used to determine the user's preferred language.
* This function intersects the user's `Accept-Language` header with the application's available locales.
*/
const localeDetector = (headers, availableLocales, defaultLocale) => {
const acceptLanguageHeader = headers["accept-language"];
return require_localization_localeResolver.localeResolver(getPreferredLanguages(acceptLanguageHeader, availableLocales), availableLocales, defaultLocale);
};
//#endregion
exports.localeDetector = localeDetector;
//# sourceMappingURL=localeDetector.cjs.map