@intlayer/core 9.5.12 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/localization/localeDetector.cjs +60 -22
- package/dist/cjs/localization/localeDetector.cjs.map +1 -1
- package/dist/esm/localization/localeDetector.mjs +60 -22
- package/dist/esm/localization/localeDetector.mjs.map +1 -1
- package/dist/types/localization/localeDetector.d.ts +2 -0
- package/dist/types/localization/localeDetector.d.ts.map +1 -1
- package/package.json +6 -6
|
@@ -8,6 +8,24 @@ const require_localization_localeResolver = require('./localeResolver.cjs');
|
|
|
8
8
|
const LANGUAGE_FORMAT_REGULAR_EXPRESSION = /^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$/;
|
|
9
9
|
const DEFAULT_QUALITY_SCORE = 1;
|
|
10
10
|
/**
|
|
11
|
+
* Upper bound of the memoization caches below. Browsers send a handful of
|
|
12
|
+
* distinct `Accept-Language` values, so a small cache holds the working set.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_CACHE_ENTRIES = 512;
|
|
15
|
+
/**
|
|
16
|
+
* Longer headers are resolved without being memoized, so a client sending
|
|
17
|
+
* arbitrary headers cannot grow the cache memory.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_CACHEABLE_HEADER_LENGTH = 128;
|
|
20
|
+
/**
|
|
21
|
+
* Inserts an entry in a bounded cache, evicting the oldest entry when full.
|
|
22
|
+
*/
|
|
23
|
+
const setBoundedCacheEntry = (cache, key, value) => {
|
|
24
|
+
if (cache.size >= MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value);
|
|
25
|
+
cache.set(key, value);
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
11
29
|
* Parses a single language tag string from the Accept-Language header.
|
|
12
30
|
* Example input: "en-US;q=0.8"
|
|
13
31
|
*/
|
|
@@ -31,11 +49,26 @@ const parseLanguageTag = (tagString, index) => {
|
|
|
31
49
|
regionCode,
|
|
32
50
|
qualityScore,
|
|
33
51
|
originalIndex: index,
|
|
34
|
-
fullLocale
|
|
52
|
+
fullLocale,
|
|
53
|
+
fullLocaleLower: fullLocale.toLowerCase(),
|
|
54
|
+
languageCodeLower: languageCode.toLowerCase()
|
|
35
55
|
};
|
|
36
56
|
};
|
|
37
57
|
/**
|
|
38
|
-
*
|
|
58
|
+
* Parsed available languages. They come from the configuration, so the same
|
|
59
|
+
* few tags are parsed on every request: parse each one once.
|
|
60
|
+
*/
|
|
61
|
+
const availableLanguageCache = /* @__PURE__ */ new Map();
|
|
62
|
+
/**
|
|
63
|
+
* Parses an available language, reusing the previous parse of the same tag.
|
|
64
|
+
*/
|
|
65
|
+
const parseAvailableLanguage = (language) => {
|
|
66
|
+
const cachedLanguage = availableLanguageCache.get(language);
|
|
67
|
+
if (cachedLanguage !== void 0) return cachedLanguage;
|
|
68
|
+
return setBoundedCacheEntry(availableLanguageCache, language, parseLanguageTag(language, 0));
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Parses the full Accept-Language header into a list of language preferences.
|
|
39
72
|
*/
|
|
40
73
|
const parseAcceptLanguageHeader = (headerValue) => {
|
|
41
74
|
const rawTags = headerValue.split(",");
|
|
@@ -50,19 +83,13 @@ const parseAcceptLanguageHeader = (headerValue) => {
|
|
|
50
83
|
return preferences;
|
|
51
84
|
};
|
|
52
85
|
/**
|
|
53
|
-
*
|
|
86
|
+
* Determines how well a specific available language matches a requested language preference.
|
|
54
87
|
*/
|
|
55
|
-
const calculateMatchSpecificity = (
|
|
56
|
-
const parsedProvided = parseLanguageTag(providedLanguage, providedIndex);
|
|
57
|
-
if (!parsedProvided) return null;
|
|
88
|
+
const calculateMatchSpecificity = (parsedProvided, preference, providedIndex) => {
|
|
58
89
|
let specificityScore = 0;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const providedPrefixLower = parsedProvided.languageCode.toLowerCase();
|
|
63
|
-
if (preferenceFullLower === providedFullLower) specificityScore |= 4;
|
|
64
|
-
else if (preferencePrefixLower === providedFullLower) specificityScore |= 2;
|
|
65
|
-
else if (preferenceFullLower === providedPrefixLower) specificityScore |= 1;
|
|
90
|
+
if (preference.fullLocaleLower === parsedProvided.fullLocaleLower) specificityScore |= 4;
|
|
91
|
+
else if (preference.languageCodeLower === parsedProvided.fullLocaleLower) specificityScore |= 2;
|
|
92
|
+
else if (preference.fullLocaleLower === parsedProvided.languageCodeLower) specificityScore |= 1;
|
|
66
93
|
else if (preference.fullLocale !== "*") return null;
|
|
67
94
|
return {
|
|
68
95
|
providedIndex,
|
|
@@ -72,7 +99,7 @@ const calculateMatchSpecificity = (providedLanguage, preference, providedIndex)
|
|
|
72
99
|
};
|
|
73
100
|
};
|
|
74
101
|
/**
|
|
75
|
-
*
|
|
102
|
+
* Finds the best matching preference from the header for a specific available language.
|
|
76
103
|
*/
|
|
77
104
|
const getBestMatchForLanguage = (providedLanguage, acceptedPreferences, providedIndex) => {
|
|
78
105
|
let bestMatch = {
|
|
@@ -81,8 +108,10 @@ const getBestMatchForLanguage = (providedLanguage, acceptedPreferences, provided
|
|
|
81
108
|
specificityScore: 0,
|
|
82
109
|
providedIndex
|
|
83
110
|
};
|
|
111
|
+
const parsedProvided = parseAvailableLanguage(providedLanguage);
|
|
112
|
+
if (!parsedProvided) return bestMatch;
|
|
84
113
|
for (const preference of acceptedPreferences) {
|
|
85
|
-
const matchSpec = calculateMatchSpecificity(
|
|
114
|
+
const matchSpec = calculateMatchSpecificity(parsedProvided, preference, providedIndex);
|
|
86
115
|
if (matchSpec) {
|
|
87
116
|
if ((bestMatch.specificityScore - matchSpec.specificityScore || bestMatch.qualityScore - matchSpec.qualityScore || bestMatch.headerIndex - matchSpec.headerIndex) < 0) bestMatch = matchSpec;
|
|
88
117
|
}
|
|
@@ -90,12 +119,8 @@ const getBestMatchForLanguage = (providedLanguage, acceptedPreferences, provided
|
|
|
90
119
|
return bestMatch;
|
|
91
120
|
};
|
|
92
121
|
/**
|
|
93
|
-
* Comparator function to sort
|
|
94
|
-
*
|
|
95
|
-
* 1. Quality Score (Descending)
|
|
96
|
-
* 2. Specificity Score (Descending)
|
|
97
|
-
* 3. Order in Header (Ascending - lower index is better)
|
|
98
|
-
* 4. Order in Provided List (Ascending)
|
|
122
|
+
* Comparator function to sort MatchResults.
|
|
123
|
+
* Order: Quality (desc) -> Specificity (desc) -> Header Order (asc) -> Provided Order (asc)
|
|
99
124
|
*/
|
|
100
125
|
const compareMatchResults = (a, b) => {
|
|
101
126
|
return b.qualityScore - a.qualityScore || b.specificityScore - a.specificityScore || a.headerIndex - b.headerIndex || a.providedIndex - b.providedIndex || 0;
|
|
@@ -110,15 +135,28 @@ const getPreferredLanguages = (acceptHeader, availableLanguages) => {
|
|
|
110
135
|
return availableLanguages.map((language, index) => getBestMatchForLanguage(language, acceptedPreferences, index)).filter((result) => result.qualityScore > 0).sort(compareMatchResults).map((result) => availableLanguages[result.providedIndex]);
|
|
111
136
|
};
|
|
112
137
|
/**
|
|
138
|
+
* Detected locales, keyed by header, available locales and default locale.
|
|
139
|
+
*/
|
|
140
|
+
const detectedLocaleCache = /* @__PURE__ */ new Map();
|
|
141
|
+
/**
|
|
113
142
|
* Detects the locale from the request headers.
|
|
114
143
|
*
|
|
115
144
|
* Headers are provided by the browser/client and can be used to determine the user's preferred language.
|
|
116
145
|
* This function intersects the user's `Accept-Language` header with the application's available locales.
|
|
146
|
+
*
|
|
147
|
+
* The result is memoized, as it only depends on its arguments.
|
|
117
148
|
*/
|
|
118
149
|
const localeDetector = (headers, availableLocales, defaultLocale) => {
|
|
119
150
|
const acceptLanguageHeader = headers["accept-language"];
|
|
151
|
+
const cacheKey = (acceptLanguageHeader?.length ?? 0) <= MAX_CACHEABLE_HEADER_LENGTH ? `${acceptLanguageHeader === void 0 ? "\0" : `=${acceptLanguageHeader}`}\u0001${availableLocales?.join(",") ?? "\0"}\u0001${defaultLocale ?? "\0"}` : void 0;
|
|
152
|
+
if (cacheKey !== void 0) {
|
|
153
|
+
const cachedLocale = detectedLocaleCache.get(cacheKey);
|
|
154
|
+
if (cachedLocale !== void 0) return cachedLocale;
|
|
155
|
+
}
|
|
120
156
|
const preferredLocaleStrings = getPreferredLanguages(acceptLanguageHeader, availableLocales);
|
|
121
|
-
|
|
157
|
+
const detectedLocale = require_localization_localeResolver.localeResolver(preferredLocaleStrings, availableLocales, defaultLocale);
|
|
158
|
+
if (cacheKey !== void 0) setBoundedCacheEntry(detectedLocaleCache, cacheKey, detectedLocale);
|
|
159
|
+
return detectedLocale;
|
|
122
160
|
};
|
|
123
161
|
|
|
124
162
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localeDetector.cjs","names":["localeResolver"],"sources":["../../../src/localization/localeDetector.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport { localeResolver } from './localeResolver';\n\n/**\n * Constants\n */\nconst LANGUAGE_FORMAT_REGULAR_EXPRESSION =\n /^\\s*([^\\s\\-;]+)(?:-([^\\s;]+))?\\s*(?:;(.*))?$/;\nconst DEFAULT_QUALITY_SCORE = 1;\n\n/**\n * Enumeration for specificity weights.\n * Higher values indicate a more precise match.\n */\nenum SpecificityWeight {\n None = 0,\n Broad = 1, // Matches prefix (e.g., 'en' matches 'en-US')\n Prefix = 2, // Matches prefix in reverse (e.g., 'en-US' matches 'en')\n Exact = 4, // Matches exact string (e.g., 'en-US' matches 'en-US')\n}\n\n/**\n * Represents a parsed language tag from the header.\n */\ntype LanguagePreference = {\n languageCode: string;\n regionCode?: string;\n fullLocale: string;\n qualityScore: number;\n originalIndex: number;\n};\n\n/**\n * Represents the result of matching a requested language against an available language.\n */\ntype MatchResult = {\n providedIndex: number;\n headerIndex: number;\n qualityScore: number;\n specificityScore: number;\n};\n\n/**\n * Parses a single language tag string from the Accept-Language header.\n * Example input: \"en-US;q=0.8\"\n */\nconst parseLanguageTag = (\n tagString: string,\n index: number\n): LanguagePreference | null => {\n const match = LANGUAGE_FORMAT_REGULAR_EXPRESSION.exec(tagString);\n if (!match) {\n return null;\n }\n\n const languageCode = match[1] as string;\n const regionCode = match[2];\n const parameters = match[3];\n\n // Construct the full locale string (e.g., \"en-US\" or \"en\")\n const fullLocale = regionCode\n ? `${languageCode}-${regionCode}`\n : languageCode;\n\n let qualityScore = DEFAULT_QUALITY_SCORE;\n\n // Parse parameters to find the quality score (\"q\")\n if (parameters) {\n const parameterList = parameters.split(';');\n\n for (const parameter of parameterList) {\n const [key, value] = parameter.split('=');\n\n if (key === 'q') {\n qualityScore = parseFloat(value as string);\n }\n }\n }\n\n return {\n languageCode,\n regionCode,\n qualityScore,\n originalIndex: index,\n fullLocale,\n };\n};\n\n/**\n * Parses the entire Accept-Language header string into a list of preferences.\n */\nconst parseAcceptLanguageHeader = (\n headerValue: string\n): LanguagePreference[] => {\n const rawTags = headerValue.split(',');\n const preferences: LanguagePreference[] = [];\n\n for (let index = 0; index < rawTags.length; index++) {\n const tag = rawTags[index]?.trim();\n\n if (tag) {\n const parsedLanguage = parseLanguageTag(tag, index);\n\n if (parsedLanguage) {\n preferences.push(parsedLanguage);\n }\n }\n }\n\n return preferences;\n};\n\n/**\n * Calculates the specificity of a match between a provided language and a requested preference.\n */\nconst calculateMatchSpecificity = (\n providedLanguage: string,\n preference: LanguagePreference,\n providedIndex: number\n): MatchResult | null => {\n const parsedProvided = parseLanguageTag(providedLanguage, providedIndex);\n if (!parsedProvided) {\n return null;\n }\n\n let specificityScore = SpecificityWeight.None;\n\n const preferenceFullLower = preference.fullLocale.toLowerCase();\n const preferencePrefixLower = preference.languageCode.toLowerCase();\n const providedFullLower = parsedProvided.fullLocale.toLowerCase();\n const providedPrefixLower = parsedProvided.languageCode.toLowerCase();\n\n if (preferenceFullLower === providedFullLower) {\n specificityScore |= SpecificityWeight.Exact;\n } else if (preferencePrefixLower === providedFullLower) {\n specificityScore |= SpecificityWeight.Prefix;\n } else if (preferenceFullLower === providedPrefixLower) {\n specificityScore |= SpecificityWeight.Broad;\n } else if (preference.fullLocale !== '*') {\n return null;\n }\n\n return {\n providedIndex,\n headerIndex: preference.originalIndex,\n qualityScore: preference.qualityScore,\n specificityScore,\n };\n};\n\n/**\n * Determines the best match for a specific available language against the list of user accepted languages.\n */\nconst getBestMatchForLanguage = (\n providedLanguage: string,\n acceptedPreferences: LanguagePreference[],\n providedIndex: number\n): MatchResult => {\n // Initialize with a non-match priority\n let bestMatch: MatchResult = {\n headerIndex: -1,\n qualityScore: 0,\n specificityScore: 0,\n providedIndex,\n };\n\n for (const preference of acceptedPreferences) {\n const matchSpec = calculateMatchSpecificity(\n providedLanguage,\n preference,\n providedIndex\n );\n\n if (matchSpec) {\n // Compare current best match with new match\n const scoreDifference =\n bestMatch.specificityScore - matchSpec.specificityScore ||\n bestMatch.qualityScore - matchSpec.qualityScore ||\n bestMatch.headerIndex - matchSpec.headerIndex;\n\n // If the new match is better (difference < 0), update priority\n if (scoreDifference < 0) {\n bestMatch = matchSpec;\n }\n }\n }\n\n return bestMatch;\n};\n\n/**\n * Comparator function to sort language matches.\n * Sorting order:\n * 1. Quality Score (Descending)\n * 2. Specificity Score (Descending)\n * 3. Order in Header (Ascending - lower index is better)\n * 4. Order in Provided List (Ascending)\n */\nconst compareMatchResults = (a: MatchResult, b: MatchResult): number => {\n return (\n b.qualityScore - a.qualityScore ||\n b.specificityScore - a.specificityScore ||\n a.headerIndex - b.headerIndex ||\n a.providedIndex - b.providedIndex ||\n 0\n );\n};\n\n/**\n * Derives the list of preferred languages based on the Accept-Language header\n * and an optional list of available languages.\n */\nexport const getPreferredLanguages = (\n acceptHeader: string | undefined,\n availableLanguages?: string[]\n): string[] => {\n // RFC 2616 sec 14.4: no header implies '*'\n const headerValue = acceptHeader === undefined ? '*' : acceptHeader || '';\n const acceptedPreferences = parseAcceptLanguageHeader(headerValue);\n\n // If no specific languages are provided to filter against, return the header languages sorted by quality\n if (!availableLanguages) {\n return acceptedPreferences\n .filter((preference) => preference.qualityScore > 0)\n .sort((a, b) => b.qualityScore - a.qualityScore) // Simple sort by quality\n .map((preference) => preference.fullLocale);\n }\n\n // Map available languages to their match priority against the header\n const matchResults = availableLanguages.map((language, index) =>\n getBestMatchForLanguage(language, acceptedPreferences, index)\n );\n\n return matchResults\n .filter((result) => result.qualityScore > 0)\n .sort(compareMatchResults)\n .map((result) => availableLanguages[result.providedIndex]) as string[];\n};\n\n/**\n * Detects the locale from the request headers.\n *\n * Headers are provided by the browser/client and can be used to determine the user's preferred language.\n * This function intersects the user's `Accept-Language` header with the application's available locales.\n */\nexport const localeDetector = (\n headers: Record<string, string | undefined>,\n availableLocales?: Locale[],\n defaultLocale?: Locale\n): DeclaredLocales => {\n const acceptLanguageHeader = headers['accept-language'];\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales as string[]\n );\n\n return localeResolver(\n preferredLocaleStrings as Locale[],\n availableLocales,\n defaultLocale\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"localeDetector.cjs","names":["localeResolver"],"sources":["../../../src/localization/localeDetector.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport { localeResolver } from './localeResolver';\n\n/**\n * Constants\n */\nconst LANGUAGE_FORMAT_REGULAR_EXPRESSION =\n /^\\s*([^\\s\\-;]+)(?:-([^\\s;]+))?\\s*(?:;(.*))?$/;\nconst DEFAULT_QUALITY_SCORE = 1;\n\n/**\n * Upper bound of the memoization caches below. Browsers send a handful of\n * distinct `Accept-Language` values, so a small cache holds the working set.\n */\nconst MAX_CACHE_ENTRIES = 512;\n\n/**\n * Longer headers are resolved without being memoized, so a client sending\n * arbitrary headers cannot grow the cache memory.\n */\nconst MAX_CACHEABLE_HEADER_LENGTH = 128;\n\n/**\n * Enumeration for specificity weights.\n * Higher values indicate a more precise match.\n */\nenum SpecificityWeight {\n None = 0,\n Broad = 1, // Matches prefix (e.g., 'en' matches 'en-US')\n Prefix = 2, // Matches prefix in reverse (e.g., 'en-US' matches 'en')\n Exact = 4, // Matches exact string (e.g., 'en-US' matches 'en-US')\n}\n\n/**\n * Represents a parsed language tag from the header.\n */\ntype LanguagePreference = {\n languageCode: string;\n regionCode?: string;\n fullLocale: string;\n /** `fullLocale` lower-cased once, for case-insensitive matching */\n fullLocaleLower: string;\n /** `languageCode` lower-cased once, for case-insensitive matching */\n languageCodeLower: string;\n qualityScore: number;\n originalIndex: number;\n};\n\n/**\n * Represents the result of matching a requested language against an available language.\n */\ntype MatchResult = {\n providedIndex: number;\n headerIndex: number;\n qualityScore: number;\n specificityScore: number;\n};\n\n/**\n * Inserts an entry in a bounded cache, evicting the oldest entry when full.\n */\nconst setBoundedCacheEntry = <Value>(\n cache: Map<string, Value>,\n key: string,\n value: Value\n): Value => {\n if (cache.size >= MAX_CACHE_ENTRIES) {\n cache.delete(cache.keys().next().value as string);\n }\n\n cache.set(key, value);\n\n return value;\n};\n\n/**\n * Parses a single language tag string from the Accept-Language header.\n * Example input: \"en-US;q=0.8\"\n */\nconst parseLanguageTag = (\n tagString: string,\n index: number\n): LanguagePreference | null => {\n const match = LANGUAGE_FORMAT_REGULAR_EXPRESSION.exec(tagString);\n if (!match) {\n return null;\n }\n\n const languageCode = match[1] as string;\n const regionCode = match[2];\n const parameters = match[3];\n\n // Construct the full locale string (e.g., \"en-US\" or \"en\")\n const fullLocale = regionCode\n ? `${languageCode}-${regionCode}`\n : languageCode;\n\n let qualityScore = DEFAULT_QUALITY_SCORE;\n\n // Parse parameters to find the quality score (\"q\")\n if (parameters) {\n const parameterList = parameters.split(';');\n\n for (const parameter of parameterList) {\n const [key, value] = parameter.split('=');\n\n if (key === 'q') {\n qualityScore = parseFloat(value as string);\n }\n }\n }\n\n return {\n languageCode,\n regionCode,\n qualityScore,\n originalIndex: index,\n fullLocale,\n fullLocaleLower: fullLocale.toLowerCase(),\n languageCodeLower: languageCode.toLowerCase(),\n };\n};\n\n/**\n * Parsed available languages. They come from the configuration, so the same\n * few tags are parsed on every request: parse each one once.\n */\nconst availableLanguageCache = new Map<string, LanguagePreference | null>();\n\n/**\n * Parses an available language, reusing the previous parse of the same tag.\n */\nconst parseAvailableLanguage = (\n language: string\n): LanguagePreference | null => {\n const cachedLanguage = availableLanguageCache.get(language);\n\n if (cachedLanguage !== undefined) return cachedLanguage;\n\n return setBoundedCacheEntry(\n availableLanguageCache,\n language,\n parseLanguageTag(language, 0)\n );\n};\n\n/**\n * Parses the full Accept-Language header into a list of language preferences.\n */\nconst parseAcceptLanguageHeader = (\n headerValue: string\n): LanguagePreference[] => {\n const rawTags = headerValue.split(',');\n const preferences: LanguagePreference[] = [];\n\n for (let index = 0; index < rawTags.length; index++) {\n const tag = rawTags[index]?.trim();\n\n if (tag) {\n const parsedLanguage = parseLanguageTag(tag, index);\n\n if (parsedLanguage) {\n preferences.push(parsedLanguage);\n }\n }\n }\n\n return preferences;\n};\n\n/**\n * Determines how well a specific available language matches a requested language preference.\n */\nconst calculateMatchSpecificity = (\n parsedProvided: LanguagePreference,\n preference: LanguagePreference,\n providedIndex: number\n): MatchResult | null => {\n let specificityScore = SpecificityWeight.None;\n\n if (preference.fullLocaleLower === parsedProvided.fullLocaleLower) {\n specificityScore |= SpecificityWeight.Exact;\n } else if (preference.languageCodeLower === parsedProvided.fullLocaleLower) {\n specificityScore |= SpecificityWeight.Prefix;\n } else if (preference.fullLocaleLower === parsedProvided.languageCodeLower) {\n specificityScore |= SpecificityWeight.Broad;\n } else if (preference.fullLocale !== '*') {\n return null;\n }\n\n return {\n providedIndex,\n headerIndex: preference.originalIndex,\n qualityScore: preference.qualityScore,\n specificityScore,\n };\n};\n\n/**\n * Finds the best matching preference from the header for a specific available language.\n */\nconst getBestMatchForLanguage = (\n providedLanguage: string,\n acceptedPreferences: LanguagePreference[],\n providedIndex: number\n): MatchResult => {\n // Initialize with a non-match priority\n let bestMatch: MatchResult = {\n headerIndex: -1,\n qualityScore: 0,\n specificityScore: 0,\n providedIndex,\n };\n\n const parsedProvided = parseAvailableLanguage(providedLanguage);\n\n if (!parsedProvided) {\n return bestMatch;\n }\n\n for (const preference of acceptedPreferences) {\n const matchSpec = calculateMatchSpecificity(\n parsedProvided,\n preference,\n providedIndex\n );\n\n if (matchSpec) {\n // Compare current best match with new match\n const scoreDifference =\n bestMatch.specificityScore - matchSpec.specificityScore ||\n bestMatch.qualityScore - matchSpec.qualityScore ||\n bestMatch.headerIndex - matchSpec.headerIndex;\n\n // If the new match is better (difference < 0), update priority\n if (scoreDifference < 0) {\n bestMatch = matchSpec;\n }\n }\n }\n\n return bestMatch;\n};\n\n/**\n * Comparator function to sort MatchResults.\n * Order: Quality (desc) -> Specificity (desc) -> Header Order (asc) -> Provided Order (asc)\n */\nconst compareMatchResults = (a: MatchResult, b: MatchResult): number => {\n return (\n b.qualityScore - a.qualityScore ||\n b.specificityScore - a.specificityScore ||\n a.headerIndex - b.headerIndex ||\n a.providedIndex - b.providedIndex ||\n 0\n );\n};\n\n/**\n * Derives the list of preferred languages based on the Accept-Language header\n * and an optional list of available languages.\n */\nexport const getPreferredLanguages = (\n acceptHeader: string | undefined,\n availableLanguages?: string[]\n): string[] => {\n // RFC 2616 sec 14.4: no header implies '*'\n const headerValue = acceptHeader === undefined ? '*' : acceptHeader || '';\n const acceptedPreferences = parseAcceptLanguageHeader(headerValue);\n\n // If no specific languages are provided to filter against, return the header languages sorted by quality\n if (!availableLanguages) {\n return acceptedPreferences\n .filter((preference) => preference.qualityScore > 0)\n .sort((a, b) => b.qualityScore - a.qualityScore) // Simple sort by quality\n .map((preference) => preference.fullLocale);\n }\n\n // Map available languages to their match priority against the header\n const matchResults = availableLanguages.map((language, index) =>\n getBestMatchForLanguage(language, acceptedPreferences, index)\n );\n\n return matchResults\n .filter((result) => result.qualityScore > 0)\n .sort(compareMatchResults)\n .map((result) => availableLanguages[result.providedIndex]) as string[];\n};\n\n/**\n * Detected locales, keyed by header, available locales and default locale.\n */\nconst detectedLocaleCache = new Map<string, DeclaredLocales>();\n\n/**\n * Detects the locale from the request headers.\n *\n * Headers are provided by the browser/client and can be used to determine the user's preferred language.\n * This function intersects the user's `Accept-Language` header with the application's available locales.\n *\n * The result is memoized, as it only depends on its arguments.\n */\nexport const localeDetector = (\n headers: Record<string, string | undefined>,\n availableLocales?: Locale[],\n defaultLocale?: Locale\n): DeclaredLocales => {\n const acceptLanguageHeader = headers['accept-language'];\n\n const isCacheable =\n (acceptLanguageHeader?.length ?? 0) <= MAX_CACHEABLE_HEADER_LENGTH;\n\n // A missing header (`*`) and an empty header resolve differently\n const cacheKey = isCacheable\n ? `${acceptLanguageHeader === undefined ? '\\u0000' : `=${acceptLanguageHeader}`}\\u0001${availableLocales?.join(',') ?? '\\u0000'}\\u0001${defaultLocale ?? '\\u0000'}`\n : undefined;\n\n if (cacheKey !== undefined) {\n const cachedLocale = detectedLocaleCache.get(cacheKey);\n\n if (cachedLocale !== undefined) return cachedLocale;\n }\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales as string[]\n );\n\n const detectedLocale = localeResolver(\n preferredLocaleStrings as Locale[],\n availableLocales,\n defaultLocale\n );\n\n if (cacheKey !== undefined) {\n setBoundedCacheEntry(detectedLocaleCache, cacheKey, detectedLocale);\n }\n\n return detectedLocale;\n};\n"],"mappings":"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"}
|
|
@@ -7,6 +7,24 @@ import { localeResolver } from "./localeResolver.mjs";
|
|
|
7
7
|
const LANGUAGE_FORMAT_REGULAR_EXPRESSION = /^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$/;
|
|
8
8
|
const DEFAULT_QUALITY_SCORE = 1;
|
|
9
9
|
/**
|
|
10
|
+
* Upper bound of the memoization caches below. Browsers send a handful of
|
|
11
|
+
* distinct `Accept-Language` values, so a small cache holds the working set.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_CACHE_ENTRIES = 512;
|
|
14
|
+
/**
|
|
15
|
+
* Longer headers are resolved without being memoized, so a client sending
|
|
16
|
+
* arbitrary headers cannot grow the cache memory.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_CACHEABLE_HEADER_LENGTH = 128;
|
|
19
|
+
/**
|
|
20
|
+
* Inserts an entry in a bounded cache, evicting the oldest entry when full.
|
|
21
|
+
*/
|
|
22
|
+
const setBoundedCacheEntry = (cache, key, value) => {
|
|
23
|
+
if (cache.size >= MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value);
|
|
24
|
+
cache.set(key, value);
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
10
28
|
* Parses a single language tag string from the Accept-Language header.
|
|
11
29
|
* Example input: "en-US;q=0.8"
|
|
12
30
|
*/
|
|
@@ -30,11 +48,26 @@ const parseLanguageTag = (tagString, index) => {
|
|
|
30
48
|
regionCode,
|
|
31
49
|
qualityScore,
|
|
32
50
|
originalIndex: index,
|
|
33
|
-
fullLocale
|
|
51
|
+
fullLocale,
|
|
52
|
+
fullLocaleLower: fullLocale.toLowerCase(),
|
|
53
|
+
languageCodeLower: languageCode.toLowerCase()
|
|
34
54
|
};
|
|
35
55
|
};
|
|
36
56
|
/**
|
|
37
|
-
*
|
|
57
|
+
* Parsed available languages. They come from the configuration, so the same
|
|
58
|
+
* few tags are parsed on every request: parse each one once.
|
|
59
|
+
*/
|
|
60
|
+
const availableLanguageCache = /* @__PURE__ */ new Map();
|
|
61
|
+
/**
|
|
62
|
+
* Parses an available language, reusing the previous parse of the same tag.
|
|
63
|
+
*/
|
|
64
|
+
const parseAvailableLanguage = (language) => {
|
|
65
|
+
const cachedLanguage = availableLanguageCache.get(language);
|
|
66
|
+
if (cachedLanguage !== void 0) return cachedLanguage;
|
|
67
|
+
return setBoundedCacheEntry(availableLanguageCache, language, parseLanguageTag(language, 0));
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Parses the full Accept-Language header into a list of language preferences.
|
|
38
71
|
*/
|
|
39
72
|
const parseAcceptLanguageHeader = (headerValue) => {
|
|
40
73
|
const rawTags = headerValue.split(",");
|
|
@@ -49,19 +82,13 @@ const parseAcceptLanguageHeader = (headerValue) => {
|
|
|
49
82
|
return preferences;
|
|
50
83
|
};
|
|
51
84
|
/**
|
|
52
|
-
*
|
|
85
|
+
* Determines how well a specific available language matches a requested language preference.
|
|
53
86
|
*/
|
|
54
|
-
const calculateMatchSpecificity = (
|
|
55
|
-
const parsedProvided = parseLanguageTag(providedLanguage, providedIndex);
|
|
56
|
-
if (!parsedProvided) return null;
|
|
87
|
+
const calculateMatchSpecificity = (parsedProvided, preference, providedIndex) => {
|
|
57
88
|
let specificityScore = 0;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const providedPrefixLower = parsedProvided.languageCode.toLowerCase();
|
|
62
|
-
if (preferenceFullLower === providedFullLower) specificityScore |= 4;
|
|
63
|
-
else if (preferencePrefixLower === providedFullLower) specificityScore |= 2;
|
|
64
|
-
else if (preferenceFullLower === providedPrefixLower) specificityScore |= 1;
|
|
89
|
+
if (preference.fullLocaleLower === parsedProvided.fullLocaleLower) specificityScore |= 4;
|
|
90
|
+
else if (preference.languageCodeLower === parsedProvided.fullLocaleLower) specificityScore |= 2;
|
|
91
|
+
else if (preference.fullLocaleLower === parsedProvided.languageCodeLower) specificityScore |= 1;
|
|
65
92
|
else if (preference.fullLocale !== "*") return null;
|
|
66
93
|
return {
|
|
67
94
|
providedIndex,
|
|
@@ -71,7 +98,7 @@ const calculateMatchSpecificity = (providedLanguage, preference, providedIndex)
|
|
|
71
98
|
};
|
|
72
99
|
};
|
|
73
100
|
/**
|
|
74
|
-
*
|
|
101
|
+
* Finds the best matching preference from the header for a specific available language.
|
|
75
102
|
*/
|
|
76
103
|
const getBestMatchForLanguage = (providedLanguage, acceptedPreferences, providedIndex) => {
|
|
77
104
|
let bestMatch = {
|
|
@@ -80,8 +107,10 @@ const getBestMatchForLanguage = (providedLanguage, acceptedPreferences, provided
|
|
|
80
107
|
specificityScore: 0,
|
|
81
108
|
providedIndex
|
|
82
109
|
};
|
|
110
|
+
const parsedProvided = parseAvailableLanguage(providedLanguage);
|
|
111
|
+
if (!parsedProvided) return bestMatch;
|
|
83
112
|
for (const preference of acceptedPreferences) {
|
|
84
|
-
const matchSpec = calculateMatchSpecificity(
|
|
113
|
+
const matchSpec = calculateMatchSpecificity(parsedProvided, preference, providedIndex);
|
|
85
114
|
if (matchSpec) {
|
|
86
115
|
if ((bestMatch.specificityScore - matchSpec.specificityScore || bestMatch.qualityScore - matchSpec.qualityScore || bestMatch.headerIndex - matchSpec.headerIndex) < 0) bestMatch = matchSpec;
|
|
87
116
|
}
|
|
@@ -89,12 +118,8 @@ const getBestMatchForLanguage = (providedLanguage, acceptedPreferences, provided
|
|
|
89
118
|
return bestMatch;
|
|
90
119
|
};
|
|
91
120
|
/**
|
|
92
|
-
* Comparator function to sort
|
|
93
|
-
*
|
|
94
|
-
* 1. Quality Score (Descending)
|
|
95
|
-
* 2. Specificity Score (Descending)
|
|
96
|
-
* 3. Order in Header (Ascending - lower index is better)
|
|
97
|
-
* 4. Order in Provided List (Ascending)
|
|
121
|
+
* Comparator function to sort MatchResults.
|
|
122
|
+
* Order: Quality (desc) -> Specificity (desc) -> Header Order (asc) -> Provided Order (asc)
|
|
98
123
|
*/
|
|
99
124
|
const compareMatchResults = (a, b) => {
|
|
100
125
|
return b.qualityScore - a.qualityScore || b.specificityScore - a.specificityScore || a.headerIndex - b.headerIndex || a.providedIndex - b.providedIndex || 0;
|
|
@@ -109,15 +134,28 @@ const getPreferredLanguages = (acceptHeader, availableLanguages) => {
|
|
|
109
134
|
return availableLanguages.map((language, index) => getBestMatchForLanguage(language, acceptedPreferences, index)).filter((result) => result.qualityScore > 0).sort(compareMatchResults).map((result) => availableLanguages[result.providedIndex]);
|
|
110
135
|
};
|
|
111
136
|
/**
|
|
137
|
+
* Detected locales, keyed by header, available locales and default locale.
|
|
138
|
+
*/
|
|
139
|
+
const detectedLocaleCache = /* @__PURE__ */ new Map();
|
|
140
|
+
/**
|
|
112
141
|
* Detects the locale from the request headers.
|
|
113
142
|
*
|
|
114
143
|
* Headers are provided by the browser/client and can be used to determine the user's preferred language.
|
|
115
144
|
* This function intersects the user's `Accept-Language` header with the application's available locales.
|
|
145
|
+
*
|
|
146
|
+
* The result is memoized, as it only depends on its arguments.
|
|
116
147
|
*/
|
|
117
148
|
const localeDetector = (headers, availableLocales, defaultLocale) => {
|
|
118
149
|
const acceptLanguageHeader = headers["accept-language"];
|
|
150
|
+
const cacheKey = (acceptLanguageHeader?.length ?? 0) <= MAX_CACHEABLE_HEADER_LENGTH ? `${acceptLanguageHeader === void 0 ? "\0" : `=${acceptLanguageHeader}`}\u0001${availableLocales?.join(",") ?? "\0"}\u0001${defaultLocale ?? "\0"}` : void 0;
|
|
151
|
+
if (cacheKey !== void 0) {
|
|
152
|
+
const cachedLocale = detectedLocaleCache.get(cacheKey);
|
|
153
|
+
if (cachedLocale !== void 0) return cachedLocale;
|
|
154
|
+
}
|
|
119
155
|
const preferredLocaleStrings = getPreferredLanguages(acceptLanguageHeader, availableLocales);
|
|
120
|
-
|
|
156
|
+
const detectedLocale = localeResolver(preferredLocaleStrings, availableLocales, defaultLocale);
|
|
157
|
+
if (cacheKey !== void 0) setBoundedCacheEntry(detectedLocaleCache, cacheKey, detectedLocale);
|
|
158
|
+
return detectedLocale;
|
|
121
159
|
};
|
|
122
160
|
|
|
123
161
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localeDetector.mjs","names":[],"sources":["../../../src/localization/localeDetector.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport { localeResolver } from './localeResolver';\n\n/**\n * Constants\n */\nconst LANGUAGE_FORMAT_REGULAR_EXPRESSION =\n /^\\s*([^\\s\\-;]+)(?:-([^\\s;]+))?\\s*(?:;(.*))?$/;\nconst DEFAULT_QUALITY_SCORE = 1;\n\n/**\n * Enumeration for specificity weights.\n * Higher values indicate a more precise match.\n */\nenum SpecificityWeight {\n None = 0,\n Broad = 1, // Matches prefix (e.g., 'en' matches 'en-US')\n Prefix = 2, // Matches prefix in reverse (e.g., 'en-US' matches 'en')\n Exact = 4, // Matches exact string (e.g., 'en-US' matches 'en-US')\n}\n\n/**\n * Represents a parsed language tag from the header.\n */\ntype LanguagePreference = {\n languageCode: string;\n regionCode?: string;\n fullLocale: string;\n qualityScore: number;\n originalIndex: number;\n};\n\n/**\n * Represents the result of matching a requested language against an available language.\n */\ntype MatchResult = {\n providedIndex: number;\n headerIndex: number;\n qualityScore: number;\n specificityScore: number;\n};\n\n/**\n * Parses a single language tag string from the Accept-Language header.\n * Example input: \"en-US;q=0.8\"\n */\nconst parseLanguageTag = (\n tagString: string,\n index: number\n): LanguagePreference | null => {\n const match = LANGUAGE_FORMAT_REGULAR_EXPRESSION.exec(tagString);\n if (!match) {\n return null;\n }\n\n const languageCode = match[1] as string;\n const regionCode = match[2];\n const parameters = match[3];\n\n // Construct the full locale string (e.g., \"en-US\" or \"en\")\n const fullLocale = regionCode\n ? `${languageCode}-${regionCode}`\n : languageCode;\n\n let qualityScore = DEFAULT_QUALITY_SCORE;\n\n // Parse parameters to find the quality score (\"q\")\n if (parameters) {\n const parameterList = parameters.split(';');\n\n for (const parameter of parameterList) {\n const [key, value] = parameter.split('=');\n\n if (key === 'q') {\n qualityScore = parseFloat(value as string);\n }\n }\n }\n\n return {\n languageCode,\n regionCode,\n qualityScore,\n originalIndex: index,\n fullLocale,\n };\n};\n\n/**\n * Parses the entire Accept-Language header string into a list of preferences.\n */\nconst parseAcceptLanguageHeader = (\n headerValue: string\n): LanguagePreference[] => {\n const rawTags = headerValue.split(',');\n const preferences: LanguagePreference[] = [];\n\n for (let index = 0; index < rawTags.length; index++) {\n const tag = rawTags[index]?.trim();\n\n if (tag) {\n const parsedLanguage = parseLanguageTag(tag, index);\n\n if (parsedLanguage) {\n preferences.push(parsedLanguage);\n }\n }\n }\n\n return preferences;\n};\n\n/**\n * Calculates the specificity of a match between a provided language and a requested preference.\n */\nconst calculateMatchSpecificity = (\n providedLanguage: string,\n preference: LanguagePreference,\n providedIndex: number\n): MatchResult | null => {\n const parsedProvided = parseLanguageTag(providedLanguage, providedIndex);\n if (!parsedProvided) {\n return null;\n }\n\n let specificityScore = SpecificityWeight.None;\n\n const preferenceFullLower = preference.fullLocale.toLowerCase();\n const preferencePrefixLower = preference.languageCode.toLowerCase();\n const providedFullLower = parsedProvided.fullLocale.toLowerCase();\n const providedPrefixLower = parsedProvided.languageCode.toLowerCase();\n\n if (preferenceFullLower === providedFullLower) {\n specificityScore |= SpecificityWeight.Exact;\n } else if (preferencePrefixLower === providedFullLower) {\n specificityScore |= SpecificityWeight.Prefix;\n } else if (preferenceFullLower === providedPrefixLower) {\n specificityScore |= SpecificityWeight.Broad;\n } else if (preference.fullLocale !== '*') {\n return null;\n }\n\n return {\n providedIndex,\n headerIndex: preference.originalIndex,\n qualityScore: preference.qualityScore,\n specificityScore,\n };\n};\n\n/**\n * Determines the best match for a specific available language against the list of user accepted languages.\n */\nconst getBestMatchForLanguage = (\n providedLanguage: string,\n acceptedPreferences: LanguagePreference[],\n providedIndex: number\n): MatchResult => {\n // Initialize with a non-match priority\n let bestMatch: MatchResult = {\n headerIndex: -1,\n qualityScore: 0,\n specificityScore: 0,\n providedIndex,\n };\n\n for (const preference of acceptedPreferences) {\n const matchSpec = calculateMatchSpecificity(\n providedLanguage,\n preference,\n providedIndex\n );\n\n if (matchSpec) {\n // Compare current best match with new match\n const scoreDifference =\n bestMatch.specificityScore - matchSpec.specificityScore ||\n bestMatch.qualityScore - matchSpec.qualityScore ||\n bestMatch.headerIndex - matchSpec.headerIndex;\n\n // If the new match is better (difference < 0), update priority\n if (scoreDifference < 0) {\n bestMatch = matchSpec;\n }\n }\n }\n\n return bestMatch;\n};\n\n/**\n * Comparator function to sort language matches.\n * Sorting order:\n * 1. Quality Score (Descending)\n * 2. Specificity Score (Descending)\n * 3. Order in Header (Ascending - lower index is better)\n * 4. Order in Provided List (Ascending)\n */\nconst compareMatchResults = (a: MatchResult, b: MatchResult): number => {\n return (\n b.qualityScore - a.qualityScore ||\n b.specificityScore - a.specificityScore ||\n a.headerIndex - b.headerIndex ||\n a.providedIndex - b.providedIndex ||\n 0\n );\n};\n\n/**\n * Derives the list of preferred languages based on the Accept-Language header\n * and an optional list of available languages.\n */\nexport const getPreferredLanguages = (\n acceptHeader: string | undefined,\n availableLanguages?: string[]\n): string[] => {\n // RFC 2616 sec 14.4: no header implies '*'\n const headerValue = acceptHeader === undefined ? '*' : acceptHeader || '';\n const acceptedPreferences = parseAcceptLanguageHeader(headerValue);\n\n // If no specific languages are provided to filter against, return the header languages sorted by quality\n if (!availableLanguages) {\n return acceptedPreferences\n .filter((preference) => preference.qualityScore > 0)\n .sort((a, b) => b.qualityScore - a.qualityScore) // Simple sort by quality\n .map((preference) => preference.fullLocale);\n }\n\n // Map available languages to their match priority against the header\n const matchResults = availableLanguages.map((language, index) =>\n getBestMatchForLanguage(language, acceptedPreferences, index)\n );\n\n return matchResults\n .filter((result) => result.qualityScore > 0)\n .sort(compareMatchResults)\n .map((result) => availableLanguages[result.providedIndex]) as string[];\n};\n\n/**\n * Detects the locale from the request headers.\n *\n * Headers are provided by the browser/client and can be used to determine the user's preferred language.\n * This function intersects the user's `Accept-Language` header with the application's available locales.\n */\nexport const localeDetector = (\n headers: Record<string, string | undefined>,\n availableLocales?: Locale[],\n defaultLocale?: Locale\n): DeclaredLocales => {\n const acceptLanguageHeader = headers['accept-language'];\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales as string[]\n );\n\n return localeResolver(\n preferredLocaleStrings as Locale[],\n availableLocales,\n defaultLocale\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"localeDetector.mjs","names":[],"sources":["../../../src/localization/localeDetector.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport { localeResolver } from './localeResolver';\n\n/**\n * Constants\n */\nconst LANGUAGE_FORMAT_REGULAR_EXPRESSION =\n /^\\s*([^\\s\\-;]+)(?:-([^\\s;]+))?\\s*(?:;(.*))?$/;\nconst DEFAULT_QUALITY_SCORE = 1;\n\n/**\n * Upper bound of the memoization caches below. Browsers send a handful of\n * distinct `Accept-Language` values, so a small cache holds the working set.\n */\nconst MAX_CACHE_ENTRIES = 512;\n\n/**\n * Longer headers are resolved without being memoized, so a client sending\n * arbitrary headers cannot grow the cache memory.\n */\nconst MAX_CACHEABLE_HEADER_LENGTH = 128;\n\n/**\n * Enumeration for specificity weights.\n * Higher values indicate a more precise match.\n */\nenum SpecificityWeight {\n None = 0,\n Broad = 1, // Matches prefix (e.g., 'en' matches 'en-US')\n Prefix = 2, // Matches prefix in reverse (e.g., 'en-US' matches 'en')\n Exact = 4, // Matches exact string (e.g., 'en-US' matches 'en-US')\n}\n\n/**\n * Represents a parsed language tag from the header.\n */\ntype LanguagePreference = {\n languageCode: string;\n regionCode?: string;\n fullLocale: string;\n /** `fullLocale` lower-cased once, for case-insensitive matching */\n fullLocaleLower: string;\n /** `languageCode` lower-cased once, for case-insensitive matching */\n languageCodeLower: string;\n qualityScore: number;\n originalIndex: number;\n};\n\n/**\n * Represents the result of matching a requested language against an available language.\n */\ntype MatchResult = {\n providedIndex: number;\n headerIndex: number;\n qualityScore: number;\n specificityScore: number;\n};\n\n/**\n * Inserts an entry in a bounded cache, evicting the oldest entry when full.\n */\nconst setBoundedCacheEntry = <Value>(\n cache: Map<string, Value>,\n key: string,\n value: Value\n): Value => {\n if (cache.size >= MAX_CACHE_ENTRIES) {\n cache.delete(cache.keys().next().value as string);\n }\n\n cache.set(key, value);\n\n return value;\n};\n\n/**\n * Parses a single language tag string from the Accept-Language header.\n * Example input: \"en-US;q=0.8\"\n */\nconst parseLanguageTag = (\n tagString: string,\n index: number\n): LanguagePreference | null => {\n const match = LANGUAGE_FORMAT_REGULAR_EXPRESSION.exec(tagString);\n if (!match) {\n return null;\n }\n\n const languageCode = match[1] as string;\n const regionCode = match[2];\n const parameters = match[3];\n\n // Construct the full locale string (e.g., \"en-US\" or \"en\")\n const fullLocale = regionCode\n ? `${languageCode}-${regionCode}`\n : languageCode;\n\n let qualityScore = DEFAULT_QUALITY_SCORE;\n\n // Parse parameters to find the quality score (\"q\")\n if (parameters) {\n const parameterList = parameters.split(';');\n\n for (const parameter of parameterList) {\n const [key, value] = parameter.split('=');\n\n if (key === 'q') {\n qualityScore = parseFloat(value as string);\n }\n }\n }\n\n return {\n languageCode,\n regionCode,\n qualityScore,\n originalIndex: index,\n fullLocale,\n fullLocaleLower: fullLocale.toLowerCase(),\n languageCodeLower: languageCode.toLowerCase(),\n };\n};\n\n/**\n * Parsed available languages. They come from the configuration, so the same\n * few tags are parsed on every request: parse each one once.\n */\nconst availableLanguageCache = new Map<string, LanguagePreference | null>();\n\n/**\n * Parses an available language, reusing the previous parse of the same tag.\n */\nconst parseAvailableLanguage = (\n language: string\n): LanguagePreference | null => {\n const cachedLanguage = availableLanguageCache.get(language);\n\n if (cachedLanguage !== undefined) return cachedLanguage;\n\n return setBoundedCacheEntry(\n availableLanguageCache,\n language,\n parseLanguageTag(language, 0)\n );\n};\n\n/**\n * Parses the full Accept-Language header into a list of language preferences.\n */\nconst parseAcceptLanguageHeader = (\n headerValue: string\n): LanguagePreference[] => {\n const rawTags = headerValue.split(',');\n const preferences: LanguagePreference[] = [];\n\n for (let index = 0; index < rawTags.length; index++) {\n const tag = rawTags[index]?.trim();\n\n if (tag) {\n const parsedLanguage = parseLanguageTag(tag, index);\n\n if (parsedLanguage) {\n preferences.push(parsedLanguage);\n }\n }\n }\n\n return preferences;\n};\n\n/**\n * Determines how well a specific available language matches a requested language preference.\n */\nconst calculateMatchSpecificity = (\n parsedProvided: LanguagePreference,\n preference: LanguagePreference,\n providedIndex: number\n): MatchResult | null => {\n let specificityScore = SpecificityWeight.None;\n\n if (preference.fullLocaleLower === parsedProvided.fullLocaleLower) {\n specificityScore |= SpecificityWeight.Exact;\n } else if (preference.languageCodeLower === parsedProvided.fullLocaleLower) {\n specificityScore |= SpecificityWeight.Prefix;\n } else if (preference.fullLocaleLower === parsedProvided.languageCodeLower) {\n specificityScore |= SpecificityWeight.Broad;\n } else if (preference.fullLocale !== '*') {\n return null;\n }\n\n return {\n providedIndex,\n headerIndex: preference.originalIndex,\n qualityScore: preference.qualityScore,\n specificityScore,\n };\n};\n\n/**\n * Finds the best matching preference from the header for a specific available language.\n */\nconst getBestMatchForLanguage = (\n providedLanguage: string,\n acceptedPreferences: LanguagePreference[],\n providedIndex: number\n): MatchResult => {\n // Initialize with a non-match priority\n let bestMatch: MatchResult = {\n headerIndex: -1,\n qualityScore: 0,\n specificityScore: 0,\n providedIndex,\n };\n\n const parsedProvided = parseAvailableLanguage(providedLanguage);\n\n if (!parsedProvided) {\n return bestMatch;\n }\n\n for (const preference of acceptedPreferences) {\n const matchSpec = calculateMatchSpecificity(\n parsedProvided,\n preference,\n providedIndex\n );\n\n if (matchSpec) {\n // Compare current best match with new match\n const scoreDifference =\n bestMatch.specificityScore - matchSpec.specificityScore ||\n bestMatch.qualityScore - matchSpec.qualityScore ||\n bestMatch.headerIndex - matchSpec.headerIndex;\n\n // If the new match is better (difference < 0), update priority\n if (scoreDifference < 0) {\n bestMatch = matchSpec;\n }\n }\n }\n\n return bestMatch;\n};\n\n/**\n * Comparator function to sort MatchResults.\n * Order: Quality (desc) -> Specificity (desc) -> Header Order (asc) -> Provided Order (asc)\n */\nconst compareMatchResults = (a: MatchResult, b: MatchResult): number => {\n return (\n b.qualityScore - a.qualityScore ||\n b.specificityScore - a.specificityScore ||\n a.headerIndex - b.headerIndex ||\n a.providedIndex - b.providedIndex ||\n 0\n );\n};\n\n/**\n * Derives the list of preferred languages based on the Accept-Language header\n * and an optional list of available languages.\n */\nexport const getPreferredLanguages = (\n acceptHeader: string | undefined,\n availableLanguages?: string[]\n): string[] => {\n // RFC 2616 sec 14.4: no header implies '*'\n const headerValue = acceptHeader === undefined ? '*' : acceptHeader || '';\n const acceptedPreferences = parseAcceptLanguageHeader(headerValue);\n\n // If no specific languages are provided to filter against, return the header languages sorted by quality\n if (!availableLanguages) {\n return acceptedPreferences\n .filter((preference) => preference.qualityScore > 0)\n .sort((a, b) => b.qualityScore - a.qualityScore) // Simple sort by quality\n .map((preference) => preference.fullLocale);\n }\n\n // Map available languages to their match priority against the header\n const matchResults = availableLanguages.map((language, index) =>\n getBestMatchForLanguage(language, acceptedPreferences, index)\n );\n\n return matchResults\n .filter((result) => result.qualityScore > 0)\n .sort(compareMatchResults)\n .map((result) => availableLanguages[result.providedIndex]) as string[];\n};\n\n/**\n * Detected locales, keyed by header, available locales and default locale.\n */\nconst detectedLocaleCache = new Map<string, DeclaredLocales>();\n\n/**\n * Detects the locale from the request headers.\n *\n * Headers are provided by the browser/client and can be used to determine the user's preferred language.\n * This function intersects the user's `Accept-Language` header with the application's available locales.\n *\n * The result is memoized, as it only depends on its arguments.\n */\nexport const localeDetector = (\n headers: Record<string, string | undefined>,\n availableLocales?: Locale[],\n defaultLocale?: Locale\n): DeclaredLocales => {\n const acceptLanguageHeader = headers['accept-language'];\n\n const isCacheable =\n (acceptLanguageHeader?.length ?? 0) <= MAX_CACHEABLE_HEADER_LENGTH;\n\n // A missing header (`*`) and an empty header resolve differently\n const cacheKey = isCacheable\n ? `${acceptLanguageHeader === undefined ? '\\u0000' : `=${acceptLanguageHeader}`}\\u0001${availableLocales?.join(',') ?? '\\u0000'}\\u0001${defaultLocale ?? '\\u0000'}`\n : undefined;\n\n if (cacheKey !== undefined) {\n const cachedLocale = detectedLocaleCache.get(cacheKey);\n\n if (cachedLocale !== undefined) return cachedLocale;\n }\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales as string[]\n );\n\n const detectedLocale = localeResolver(\n preferredLocaleStrings as Locale[],\n availableLocales,\n defaultLocale\n );\n\n if (cacheKey !== undefined) {\n setBoundedCacheEntry(detectedLocaleCache, cacheKey, detectedLocale);\n }\n\n return detectedLocale;\n};\n"],"mappings":"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"}
|
|
@@ -11,6 +11,8 @@ export declare const getPreferredLanguages: (acceptHeader: string | undefined, a
|
|
|
11
11
|
*
|
|
12
12
|
* Headers are provided by the browser/client and can be used to determine the user's preferred language.
|
|
13
13
|
* This function intersects the user's `Accept-Language` header with the application's available locales.
|
|
14
|
+
*
|
|
15
|
+
* The result is memoized, as it only depends on its arguments.
|
|
14
16
|
*/
|
|
15
17
|
export declare const localeDetector: (headers: Record<string, string | undefined>, availableLocales?: Locale[], defaultLocale?: Locale) => DeclaredLocales;
|
|
16
18
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localeDetector.d.ts","names":[],"sources":["../../../src/localization/localeDetector.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"localeDetector.d.ts","names":[],"sources":["../../../src/localization/localeDetector.ts"],"mappings":";;;;;;;qBAuQa,wBAAqB,kCACA;;;;;;;;;qBAuCrB,iBAAc,SAChB,oCAAkC,mBACxB,UAAQ,gBACX,WACf"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intlayer/core",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Includes core Intlayer functions like translation, dictionary, and utility functions shared across multiple packages.",
|
|
6
6
|
"keywords": [
|
|
@@ -173,10 +173,10 @@
|
|
|
173
173
|
"typecheck": "tsc --noEmit --project tsconfig.types.json"
|
|
174
174
|
},
|
|
175
175
|
"dependencies": {
|
|
176
|
-
"@intlayer/api": "9.
|
|
177
|
-
"@intlayer/config": "9.
|
|
178
|
-
"@intlayer/dictionaries-entry": "9.
|
|
179
|
-
"@intlayer/types": "9.
|
|
176
|
+
"@intlayer/api": "9.6.0",
|
|
177
|
+
"@intlayer/config": "9.6.0",
|
|
178
|
+
"@intlayer/dictionaries-entry": "9.6.0",
|
|
179
|
+
"@intlayer/types": "9.6.0",
|
|
180
180
|
"defu": "6.1.7"
|
|
181
181
|
},
|
|
182
182
|
"devDependencies": {
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"rimraf": "6.1.3",
|
|
188
188
|
"tsdown": "0.23.0",
|
|
189
189
|
"typescript": "7.0.2",
|
|
190
|
-
"vitest": "5.0.
|
|
190
|
+
"vitest": "5.0.3"
|
|
191
191
|
},
|
|
192
192
|
"engines": {
|
|
193
193
|
"node": ">=14.18"
|