@intlayer/core 9.5.11 → 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/index.cjs +7 -2
- package/dist/cjs/interpreter/getDictionary.cjs +4 -3
- package/dist/cjs/interpreter/getDictionary.cjs.map +1 -1
- package/dist/cjs/interpreter/getDictionaryAsync.cjs +2 -2
- package/dist/cjs/interpreter/getDictionaryAsync.cjs.map +1 -1
- package/dist/cjs/interpreter/getIntlayer.cjs.map +1 -1
- package/dist/cjs/interpreter/getIntlayerAsync.cjs +10 -1
- package/dist/cjs/interpreter/getIntlayerAsync.cjs.map +1 -1
- package/dist/cjs/interpreter/index.cjs +5 -0
- package/dist/cjs/interpreter/resolveInterpreterLocale.cjs +101 -0
- package/dist/cjs/interpreter/resolveInterpreterLocale.cjs.map +1 -0
- package/dist/cjs/localization/getLocaleFromRequest.cjs +1 -1
- package/dist/cjs/localization/localeDetector.cjs +60 -22
- package/dist/cjs/localization/localeDetector.cjs.map +1 -1
- package/dist/cjs/localization/resolveBrowserLocale.cjs +1 -1
- package/dist/cjs/localization/rewriteMatch.cjs +11 -1
- package/dist/cjs/localization/rewriteMatch.cjs.map +1 -1
- package/dist/cjs/markdown/compiler.cjs.map +1 -1
- package/dist/cjs/markdown/constants.cjs.map +1 -1
- package/dist/cjs/transpiler/markdown/getMarkdownMetadata.cjs +29 -9
- package/dist/cjs/transpiler/markdown/getMarkdownMetadata.cjs.map +1 -1
- package/dist/cjs/transpiler/markdown/markdown.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/cjs/utils/intl.cjs.map +1 -1
- package/dist/esm/index.mjs +4 -3
- package/dist/esm/interpreter/getDictionary.mjs +4 -3
- package/dist/esm/interpreter/getDictionary.mjs.map +1 -1
- package/dist/esm/interpreter/getDictionaryAsync.mjs +2 -2
- package/dist/esm/interpreter/getDictionaryAsync.mjs.map +1 -1
- package/dist/esm/interpreter/getIntlayer.mjs +1 -1
- package/dist/esm/interpreter/getIntlayer.mjs.map +1 -1
- package/dist/esm/interpreter/getIntlayerAsync.mjs +10 -1
- package/dist/esm/interpreter/getIntlayerAsync.mjs.map +1 -1
- package/dist/esm/interpreter/index.mjs +2 -1
- package/dist/esm/interpreter/resolveInterpreterLocale.mjs +97 -0
- package/dist/esm/interpreter/resolveInterpreterLocale.mjs.map +1 -0
- package/dist/esm/localization/getLocaleFromRequest.mjs +1 -1
- package/dist/esm/localization/localeDetector.mjs +60 -22
- package/dist/esm/localization/localeDetector.mjs.map +1 -1
- package/dist/esm/localization/resolveBrowserLocale.mjs +1 -1
- package/dist/esm/localization/rewriteMatch.mjs +11 -1
- package/dist/esm/localization/rewriteMatch.mjs.map +1 -1
- package/dist/esm/markdown/compiler.mjs +1 -1
- package/dist/esm/markdown/compiler.mjs.map +1 -1
- package/dist/esm/markdown/constants.mjs +3 -0
- package/dist/esm/markdown/constants.mjs.map +1 -1
- package/dist/esm/transpiler/markdown/getMarkdownMetadata.mjs +29 -9
- package/dist/esm/transpiler/markdown/getMarkdownMetadata.mjs.map +1 -1
- package/dist/esm/transpiler/markdown/markdown.mjs +1 -1
- package/dist/esm/transpiler/markdown/markdown.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/esm/utils/intl.mjs +1 -0
- package/dist/esm/utils/intl.mjs.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/interpreter/getIntlayerAsync.d.ts.map +1 -1
- package/dist/types/interpreter/index.d.ts +2 -1
- package/dist/types/interpreter/resolveInterpreterLocale.d.ts +63 -0
- package/dist/types/interpreter/resolveInterpreterLocale.d.ts.map +1 -0
- package/dist/types/localization/localeDetector.d.ts +2 -0
- package/dist/types/localization/localeDetector.d.ts.map +1 -1
- package/dist/types/localization/rewriteMatch.d.ts.map +1 -1
- package/dist/types/transpiler/markdown/getMarkdownMetadata.d.ts.map +1 -1
- package/package.json +6 -6
|
@@ -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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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { getLocaleFromStorageClient } from "../utils/localeStorage.mjs";
|
|
1
2
|
import { getLocaleFromDomain } from "./domainUtils.mjs";
|
|
2
3
|
import { resolveRoutingConfig } from "./getPrefix.mjs";
|
|
3
|
-
import { getLocaleFromStorageClient } from "../utils/localeStorage.mjs";
|
|
4
4
|
import { getLocaleFromPath } from "./getLocaleFromPath.mjs";
|
|
5
5
|
|
|
6
6
|
//#region src/localization/resolveBrowserLocale.ts
|
|
@@ -17,6 +17,12 @@ const getRewriteRules = (rewrite, context = "url") => {
|
|
|
17
17
|
})) };
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
+
* Compiled regexes by pattern. Patterns come from the finite set of rewrite
|
|
21
|
+
* rules, while URL localization runs per link, so compiling once pays off.
|
|
22
|
+
* The regexes have no `g` flag, so sharing them is stateless.
|
|
23
|
+
*/
|
|
24
|
+
const patternRegexCache = /* @__PURE__ */ new Map();
|
|
25
|
+
/**
|
|
20
26
|
* Converts normalized pattern to Regex.
|
|
21
27
|
* Internal syntax supports:
|
|
22
28
|
* - :param -> ([^/]+) (one segment)
|
|
@@ -25,8 +31,12 @@ const getRewriteRules = (rewrite, context = "url") => {
|
|
|
25
31
|
* - :param? -> ([^/]*) (zero or one segment)
|
|
26
32
|
*/
|
|
27
33
|
const patternToRegex = (pattern) => {
|
|
34
|
+
const cachedRegex = patternRegexCache.get(pattern);
|
|
35
|
+
if (cachedRegex) return cachedRegex;
|
|
28
36
|
const regexString = pattern.replace(/\//g, "\\/").replace(/\\\/:(?:[^/\\*+?]+)\*/g, "(?:\\/(.*))?").replace(/\\\/:(?:[^/\\*+?]+)\?/g, "(?:\\/([^\\/]+))?").replace(/:([^/\\*+?]+)\*/g, "(.*)").replace(/:([^/\\*+?]+)\?/g, "([^\\/]*)").replace(/:([^/\\*+?]+)\+/g, "(.+)").replace(/:([^/\\*+?]+)/g, "([^\\/]+)");
|
|
29
|
-
|
|
37
|
+
const regex = new RegExp(`^${regexString}$`);
|
|
38
|
+
patternRegexCache.set(pattern, regex);
|
|
39
|
+
return regex;
|
|
30
40
|
};
|
|
31
41
|
/**
|
|
32
42
|
* Replaces route parameters in a path with provided values.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rewriteMatch.mjs","names":["legacyRules"],"sources":["../../../src/localization/rewriteMatch.ts"],"sourcesContent":["import type {\n RewriteObject,\n RewriteRules,\n RoutingConfig,\n} from '@intlayer/types/config';\n\n/**\n * Normalizes legacy Record format or extracts specialized rules from RewriteObject.\n *\n * Idempotent: already-normalized rules are handed back untouched, so a caller\n * holding resolved rules can pass them on without re-normalizing.\n */\nexport const getRewriteRules = <\n const Context extends keyof RewriteObject = 'url',\n>(\n rewrite: RoutingConfig['rewrite'] | RewriteRules,\n context: Context = 'url' as Context\n): RewriteRules | undefined => {\n if (\n !rewrite ||\n (process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false')\n )\n return undefined;\n\n if (Array.isArray((rewrite as RewriteRules).rules)) {\n return rewrite as RewriteRules;\n }\n\n if ('url' in rewrite) {\n return (rewrite as RewriteObject)[context];\n }\n\n // Normalize legacy format\n const legacyRules = rewrite as Record<string, Record<string, string>>;\n\n return {\n rules: Object.entries(legacyRules).map(([canonical, localized]) => ({\n // Normalize canonical path\n canonical: canonical.startsWith('/')\n ? canonical.replace(/\\[([^\\]]+)\\]/g, ':$1')\n : `/${canonical.replace(/\\[([^\\]]+)\\]/g, ':$1')}`,\n\n // Normalize localized path\n localized: Object.fromEntries(\n Object.entries(localized).map(([locale, pattern]) => {\n const normalizedPattern = pattern?.startsWith('/')\n ? pattern.replace(/\\[([^\\]]+)\\]/g, ':$1')\n : `/${(pattern || '').replace(/\\[([^\\]]+)\\]/g, ':$1')}`;\n return [locale, normalizedPattern];\n })\n ),\n })),\n };\n};\n\n/**\n * Converts normalized pattern to Regex.\n * Internal syntax supports:\n * - :param -> ([^/]+) (one segment)\n * - :param* -> (.*) (zero or more segments)\n * - :param+ -> (.+) (one or more segments)\n * - :param? -> ([^/]*) (zero or one segment)\n */\nconst patternToRegex = (pattern: string) => {\n const regexString = pattern\n .replace(/\\//g, '\\\\/') // Escape slashes\n .replace(/\\\\\\/:(?:[^/\\\\*+?]+)\\*/g, '(?:\\\\/(.*))?') // /:param*\n .replace(/\\\\\\/:(?:[^/\\\\*+?]+)\\?/g, '(?:\\\\/([^\\\\/]+))?') // /:param?\n .replace(/:([^/\\\\*+?]+)\\*/g, '(.*)') // :param* (if no leading slash)\n .replace(/:([^/\\\\*+?]+)\\?/g, '([^\\\\/]*)') // :param? (if no leading slash)\n .replace(/:([^/\\\\*+?]+)\\+/g, '(.+)') // :param+\n .replace(/:([^/\\\\*+?]+)/g, '([^\\\\/]+)'); // :param\n\n
|
|
1
|
+
{"version":3,"file":"rewriteMatch.mjs","names":["legacyRules"],"sources":["../../../src/localization/rewriteMatch.ts"],"sourcesContent":["import type {\n RewriteObject,\n RewriteRules,\n RoutingConfig,\n} from '@intlayer/types/config';\n\n/**\n * Normalizes legacy Record format or extracts specialized rules from RewriteObject.\n *\n * Idempotent: already-normalized rules are handed back untouched, so a caller\n * holding resolved rules can pass them on without re-normalizing.\n */\nexport const getRewriteRules = <\n const Context extends keyof RewriteObject = 'url',\n>(\n rewrite: RoutingConfig['rewrite'] | RewriteRules,\n context: Context = 'url' as Context\n): RewriteRules | undefined => {\n if (\n !rewrite ||\n (process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false')\n )\n return undefined;\n\n if (Array.isArray((rewrite as RewriteRules).rules)) {\n return rewrite as RewriteRules;\n }\n\n if ('url' in rewrite) {\n return (rewrite as RewriteObject)[context];\n }\n\n // Normalize legacy format\n const legacyRules = rewrite as Record<string, Record<string, string>>;\n\n return {\n rules: Object.entries(legacyRules).map(([canonical, localized]) => ({\n // Normalize canonical path\n canonical: canonical.startsWith('/')\n ? canonical.replace(/\\[([^\\]]+)\\]/g, ':$1')\n : `/${canonical.replace(/\\[([^\\]]+)\\]/g, ':$1')}`,\n\n // Normalize localized path\n localized: Object.fromEntries(\n Object.entries(localized).map(([locale, pattern]) => {\n const normalizedPattern = pattern?.startsWith('/')\n ? pattern.replace(/\\[([^\\]]+)\\]/g, ':$1')\n : `/${(pattern || '').replace(/\\[([^\\]]+)\\]/g, ':$1')}`;\n return [locale, normalizedPattern];\n })\n ),\n })),\n };\n};\n\n/**\n * Compiled regexes by pattern. Patterns come from the finite set of rewrite\n * rules, while URL localization runs per link, so compiling once pays off.\n * The regexes have no `g` flag, so sharing them is stateless.\n */\nconst patternRegexCache = new Map<string, RegExp>();\n\n/**\n * Converts normalized pattern to Regex.\n * Internal syntax supports:\n * - :param -> ([^/]+) (one segment)\n * - :param* -> (.*) (zero or more segments)\n * - :param+ -> (.+) (one or more segments)\n * - :param? -> ([^/]*) (zero or one segment)\n */\nconst patternToRegex = (pattern: string): RegExp => {\n const cachedRegex = patternRegexCache.get(pattern);\n\n if (cachedRegex) return cachedRegex;\n\n const regexString = pattern\n .replace(/\\//g, '\\\\/') // Escape slashes\n .replace(/\\\\\\/:(?:[^/\\\\*+?]+)\\*/g, '(?:\\\\/(.*))?') // /:param*\n .replace(/\\\\\\/:(?:[^/\\\\*+?]+)\\?/g, '(?:\\\\/([^\\\\/]+))?') // /:param?\n .replace(/:([^/\\\\*+?]+)\\*/g, '(.*)') // :param* (if no leading slash)\n .replace(/:([^/\\\\*+?]+)\\?/g, '([^\\\\/]*)') // :param? (if no leading slash)\n .replace(/:([^/\\\\*+?]+)\\+/g, '(.+)') // :param+\n .replace(/:([^/\\\\*+?]+)/g, '([^\\\\/]+)'); // :param\n\n const regex = new RegExp(`^${regexString}$`);\n patternRegexCache.set(pattern, regex);\n\n return regex;\n};\n\n/**\n * Replaces route parameters in a path with provided values.\n */\nexport const fillPath = (pattern: string, params: string[]) => {\n let index = 0;\n return (\n pattern\n .replace(/:([^/\\\\*+?]+)[*+?]?/g, () => params[index++] ?? '')\n .replace(/\\/+/g, '/')\n .replace(/\\/$/, '') || '/'\n );\n};\n\n/**\n * Extract values from a URL based on a pattern.\n */\nexport const extractParams = (\n url: string,\n pattern: string\n): string[] | null => {\n const regex = patternToRegex(pattern);\n const match = url.match(regex);\n return match ? match.slice(1) : null;\n};\n"],"mappings":";;;;;;;AAYA,MAAa,mBAGX,SACA,UAAmB,UACU;CAC7B,IACE,CAAC,WACA,QAAQ,IAAI,kCACX,QAAQ,IAAI,mCAAmC,SAEjD,OAAO;CAET,IAAI,MAAM,QAAS,QAAyB,KAAK,GAC/C,OAAO;CAGT,IAAI,SAAS,SACX,OAAQ,QAA0B;CAMpC,OAAO,EACL,OAAO,OAAO,QAAQA,OAAW,CAAC,CAAC,KAAK,CAAC,WAAW,gBAAgB;EAElE,WAAW,UAAU,WAAW,GAAG,IAC/B,UAAU,QAAQ,iBAAiB,KAAK,IACxC,IAAI,UAAU,QAAQ,iBAAiB,KAAK;EAGhD,WAAW,OAAO,YAChB,OAAO,QAAQ,SAAS,CAAC,CAAC,KAAK,CAAC,QAAQ,aAAa;GAInD,OAAO,CAAC,QAHkB,SAAS,WAAW,GAAG,IAC7C,QAAQ,QAAQ,iBAAiB,KAAK,IACtC,KAAK,WAAW,GAAE,CAAE,QAAQ,iBAAiB,KAAK,GACrB;EACnC,CAAC,CACH;CACF,EAAE,EACJ;AACF;;;;;;AAOA,MAAM,oCAAoB,IAAI,IAAoB;;;;;;;;;AAUlD,MAAM,kBAAkB,YAA4B;CAClD,MAAM,cAAc,kBAAkB,IAAI,OAAO;CAEjD,IAAI,aAAa,OAAO;CAExB,MAAM,cAAc,QACjB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,0BAA0B,cAAc,CAAC,CACjD,QAAQ,0BAA0B,mBAAmB,CAAC,CACtD,QAAQ,oBAAoB,MAAM,CAAC,CACnC,QAAQ,oBAAoB,WAAW,CAAC,CACxC,QAAQ,oBAAoB,MAAM,CAAC,CACnC,QAAQ,kBAAkB,WAAW;CAExC,MAAM,QAAQ,IAAI,OAAO,IAAI,YAAY,EAAE;CAC3C,kBAAkB,IAAI,SAAS,KAAK;CAEpC,OAAO;AACT;;;;AAKA,MAAa,YAAY,SAAiB,WAAqB;CAC7D,IAAI,QAAQ;CACZ,OACE,QACG,QAAQ,8BAA8B,OAAO,YAAY,EAAE,CAAC,CAC5D,QAAQ,QAAQ,GAAG,CAAC,CACpB,QAAQ,OAAO,EAAE,KAAK;AAE7B;;;;AAKA,MAAa,iBACX,KACA,YACoB;CACpB,MAAM,QAAQ,eAAe,OAAO;CACpC,MAAM,QAAQ,IAAI,MAAM,KAAK;CAC7B,OAAO,QAAQ,MAAM,MAAM,CAAC,IAAI;AAClC"}
|
|
@@ -1091,7 +1091,7 @@ const renderMarkdownAst = (parsed, ctx, options = {}) => {
|
|
|
1091
1091
|
};
|
|
1092
1092
|
const compile = (markdown = "", ctx, options = {}) => {
|
|
1093
1093
|
if (typeof markdown !== "string") {
|
|
1094
|
-
console.error("intlayer: the first argument must be a string. Received", typeof markdown);
|
|
1094
|
+
if (process.env.NODE_ENV !== "production") console.error("intlayer: the first argument must be a string. Received", typeof markdown);
|
|
1095
1095
|
throw new Error("intlayer: the first argument must be a string");
|
|
1096
1096
|
}
|
|
1097
1097
|
const { parse, emit, createElement, scope } = getRuleSet(ctx, options, false);
|