@od-oneapp/internationalization 2026.1.1301
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/README.md +164 -0
- package/dist/client-next.d.mts +4 -0
- package/dist/client-next.mjs +4 -0
- package/dist/client.d.mts +24 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +16 -0
- package/dist/client.mjs.map +1 -0
- package/dist/en-BG_-Eo3m.mjs +192 -0
- package/dist/en-BG_-Eo3m.mjs.map +1 -0
- package/dist/middleware-DgXJ2JFB.mjs +10 -0
- package/dist/middleware-DgXJ2JFB.mjs.map +1 -0
- package/dist/middleware.d.mts +10 -0
- package/dist/middleware.d.mts.map +1 -0
- package/dist/middleware.mjs +17 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/navigation-BCo6Vugt.mjs +39 -0
- package/dist/navigation-BCo6Vugt.mjs.map +1 -0
- package/dist/navigation-BSuDX6-H.d.mts +20 -0
- package/dist/navigation-BSuDX6-H.d.mts.map +1 -0
- package/dist/navigation-Di2SkyuD.mjs +20 -0
- package/dist/navigation-Di2SkyuD.mjs.map +1 -0
- package/dist/next-intl-server-CnBEO3Z3.mjs +25 -0
- package/dist/next-intl-server-CnBEO3Z3.mjs.map +1 -0
- package/dist/react19-cache-DHL04P0L.mjs +485 -0
- package/dist/react19-cache-DHL04P0L.mjs.map +1 -0
- package/dist/request.d.mts +5 -0
- package/dist/request.d.mts.map +1 -0
- package/dist/request.mjs +114 -0
- package/dist/request.mjs.map +1 -0
- package/dist/routing-CK71AHzC.mjs +80 -0
- package/dist/routing-CK71AHzC.mjs.map +1 -0
- package/dist/routing-Cqn-FuJK.mjs +9 -0
- package/dist/routing-Cqn-FuJK.mjs.map +1 -0
- package/dist/routing.d.mts +13 -0
- package/dist/routing.d.mts.map +1 -0
- package/dist/routing.mjs +3 -0
- package/dist/server-CiAHG84s.d.mts +167 -0
- package/dist/server-CiAHG84s.d.mts.map +1 -0
- package/dist/server-next.d.mts +37 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +52 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +3 -0
- package/dist/server.mjs +12 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +104 -0
- package/src/__tests__/client.test.ts +37 -0
- package/src/__tests__/dictionary-loader.test.ts +228 -0
- package/src/__tests__/exports.test.ts +76 -0
- package/src/__tests__/extend.test.ts +55 -0
- package/src/__tests__/integration.test.ts +341 -0
- package/src/__tests__/middleware.test.ts +38 -0
- package/src/__tests__/navigation.test.ts +64 -0
- package/src/__tests__/request.test.ts +151 -0
- package/src/__tests__/routing.test.ts +39 -0
- package/src/__tests__/security.test.ts +293 -0
- package/src/__tests__/test-utils.ts +28 -0
- package/src/__tests__/vitest.d.ts +13 -0
- package/src/client-next.ts +31 -0
- package/src/client.ts +27 -0
- package/src/dictionaries/de.json +193 -0
- package/src/dictionaries/en.json +193 -0
- package/src/dictionaries/es.json +193 -0
- package/src/dictionaries/fr.json +193 -0
- package/src/dictionaries/pt.json +193 -0
- package/src/index.ts +24 -0
- package/src/middleware.ts +27 -0
- package/src/navigation.ts +91 -0
- package/src/request.ts +126 -0
- package/src/routing.export.ts +11 -0
- package/src/routing.ts +63 -0
- package/src/server-next.ts +57 -0
- package/src/server.ts +39 -0
- package/src/shared/constants.ts +42 -0
- package/src/shared/dictionary-loader.ts +599 -0
- package/src/shared/next-intl-adapter.ts +80 -0
- package/src/shared/next-intl-server.ts +45 -0
- package/src/shared/react19-cache.ts +132 -0
- package/src/types.ts +55 -0
- package/src/utils/extend.ts +48 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { a as locale } from "./routing-CK71AHzC.mjs";
|
|
2
|
+
import { logDebug, logError, logWarn } from "@od-oneapp/shared/logger";
|
|
3
|
+
|
|
4
|
+
//#region src/shared/dictionary-loader.ts
|
|
5
|
+
/**
|
|
6
|
+
* @fileoverview Shared dictionary loading utility
|
|
7
|
+
*
|
|
8
|
+
* This utility provides type-safe dictionary loading with proper error handling
|
|
9
|
+
* and fallback mechanisms. It eliminates code duplication between server.ts and index.ts.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - In-memory LRU cache for loaded dictionaries
|
|
13
|
+
* - Cache statistics for monitoring
|
|
14
|
+
* - Error handling with fallback to default locale
|
|
15
|
+
* - Support for multiple locales
|
|
16
|
+
*
|
|
17
|
+
* @module @od-oneapp/internationalization/shared/dictionary-loader
|
|
18
|
+
*/
|
|
19
|
+
const locales = [locale.source, ...locale.targets];
|
|
20
|
+
const MAX_CACHE_SIZE = Number.parseInt(process.env.I18N_CACHE_MAX_SIZE ?? "10", 10) ?? 10;
|
|
21
|
+
const dictionaryCache = /* @__PURE__ */ new Map();
|
|
22
|
+
const MAX_SAFE_COUNT = Number.MAX_SAFE_INTEGER;
|
|
23
|
+
const cacheStats = {
|
|
24
|
+
hits: 0,
|
|
25
|
+
misses: 0,
|
|
26
|
+
evictions: 0,
|
|
27
|
+
errors: 0
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Gets current cache statistics for monitoring and debugging.
|
|
31
|
+
* Useful for production performance analysis.
|
|
32
|
+
*
|
|
33
|
+
* @returns Cache statistics object with hits, misses, evictions, and errors
|
|
34
|
+
*/
|
|
35
|
+
function getCacheStats() {
|
|
36
|
+
return {
|
|
37
|
+
...cacheStats,
|
|
38
|
+
size: dictionaryCache.size,
|
|
39
|
+
maxSize: MAX_CACHE_SIZE,
|
|
40
|
+
hitRate: cacheStats.hits + cacheStats.misses > 0 ? `${(cacheStats.hits / (cacheStats.hits + cacheStats.misses) * 100).toFixed(2)}%` : "0%"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resets cache statistics. Useful for testing or periodic reset.
|
|
45
|
+
* Also prevents overflow in long-running processes.
|
|
46
|
+
*
|
|
47
|
+
* Note: This only resets statistics counters, not the cache itself.
|
|
48
|
+
* To clear the cache entries, create a new dictionary loader instance.
|
|
49
|
+
*/
|
|
50
|
+
function resetCacheStats() {
|
|
51
|
+
cacheStats.hits = 0;
|
|
52
|
+
cacheStats.misses = 0;
|
|
53
|
+
cacheStats.evictions = 0;
|
|
54
|
+
cacheStats.errors = 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Implements LRU (Least Recently Used) cache eviction.
|
|
58
|
+
* Moves accessed item to end of Map (most recent).
|
|
59
|
+
* Node 22 Map maintains insertion order.
|
|
60
|
+
*
|
|
61
|
+
* @param locale - The locale to mark as recently used
|
|
62
|
+
*/
|
|
63
|
+
function touchCache(locale) {
|
|
64
|
+
const value = dictionaryCache.get(locale);
|
|
65
|
+
if (value) {
|
|
66
|
+
dictionaryCache.delete(locale);
|
|
67
|
+
dictionaryCache.set(locale, value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Removes the least-recently-used dictionary from the in-memory cache when the cache size reaches the configured maximum.
|
|
72
|
+
*
|
|
73
|
+
* If an entry is evicted, increments the `cacheStats.evictions` counter with overflow protection and emits a debug log entry with eviction metadata when a logger is available.
|
|
74
|
+
*/
|
|
75
|
+
function evictOldestIfNeeded() {
|
|
76
|
+
if (dictionaryCache.size >= MAX_CACHE_SIZE) {
|
|
77
|
+
const oldestKey = dictionaryCache.keys().next().value;
|
|
78
|
+
if (oldestKey) {
|
|
79
|
+
dictionaryCache.delete(oldestKey);
|
|
80
|
+
cacheStats.evictions = Math.min((cacheStats.evictions ?? 0) + 1, MAX_SAFE_COUNT);
|
|
81
|
+
logDebug("Dictionary cache eviction", {
|
|
82
|
+
evictedLocale: oldestKey,
|
|
83
|
+
cacheSize: dictionaryCache.size,
|
|
84
|
+
totalEvictions: cacheStats.evictions,
|
|
85
|
+
context: {
|
|
86
|
+
function: "evictOldestIfNeeded",
|
|
87
|
+
package: "@od-oneapp/internationalization"
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const EMPTY_DICTIONARY = {
|
|
94
|
+
common: {
|
|
95
|
+
locale: "",
|
|
96
|
+
language: ""
|
|
97
|
+
},
|
|
98
|
+
web: {
|
|
99
|
+
global: {
|
|
100
|
+
primaryCta: "",
|
|
101
|
+
secondaryCta: ""
|
|
102
|
+
},
|
|
103
|
+
header: {
|
|
104
|
+
home: "",
|
|
105
|
+
product: {
|
|
106
|
+
title: "",
|
|
107
|
+
description: "",
|
|
108
|
+
pricing: ""
|
|
109
|
+
},
|
|
110
|
+
blog: "",
|
|
111
|
+
docs: "",
|
|
112
|
+
contact: "",
|
|
113
|
+
signIn: "",
|
|
114
|
+
signUp: ""
|
|
115
|
+
},
|
|
116
|
+
home: {
|
|
117
|
+
meta: {
|
|
118
|
+
title: "",
|
|
119
|
+
description: ""
|
|
120
|
+
},
|
|
121
|
+
hero: { announcement: "" },
|
|
122
|
+
cases: { title: "" },
|
|
123
|
+
features: {
|
|
124
|
+
title: "",
|
|
125
|
+
description: "",
|
|
126
|
+
items: []
|
|
127
|
+
},
|
|
128
|
+
stats: {
|
|
129
|
+
title: "",
|
|
130
|
+
description: "",
|
|
131
|
+
items: []
|
|
132
|
+
},
|
|
133
|
+
testimonials: {
|
|
134
|
+
title: "",
|
|
135
|
+
items: []
|
|
136
|
+
},
|
|
137
|
+
faq: {
|
|
138
|
+
title: "",
|
|
139
|
+
description: "",
|
|
140
|
+
cta: "",
|
|
141
|
+
items: []
|
|
142
|
+
},
|
|
143
|
+
cta: {
|
|
144
|
+
title: "",
|
|
145
|
+
description: ""
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
blog: { meta: {
|
|
149
|
+
title: "",
|
|
150
|
+
description: ""
|
|
151
|
+
} },
|
|
152
|
+
contact: {
|
|
153
|
+
meta: {
|
|
154
|
+
title: "",
|
|
155
|
+
description: ""
|
|
156
|
+
},
|
|
157
|
+
hero: {
|
|
158
|
+
benefits: [],
|
|
159
|
+
form: {
|
|
160
|
+
title: "",
|
|
161
|
+
date: "",
|
|
162
|
+
firstName: "",
|
|
163
|
+
lastName: "",
|
|
164
|
+
resume: "",
|
|
165
|
+
cta: ""
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Import a module with a timeout to prevent indefinite hangs.
|
|
173
|
+
* Uses Promise.race for timeout handling (import() cannot be cancelled).
|
|
174
|
+
*
|
|
175
|
+
* @param path - The module path to import
|
|
176
|
+
* @param timeoutMs - Timeout in milliseconds (default: 5000ms)
|
|
177
|
+
* @returns Promise that resolves to the imported module
|
|
178
|
+
* @throws Error if import times out or fails
|
|
179
|
+
*/
|
|
180
|
+
async function importWithTimeout(path, timeoutMs = 5e3) {
|
|
181
|
+
let timeoutId;
|
|
182
|
+
try {
|
|
183
|
+
const result = await Promise.race([import(path), new Promise((_resolve, reject) => {
|
|
184
|
+
timeoutId = setTimeout(() => {
|
|
185
|
+
reject(/* @__PURE__ */ new Error(`Import timeout after ${timeoutMs}ms: ${path}`));
|
|
186
|
+
}, timeoutMs);
|
|
187
|
+
})]);
|
|
188
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
189
|
+
return result;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Validates and normalizes a locale string to prevent path traversal attacks.
|
|
197
|
+
*
|
|
198
|
+
* @param locale - The locale string to validate
|
|
199
|
+
* @returns The normalized locale if valid, null otherwise
|
|
200
|
+
*/
|
|
201
|
+
function validateAndNormalizeLocale(locale) {
|
|
202
|
+
if (!locale || typeof locale !== "string" || locale.length > 20) return null;
|
|
203
|
+
if (!/^[a-z]{2}(-[a-z]{2,4})?$/i.test(locale)) return null;
|
|
204
|
+
const normalizedLocale = (locale.split("-")[0] ?? locale).toLowerCase();
|
|
205
|
+
if (!locales.includes(normalizedLocale)) return null;
|
|
206
|
+
return normalizedLocale;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Creates a type-safe dictionary loader with proper error handling
|
|
210
|
+
*
|
|
211
|
+
* @returns An object with methods to load dictionaries and check locale support
|
|
212
|
+
*/
|
|
213
|
+
function createDictionaryLoader() {
|
|
214
|
+
const dictionaries = Object.fromEntries(locales.map((locale) => [locale, () => {
|
|
215
|
+
if (dictionaryCache.has(locale)) {
|
|
216
|
+
touchCache(locale);
|
|
217
|
+
cacheStats.hits = Math.min((cacheStats.hits ?? 0) + 1, MAX_SAFE_COUNT);
|
|
218
|
+
const cached = dictionaryCache.get(locale);
|
|
219
|
+
if (!cached) throw new Error(`Cache inconsistency: locale ${locale} was in cache but get returned undefined`);
|
|
220
|
+
logDebug("Dictionary cache hit", {
|
|
221
|
+
locale,
|
|
222
|
+
cached: true,
|
|
223
|
+
cacheSize: dictionaryCache.size,
|
|
224
|
+
hitRate: getCacheStats().hitRate,
|
|
225
|
+
context: {
|
|
226
|
+
function: "createDictionaryLoader",
|
|
227
|
+
package: "@od-oneapp/internationalization"
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return cached;
|
|
231
|
+
}
|
|
232
|
+
cacheStats.misses = Math.min((cacheStats.misses ?? 0) + 1, MAX_SAFE_COUNT);
|
|
233
|
+
evictOldestIfNeeded();
|
|
234
|
+
let originalLoadSucceeded = false;
|
|
235
|
+
const loadPromise = (async () => {
|
|
236
|
+
const startTime = performance.now();
|
|
237
|
+
try {
|
|
238
|
+
const mod = await importWithTimeout(`../dictionaries/${locale}.json`, 5e3);
|
|
239
|
+
originalLoadSucceeded = true;
|
|
240
|
+
const duration = performance.now() - startTime;
|
|
241
|
+
const dictionary = mod.default;
|
|
242
|
+
logDebug("Dictionary loaded successfully", {
|
|
243
|
+
locale,
|
|
244
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
245
|
+
cached: false,
|
|
246
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
247
|
+
context: {
|
|
248
|
+
function: "createDictionaryLoader",
|
|
249
|
+
package: "@od-oneapp/internationalization"
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
return dictionary;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
const duration = performance.now() - startTime;
|
|
255
|
+
cacheStats.errors = Math.min((cacheStats.errors ?? 0) + 1, MAX_SAFE_COUNT);
|
|
256
|
+
const errorMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
257
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
258
|
+
logError(`Failed to load dictionary for locale: ${locale}`, {
|
|
259
|
+
error: errorMessage,
|
|
260
|
+
stack: errorStack,
|
|
261
|
+
locale,
|
|
262
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
263
|
+
totalErrors: cacheStats.errors,
|
|
264
|
+
context: {
|
|
265
|
+
function: "createDictionaryLoader",
|
|
266
|
+
package: "@od-oneapp/internationalization"
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const fallbackStartTime = performance.now();
|
|
271
|
+
const fallbackMod = await importWithTimeout("../dictionaries/en.json", 5e3);
|
|
272
|
+
const fallbackDuration = performance.now() - fallbackStartTime;
|
|
273
|
+
const fallbackDictionary = fallbackMod.default;
|
|
274
|
+
logDebug("English fallback dictionary loaded", {
|
|
275
|
+
originalLocale: locale,
|
|
276
|
+
fallbackLocale: "en",
|
|
277
|
+
duration: `${fallbackDuration.toFixed(2)}ms`,
|
|
278
|
+
context: {
|
|
279
|
+
function: "createDictionaryLoader",
|
|
280
|
+
package: "@od-oneapp/internationalization"
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return fallbackDictionary;
|
|
284
|
+
} catch (fallbackError) {
|
|
285
|
+
logError("Critical: English fallback dictionary failed to load", {
|
|
286
|
+
error: fallbackError instanceof Error ? fallbackError.message : typeof fallbackError === "string" ? fallbackError : "Unknown error",
|
|
287
|
+
stack: fallbackError instanceof Error ? fallbackError.stack : void 0,
|
|
288
|
+
locale
|
|
289
|
+
});
|
|
290
|
+
throw fallbackError;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
})();
|
|
294
|
+
const cachedPromiseWrapper = async () => {
|
|
295
|
+
try {
|
|
296
|
+
const result = await loadPromise;
|
|
297
|
+
const cached = dictionaryCache.get(locale);
|
|
298
|
+
if (!originalLoadSucceeded && cached === cachedPromise) dictionaryCache.delete(locale);
|
|
299
|
+
return result;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (dictionaryCache.get(locale) === cachedPromise) dictionaryCache.delete(locale);
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const cachedPromise = cachedPromiseWrapper();
|
|
306
|
+
dictionaryCache.set(locale, cachedPromise);
|
|
307
|
+
return cachedPromise;
|
|
308
|
+
}]));
|
|
309
|
+
return {
|
|
310
|
+
async getDictionary(locale) {
|
|
311
|
+
const startTime = performance.now();
|
|
312
|
+
const validatedLocale = validateAndNormalizeLocale(locale);
|
|
313
|
+
if (!validatedLocale) {
|
|
314
|
+
logWarn(`Invalid or unsupported locale "${locale}", defaulting to "en"`, {
|
|
315
|
+
locale,
|
|
316
|
+
context: {
|
|
317
|
+
function: "getDictionary",
|
|
318
|
+
package: "@od-oneapp/internationalization"
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
try {
|
|
322
|
+
const resolvedDictionary = await (dictionaries.en?.() ?? Promise.resolve(EMPTY_DICTIONARY));
|
|
323
|
+
logDebug("Dictionary loaded via fallback", {
|
|
324
|
+
requestedLocale: locale,
|
|
325
|
+
actualLocale: "en",
|
|
326
|
+
duration: `${(performance.now() - startTime).toFixed(2)}ms`,
|
|
327
|
+
cached: dictionaryCache.has("en"),
|
|
328
|
+
context: {
|
|
329
|
+
function: "getDictionary",
|
|
330
|
+
package: "@od-oneapp/internationalization"
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
return resolvedDictionary;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
const duration = performance.now() - startTime;
|
|
336
|
+
logError("Failed to load English fallback dictionary", {
|
|
337
|
+
error: error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error",
|
|
338
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
339
|
+
duration: `${duration.toFixed(2)}ms`
|
|
340
|
+
});
|
|
341
|
+
return EMPTY_DICTIONARY;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const resolvedDictionary = await (dictionaries[validatedLocale]?.() ?? Promise.resolve(EMPTY_DICTIONARY));
|
|
346
|
+
logDebug("Dictionary retrieved", {
|
|
347
|
+
locale: validatedLocale,
|
|
348
|
+
duration: `${(performance.now() - startTime).toFixed(2)}ms`,
|
|
349
|
+
cached: dictionaryCache.has(validatedLocale),
|
|
350
|
+
context: {
|
|
351
|
+
function: "getDictionary",
|
|
352
|
+
package: "@od-oneapp/internationalization"
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return resolvedDictionary;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const duration = performance.now() - startTime;
|
|
358
|
+
logError(`Error loading dictionary for locale "${validatedLocale}", falling back to "en"`, {
|
|
359
|
+
error: error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error",
|
|
360
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
361
|
+
locale: validatedLocale,
|
|
362
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
363
|
+
context: {
|
|
364
|
+
function: "getDictionary",
|
|
365
|
+
package: "@od-oneapp/internationalization"
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
try {
|
|
369
|
+
return await (dictionaries.en?.() ?? Promise.resolve(EMPTY_DICTIONARY));
|
|
370
|
+
} catch (fallbackError) {
|
|
371
|
+
logError("Failed to load English fallback dictionary", {
|
|
372
|
+
error: fallbackError instanceof Error ? fallbackError.message : typeof fallbackError === "string" ? fallbackError : "Unknown error",
|
|
373
|
+
stack: fallbackError instanceof Error ? fallbackError.stack : void 0
|
|
374
|
+
});
|
|
375
|
+
return EMPTY_DICTIONARY;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
getLocales: () => locales,
|
|
380
|
+
isLocaleSupported: (locale) => {
|
|
381
|
+
return validateAndNormalizeLocale(locale) !== null;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/shared/react19-cache.ts
|
|
388
|
+
/**
|
|
389
|
+
* @fileoverview React 19 optimized dictionary cache
|
|
390
|
+
*
|
|
391
|
+
* This module provides React 19-aware dictionary caching that works
|
|
392
|
+
* seamlessly with React Server Components and the `use` hook.
|
|
393
|
+
*
|
|
394
|
+
* Features:
|
|
395
|
+
* - Promise-based dictionary loading for React 19's `use` hook
|
|
396
|
+
* - Suspense support for Server Components
|
|
397
|
+
* - Cache management and preloading utilities
|
|
398
|
+
*
|
|
399
|
+
* @module @od-oneapp/internationalization/shared/react19-cache
|
|
400
|
+
*/
|
|
401
|
+
const dictionaryLoader = createDictionaryLoader();
|
|
402
|
+
/**
|
|
403
|
+
* Cache for dictionary promises that can be used with React 19's `use` hook.
|
|
404
|
+
* React 19 can suspend on promises, so we expose the raw promises.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```tsx
|
|
408
|
+
* // In a React 19 Server Component
|
|
409
|
+
* import { use } from 'react';
|
|
410
|
+
* import { getDictionaryPromise } from '@od-oneapp/internationalization/server';
|
|
411
|
+
*
|
|
412
|
+
* export default function Page({ locale }: { locale: string }) {
|
|
413
|
+
* // React 19's `use` hook will suspend until dictionary loads
|
|
414
|
+
* const dict = use(getDictionaryPromise(locale));
|
|
415
|
+
* return <h1>{dict.web.home.meta.title}</h1>;
|
|
416
|
+
* }
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
const react19PromiseCache = /* @__PURE__ */ new Map();
|
|
420
|
+
/**
|
|
421
|
+
* Gets a dictionary promise suitable for React 19's `use` hook.
|
|
422
|
+
* The promise is cached to prevent unnecessary re-renders.
|
|
423
|
+
* This cache is synchronized with the LRU cache to prevent desynchronization.
|
|
424
|
+
*
|
|
425
|
+
* @param locale - The locale to load
|
|
426
|
+
* @returns A promise that resolves to the dictionary (stable reference)
|
|
427
|
+
*/
|
|
428
|
+
function getDictionaryPromise(locale) {
|
|
429
|
+
const promise = dictionaryLoader.getDictionary(locale);
|
|
430
|
+
react19PromiseCache.set(locale, promise);
|
|
431
|
+
return promise;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Preloads a dictionary for optimal React 19 performance.
|
|
435
|
+
* Call this in route handlers or middleware to warm the cache.
|
|
436
|
+
*
|
|
437
|
+
* @param locale - The locale to preload
|
|
438
|
+
* @returns The preload promise (fire and forget)
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```tsx
|
|
442
|
+
* // In middleware or route handler
|
|
443
|
+
* import { preloadDictionary } from '@od-oneapp/internationalization/server';
|
|
444
|
+
*
|
|
445
|
+
* export function middleware(request: NextRequest) {
|
|
446
|
+
* const locale = getLocaleFromRequest(request);
|
|
447
|
+
* preloadDictionary(locale); // Fire and forget
|
|
448
|
+
* // ...
|
|
449
|
+
* }
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
function preloadDictionary(locale) {
|
|
453
|
+
return getDictionaryPromise(locale);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Clears the React 19 promise cache.
|
|
457
|
+
* Useful for testing or manual cache invalidation.
|
|
458
|
+
*/
|
|
459
|
+
function clearReact19Cache() {
|
|
460
|
+
react19PromiseCache.clear();
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Type guard for checking if a value is a valid locale.
|
|
464
|
+
* React 19 benefits from proper type narrowing.
|
|
465
|
+
* Uses the same validation logic as dictionary-loader for consistency.
|
|
466
|
+
*
|
|
467
|
+
* @param value - The value to check
|
|
468
|
+
* @returns True if the value is a valid Locale
|
|
469
|
+
*/
|
|
470
|
+
function isValidLocale(value) {
|
|
471
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 20) return false;
|
|
472
|
+
if (!/^[a-z]{2}(-[a-z]{2,4})?$/i.test(value)) return false;
|
|
473
|
+
const normalizedLocale = (value.split("-")[0] ?? value).toLowerCase();
|
|
474
|
+
return [
|
|
475
|
+
"en",
|
|
476
|
+
"es",
|
|
477
|
+
"de",
|
|
478
|
+
"fr",
|
|
479
|
+
"pt"
|
|
480
|
+
].includes(normalizedLocale);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
//#endregion
|
|
484
|
+
export { createDictionaryLoader as a, preloadDictionary as i, getDictionaryPromise as n, getCacheStats as o, isValidLocale as r, resetCacheStats as s, clearReact19Cache as t };
|
|
485
|
+
//# sourceMappingURL=react19-cache-DHL04P0L.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react19-cache-DHL04P0L.mjs","names":[],"sources":["../src/shared/dictionary-loader.ts","../src/shared/react19-cache.ts"],"sourcesContent":["/**\n * @fileoverview Shared dictionary loading utility\n *\n * This utility provides type-safe dictionary loading with proper error handling\n * and fallback mechanisms. It eliminates code duplication between server.ts and index.ts.\n *\n * Features:\n * - In-memory LRU cache for loaded dictionaries\n * - Cache statistics for monitoring\n * - Error handling with fallback to default locale\n * - Support for multiple locales\n *\n * @module @repo/internationalization/shared/dictionary-loader\n */\n\nimport { logDebug, logError, logWarn } from '@repo/shared/logger';\n\nimport languine from '../../languine.json';\n\nimport type en from '../dictionaries/en.json';\n\nconst locales = [languine.locale.source, ...languine.locale.targets] as const;\n\nexport type Locale = (typeof locales)[number];\nexport type Dictionary = typeof en;\n\n// In-memory cache for loaded dictionaries with LRU eviction\n// Limited to prevent memory bloat in long-running Node 22 processes\n// Can be configured via environment variable for different deployment scenarios\nconst MAX_CACHE_SIZE = Number.parseInt(process.env.I18N_CACHE_MAX_SIZE ?? '10', 10) ?? 10;\nconst dictionaryCache = new Map<Locale, Promise<Dictionary>>();\n\n// Cache statistics for monitoring (Node 22 performance tracking)\n// Use Number.MAX_SAFE_INTEGER to prevent overflow in long-running processes\nconst MAX_SAFE_COUNT = Number.MAX_SAFE_INTEGER;\nconst cacheStats = {\n hits: 0,\n misses: 0,\n evictions: 0,\n errors: 0,\n};\n\n/**\n * Gets current cache statistics for monitoring and debugging.\n * Useful for production performance analysis.\n *\n * @returns Cache statistics object with hits, misses, evictions, and errors\n */\nexport function getCacheStats() {\n return {\n ...cacheStats,\n size: dictionaryCache.size,\n maxSize: MAX_CACHE_SIZE,\n hitRate:\n cacheStats.hits + cacheStats.misses > 0\n ? `${((cacheStats.hits / (cacheStats.hits + cacheStats.misses)) * 100).toFixed(2)}%`\n : '0%',\n };\n}\n\n/**\n * Resets cache statistics. Useful for testing or periodic reset.\n * Also prevents overflow in long-running processes.\n *\n * Note: This only resets statistics counters, not the cache itself.\n * To clear the cache entries, create a new dictionary loader instance.\n */\nexport function resetCacheStats(): void {\n cacheStats.hits = 0;\n cacheStats.misses = 0;\n cacheStats.evictions = 0;\n cacheStats.errors = 0;\n}\n\n/**\n * Implements LRU (Least Recently Used) cache eviction.\n * Moves accessed item to end of Map (most recent).\n * Node 22 Map maintains insertion order.\n *\n * @param locale - The locale to mark as recently used\n */\nfunction touchCache(locale: Locale): void {\n const value = dictionaryCache.get(locale);\n if (value) {\n // Remove and re-add to move to end (most recent)\n dictionaryCache.delete(locale);\n dictionaryCache.set(locale, value);\n }\n}\n\n/**\n * Removes the least-recently-used dictionary from the in-memory cache when the cache size reaches the configured maximum.\n *\n * If an entry is evicted, increments the `cacheStats.evictions` counter with overflow protection and emits a debug log entry with eviction metadata when a logger is available.\n */\nfunction evictOldestIfNeeded(): void {\n if (dictionaryCache.size >= MAX_CACHE_SIZE) {\n // First key is oldest (Map maintains insertion order)\n const oldestKey = dictionaryCache.keys().next().value;\n if (oldestKey) {\n dictionaryCache.delete(oldestKey);\n // Atomic increment with overflow protection\n cacheStats.evictions = Math.min((cacheStats.evictions ?? 0) + 1, MAX_SAFE_COUNT);\n logDebug('Dictionary cache eviction', {\n evictedLocale: oldestKey,\n cacheSize: dictionaryCache.size,\n totalEvictions: cacheStats.evictions,\n context: {\n function: 'evictOldestIfNeeded',\n package: '@repo/internationalization',\n },\n });\n }\n }\n}\n\n// Minimal valid dictionary structure for fallback\n// Matches the complete structure of en.json to prevent runtime errors\nconst EMPTY_DICTIONARY: Dictionary = {\n common: {\n locale: '',\n language: '',\n },\n web: {\n global: {\n primaryCta: '',\n secondaryCta: '',\n },\n header: {\n home: '',\n product: {\n title: '',\n description: '',\n pricing: '',\n },\n blog: '',\n docs: '',\n contact: '',\n signIn: '',\n signUp: '',\n },\n home: {\n meta: {\n title: '',\n description: '',\n },\n hero: {\n announcement: '',\n },\n cases: {\n title: '',\n },\n features: {\n title: '',\n description: '',\n items: [],\n },\n stats: {\n title: '',\n description: '',\n items: [],\n },\n testimonials: {\n title: '',\n items: [],\n },\n faq: {\n title: '',\n description: '',\n cta: '',\n items: [],\n },\n cta: {\n title: '',\n description: '',\n },\n },\n blog: {\n meta: {\n title: '',\n description: '',\n },\n },\n contact: {\n meta: {\n title: '',\n description: '',\n },\n hero: {\n benefits: [],\n form: {\n title: '',\n date: '',\n firstName: '',\n lastName: '',\n resume: '',\n cta: '',\n },\n },\n },\n },\n};\n\n// Use the structured empty dictionary for type-safe fallback\n// EMPTY_DICTIONARY already provides the correct shape\n\n// Type-safe error for dictionary loading (reserved for future use)\nexport interface _DictionaryLoadError {\n locale: string;\n message: string;\n originalError: unknown;\n}\n\n/**\n * Import a module with a timeout to prevent indefinite hangs.\n * Uses Promise.race for timeout handling (import() cannot be cancelled).\n *\n * @param path - The module path to import\n * @param timeoutMs - Timeout in milliseconds (default: 5000ms)\n * @returns Promise that resolves to the imported module\n * @throws Error if import times out or fails\n */\nasync function importWithTimeout<T>(path: string, timeoutMs = 5000): Promise<T> {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n try {\n // Race between import and timeout\n // Note: import() cannot be cancelled, but we can reject the promise on timeout\n const result = await Promise.race([\n import(path) as Promise<T>,\n new Promise<never>((_resolve, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(`Import timeout after ${timeoutMs}ms: ${path}`));\n }, timeoutMs);\n }),\n ]);\n\n // Clear timeout if import succeeded before timeout\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n\n return result;\n } catch (error: unknown) {\n // Ensure timeout is cleared on error\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n throw error;\n }\n}\n\n/**\n * Validates and normalizes a locale string to prevent path traversal attacks.\n *\n * @param locale - The locale string to validate\n * @returns The normalized locale if valid, null otherwise\n */\nfunction validateAndNormalizeLocale(locale: string): Locale | null {\n // Validate input length and type\n if (!locale || typeof locale !== 'string' || locale.length > 20) {\n return null;\n }\n\n // Validate pattern (alphanumeric and hyphens only, BCP 47 format)\n // Safe regex: bounded quantifiers ({2}, {2,4}), input length pre-validated (max 20), no catastrophic backtracking\n // eslint-disable-next-line regexp/no-unused-capturing-group, security/detect-unsafe-regex -- Capturing group needed for validation; regex is safe (bounded quantifiers, input length limited)\n if (!/^[a-z]{2}(-[a-z]{2,4})?$/i.test(locale)) {\n return null;\n }\n\n // Normalize to base locale (e.g., 'en-US' -> 'en')\n const localeParts = locale.split('-');\n const normalizedLocale = (localeParts[0] ?? locale).toLowerCase();\n\n // Validate normalized locale exists in supported locales\n if (!locales.includes(normalizedLocale)) {\n return null;\n }\n\n return normalizedLocale;\n}\n\n/**\n * Creates a type-safe dictionary loader with proper error handling\n *\n * @returns An object with methods to load dictionaries and check locale support\n */\nexport function createDictionaryLoader() {\n const dictionaries: Record<Locale, () => Promise<Dictionary>> = Object.fromEntries(\n locales.map(locale => [\n locale,\n (): Promise<Dictionary> => {\n // Check cache first (LRU-aware)\n if (dictionaryCache.has(locale)) {\n touchCache(locale); // Mark as recently used\n // Atomic increment for cache hit statistics with overflow protection\n cacheStats.hits = Math.min((cacheStats.hits ?? 0) + 1, MAX_SAFE_COUNT);\n const cached = dictionaryCache.get(locale);\n if (!cached) {\n throw new Error(\n `Cache inconsistency: locale ${locale} was in cache but get returned undefined`,\n );\n }\n // Log cache hit without sensitive performance data in production\n if (process.env.NODE_ENV === 'development') {\n logDebug('Dictionary cache hit', {\n locale,\n cached: true,\n cacheSize: dictionaryCache.size,\n hitRate: getCacheStats().hitRate,\n context: {\n function: 'createDictionaryLoader',\n package: '@repo/internationalization',\n },\n });\n }\n return cached;\n }\n\n // Cache miss - atomic increment with overflow protection\n cacheStats.misses = Math.min((cacheStats.misses ?? 0) + 1, MAX_SAFE_COUNT);\n\n // Evict oldest entry if cache is full\n evictOldestIfNeeded();\n\n // Load and cache dictionary\n // Track whether the original locale load succeeded (not a fallback)\n let originalLoadSucceeded = false;\n const loadPromise = (async (): Promise<Dictionary> => {\n const startTime = performance.now();\n\n try {\n // Locale is guaranteed safe here as it comes from the locales array\n // Use timeout to prevent indefinite hangs\n const mod = await importWithTimeout<{ default: Dictionary }>(\n `../dictionaries/${locale}.json`,\n 5000,\n );\n\n // Mark that original load succeeded (not a fallback)\n originalLoadSucceeded = true;\n const duration = performance.now() - startTime;\n\n const dictionary = mod.default;\n\n logDebug('Dictionary loaded successfully', {\n locale,\n duration: `${duration.toFixed(2)}ms`,\n cached: false,\n timestamp: new Date().toISOString(),\n context: {\n function: 'createDictionaryLoader',\n package: '@repo/internationalization',\n },\n });\n\n return dictionary;\n } catch (error: unknown) {\n const duration = performance.now() - startTime;\n // Atomic increment for error statistics with overflow protection\n cacheStats.errors = Math.min((cacheStats.errors ?? 0) + 1, MAX_SAFE_COUNT);\n const errorMessage =\n error instanceof Error\n ? error.message\n : typeof error === 'string'\n ? error\n : 'Unknown error';\n const errorStack = error instanceof Error ? error.stack : undefined;\n\n logError(`Failed to load dictionary for locale: ${locale}`, {\n error: errorMessage,\n stack: errorStack,\n locale,\n duration: `${duration.toFixed(2)}ms`,\n totalErrors: cacheStats.errors,\n context: {\n function: 'createDictionaryLoader',\n package: '@repo/internationalization',\n },\n });\n\n // Fallback to English dictionary\n // IMPORTANT: Do not cache fallback results - they should not be cached\n // under the original locale key to prevent incorrect caching\n try {\n const fallbackStartTime = performance.now();\n const fallbackMod = await importWithTimeout<{ default: Dictionary }>(\n '../dictionaries/en.json',\n 5000,\n );\n const fallbackDuration = performance.now() - fallbackStartTime;\n\n const fallbackDictionary = fallbackMod.default;\n\n logDebug('English fallback dictionary loaded', {\n originalLocale: locale,\n fallbackLocale: 'en',\n duration: `${fallbackDuration.toFixed(2)}ms`,\n context: {\n function: 'createDictionaryLoader',\n package: '@repo/internationalization',\n },\n });\n\n // Return fallback but don't cache it - let it fail again next time\n // This ensures we don't cache incorrect locale mappings\n return fallbackDictionary;\n } catch (fallbackError: unknown) {\n logError('Critical: English fallback dictionary failed to load', {\n error:\n fallbackError instanceof Error\n ? fallbackError.message\n : typeof fallbackError === 'string'\n ? fallbackError\n : 'Unknown error',\n stack: fallbackError instanceof Error ? fallbackError.stack : undefined,\n locale,\n });\n // Bubble up failure so callers can log additional context\n throw fallbackError;\n }\n }\n })();\n\n // Wrap promise to check if original load succeeded before caching\n const cachedPromiseWrapper = async (): Promise<Dictionary> => {\n try {\n const result = await loadPromise;\n // Only keep in cache if original load succeeded (not a fallback)\n // If original load failed and we used fallback, remove from cache\n const cached = dictionaryCache.get(locale);\n if (!originalLoadSucceeded && cached === cachedPromise) {\n dictionaryCache.delete(locale);\n }\n return result;\n } catch (error: unknown) {\n // If promise rejects, remove from cache\n const cached = dictionaryCache.get(locale);\n if (cached === cachedPromise) {\n dictionaryCache.delete(locale);\n }\n throw error;\n }\n };\n\n // Create the promise and cache it\n const cachedPromise = cachedPromiseWrapper();\n\n // Cache the promise initially - we'll remove it later if it was a fallback\n dictionaryCache.set(locale, cachedPromise);\n\n return cachedPromise;\n },\n ]) satisfies Array<[Locale, () => Promise<Dictionary>]>,\n ) as Record<Locale, () => Promise<Dictionary>>;\n\n return {\n /**\n * Loads a dictionary for the specified locale with proper type safety and fallbacks.\n *\n * @param locale - The locale string to load (e.g., 'en', 'en-US', 'es')\n * @returns A promise that resolves to the dictionary for the locale\n *\n * @example\n * ```typescript\n * const loader = createDictionaryLoader();\n * const dict = await loader.getDictionary('en');\n * ```\n */\n async getDictionary(locale: string): Promise<Dictionary> {\n const startTime = performance.now();\n\n // Validate and normalize locale to prevent path traversal\n const validatedLocale = validateAndNormalizeLocale(locale);\n\n if (!validatedLocale) {\n logWarn(`Invalid or unsupported locale \"${locale}\", defaulting to \"en\"`, {\n locale,\n context: {\n function: 'getDictionary',\n package: '@repo/internationalization',\n },\n });\n // Safely attempt English fallback\n try {\n const result = await (dictionaries.en?.() ?? Promise.resolve(EMPTY_DICTIONARY));\n const resolvedDictionary = result;\n const duration = performance.now() - startTime;\n\n logDebug('Dictionary loaded via fallback', {\n requestedLocale: locale,\n actualLocale: 'en',\n duration: `${duration.toFixed(2)}ms`,\n cached: dictionaryCache.has('en'),\n context: {\n function: 'getDictionary',\n package: '@repo/internationalization',\n },\n });\n\n return resolvedDictionary;\n } catch (error: unknown) {\n const duration = performance.now() - startTime;\n\n logError('Failed to load English fallback dictionary', {\n error:\n error instanceof Error\n ? error.message\n : typeof error === 'string'\n ? error\n : 'Unknown error',\n stack: error instanceof Error ? error.stack : undefined,\n duration: `${duration.toFixed(2)}ms`,\n });\n return EMPTY_DICTIONARY;\n }\n }\n\n try {\n const result = await (dictionaries[validatedLocale]?.() ??\n Promise.resolve(EMPTY_DICTIONARY));\n const resolvedDictionary = result;\n const duration = performance.now() - startTime;\n\n logDebug('Dictionary retrieved', {\n locale: validatedLocale,\n duration: `${duration.toFixed(2)}ms`,\n cached: dictionaryCache.has(validatedLocale),\n context: {\n function: 'getDictionary',\n package: '@repo/internationalization',\n },\n });\n\n return resolvedDictionary;\n } catch (error: unknown) {\n const duration = performance.now() - startTime;\n\n logError(`Error loading dictionary for locale \"${validatedLocale}\", falling back to \"en\"`, {\n error:\n error instanceof Error\n ? error.message\n : typeof error === 'string'\n ? error\n : 'Unknown error',\n stack: error instanceof Error ? error.stack : undefined,\n locale: validatedLocale,\n duration: `${duration.toFixed(2)}ms`,\n context: {\n function: 'getDictionary',\n package: '@repo/internationalization',\n },\n });\n // Safely attempt English fallback\n try {\n const fallbackResult = await (dictionaries.en?.() ?? Promise.resolve(EMPTY_DICTIONARY));\n return fallbackResult;\n } catch (fallbackError: unknown) {\n logError('Failed to load English fallback dictionary', {\n error:\n fallbackError instanceof Error\n ? fallbackError.message\n : typeof fallbackError === 'string'\n ? fallbackError\n : 'Unknown error',\n stack: fallbackError instanceof Error ? fallbackError.stack : undefined,\n });\n return EMPTY_DICTIONARY;\n }\n }\n },\n\n /**\n * Get all available locales.\n *\n * @returns An array of all supported locale codes\n */\n getLocales: () => locales,\n\n /**\n * Check if a locale is supported.\n *\n * @param locale - The locale string to check\n * @returns True if the locale is supported, false otherwise\n *\n * @example\n * ```typescript\n * const loader = createDictionaryLoader();\n * if (loader.isLocaleSupported('en-US')) {\n * // Load dictionary\n * }\n * ```\n */\n isLocaleSupported: (locale: string): boolean => {\n return validateAndNormalizeLocale(locale) !== null;\n },\n };\n}\n","/**\n * @fileoverview React 19 optimized dictionary cache\n *\n * This module provides React 19-aware dictionary caching that works\n * seamlessly with React Server Components and the `use` hook.\n *\n * Features:\n * - Promise-based dictionary loading for React 19's `use` hook\n * - Suspense support for Server Components\n * - Cache management and preloading utilities\n *\n * @module @repo/internationalization/shared/react19-cache\n */\n\nimport { createDictionaryLoader } from './dictionary-loader';\n\nimport type { Dictionary, Locale } from './dictionary-loader';\n\n// Create dictionary loader instance for React 19 integration\nconst dictionaryLoader = createDictionaryLoader();\n\n/**\n * Cache for dictionary promises that can be used with React 19's `use` hook.\n * React 19 can suspend on promises, so we expose the raw promises.\n *\n * @example\n * ```tsx\n * // In a React 19 Server Component\n * import { use } from 'react';\n * import { getDictionaryPromise } from '@repo/internationalization/server';\n *\n * export default function Page({ locale }: { locale: string }) {\n * // React 19's `use` hook will suspend until dictionary loads\n * const dict = use(getDictionaryPromise(locale));\n * return <h1>{dict.web.home.meta.title}</h1>;\n * }\n * ```\n */\n\n// React 19 promise cache for `use` hook compatibility\n// This cache is synchronized with the LRU cache in dictionary-loader to prevent memory leaks\nconst react19PromiseCache = new Map<Locale, Promise<Dictionary>>();\n\n/**\n * Gets a dictionary promise suitable for React 19's `use` hook.\n * The promise is cached to prevent unnecessary re-renders.\n * This cache is synchronized with the LRU cache to prevent desynchronization.\n *\n * @param locale - The locale to load\n * @returns A promise that resolves to the dictionary (stable reference)\n */\nexport function getDictionaryPromise(locale: Locale): Promise<Dictionary> {\n // Always get fresh promise from dictionary loader to ensure cache synchronization\n // The dictionary loader's LRU cache handles the actual caching\n const promise = dictionaryLoader.getDictionary(locale);\n\n // Store in React 19 cache for stable reference (required for `use` hook)\n // This is safe because dictionaryLoader.getDictionary returns the same promise\n // for the same locale when cached, maintaining referential equality\n react19PromiseCache.set(locale, promise);\n\n return promise;\n}\n\n/**\n * Preloads a dictionary for optimal React 19 performance.\n * Call this in route handlers or middleware to warm the cache.\n *\n * @param locale - The locale to preload\n * @returns The preload promise (fire and forget)\n *\n * @example\n * ```tsx\n * // In middleware or route handler\n * import { preloadDictionary } from '@repo/internationalization/server';\n *\n * export function middleware(request: NextRequest) {\n * const locale = getLocaleFromRequest(request);\n * preloadDictionary(locale); // Fire and forget\n * // ...\n * }\n * ```\n */\nexport function preloadDictionary(locale: Locale): Promise<Dictionary> {\n return getDictionaryPromise(locale);\n}\n\n/**\n * Clears the React 19 promise cache.\n * Useful for testing or manual cache invalidation.\n */\nexport function clearReact19Cache(): void {\n react19PromiseCache.clear();\n}\n\n/**\n * Type guard for checking if a value is a valid locale.\n * React 19 benefits from proper type narrowing.\n * Uses the same validation logic as dictionary-loader for consistency.\n *\n * @param value - The value to check\n * @returns True if the value is a valid Locale\n */\nexport function isValidLocale(value: unknown): value is Locale {\n if (typeof value !== 'string' || value.length === 0 || value.length > 20) {\n return false;\n }\n\n // Validate pattern (BCP 47 format: 2-letter code optionally followed by region)\n // Safe regex: bounded quantifiers ({2}, {2,4}), input length pre-validated (max 20), no catastrophic backtracking\n // eslint-disable-next-line regexp/no-unused-capturing-group, security/detect-unsafe-regex -- Capturing group needed for validation; regex is safe (bounded quantifiers, input length limited)\n if (!/^[a-z]{2}(-[a-z]{2,4})?$/i.test(value)) {\n return false;\n }\n\n // Normalize and check against supported locales\n const localeParts = value.split('-');\n const normalizedLocale = (localeParts[0] ?? value).toLowerCase();\n const supportedLocales: readonly Locale[] = ['en', 'es', 'de', 'fr', 'pt'];\n return supportedLocales.includes(normalizedLocale);\n}\n\n/**\n * React 19 Suspense boundary helper types\n */\nexport type SuspendedDictionary = Promise<Dictionary> | Dictionary;\n\n/**\n * Unwraps a dictionary that might be suspended (React 19 pattern).\n * This is a type helper for components using `use`.\n */\nexport type UnwrapDictionary<T> = T extends Promise<infer U> ? U : T;\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.mts","names":[],"sources":["../src/request.ts"],"mappings":""}
|
package/dist/request.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { n as routing } from "./routing-CK71AHzC.mjs";
|
|
2
|
+
import { i as getRequestConfig } from "./next-intl-server-CnBEO3Z3.mjs";
|
|
3
|
+
import { logError, logWarn } from "@od-oneapp/shared/logger";
|
|
4
|
+
import { hasLocale } from "next-intl";
|
|
5
|
+
|
|
6
|
+
//#region src/shared/constants.ts
|
|
7
|
+
/**
|
|
8
|
+
* Default currency code used when locale-specific currency is not available
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CURRENCY = "USD";
|
|
11
|
+
/**
|
|
12
|
+
* Default timezone used when locale-specific timezone is not available
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_TIMEZONE = "UTC";
|
|
15
|
+
/**
|
|
16
|
+
* Map of locales to their default currency codes
|
|
17
|
+
*/
|
|
18
|
+
const LOCALE_CURRENCY_MAP = {
|
|
19
|
+
en: "USD",
|
|
20
|
+
es: "EUR",
|
|
21
|
+
fr: "EUR",
|
|
22
|
+
de: "EUR",
|
|
23
|
+
pt: "EUR"
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Map of locales to their default timezones
|
|
27
|
+
*/
|
|
28
|
+
const LOCALE_TIMEZONE_MAP = {
|
|
29
|
+
en: "America/New_York",
|
|
30
|
+
es: "Europe/Madrid",
|
|
31
|
+
fr: "Europe/Paris",
|
|
32
|
+
de: "Europe/Berlin",
|
|
33
|
+
pt: "Europe/Lisbon"
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/request.ts
|
|
38
|
+
/**
|
|
39
|
+
* @fileoverview Request-scoped configuration for next-intl
|
|
40
|
+
*
|
|
41
|
+
* This file is used by the next-intl plugin to provide
|
|
42
|
+
* messages and other i18n settings to Server Components.
|
|
43
|
+
* Configures locale detection, currency, timezone, and formatting.
|
|
44
|
+
*
|
|
45
|
+
* @module @od-oneapp/internationalization/request
|
|
46
|
+
*/
|
|
47
|
+
var request_default = getRequestConfig(async ({ requestLocale }) => {
|
|
48
|
+
const requested = await requestLocale;
|
|
49
|
+
const locale = requested && hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
|
|
50
|
+
if (requested && requested !== locale) logWarn(`Unsupported locale "${requested}" requested, falling back to "${locale}"`, {
|
|
51
|
+
requested,
|
|
52
|
+
locale,
|
|
53
|
+
context: {
|
|
54
|
+
function: "getRequestConfig",
|
|
55
|
+
package: "@od-oneapp/internationalization"
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
const messages = (await import(`./dictionaries/${locale}.json`)).default;
|
|
60
|
+
const currency = LOCALE_CURRENCY_MAP[locale] ?? DEFAULT_CURRENCY;
|
|
61
|
+
const timeZone = LOCALE_TIMEZONE_MAP[locale] ?? DEFAULT_TIMEZONE;
|
|
62
|
+
return {
|
|
63
|
+
locale,
|
|
64
|
+
messages,
|
|
65
|
+
formats: {
|
|
66
|
+
dateTime: { short: {
|
|
67
|
+
day: "numeric",
|
|
68
|
+
month: "short",
|
|
69
|
+
year: "numeric"
|
|
70
|
+
} },
|
|
71
|
+
number: { currency: {
|
|
72
|
+
style: "currency",
|
|
73
|
+
currency
|
|
74
|
+
} }
|
|
75
|
+
},
|
|
76
|
+
timeZone,
|
|
77
|
+
now: /* @__PURE__ */ new Date()
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const errorMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
81
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
82
|
+
logError(`Failed to load messages for locale "${locale}"`, {
|
|
83
|
+
error: errorMessage,
|
|
84
|
+
stack: errorStack,
|
|
85
|
+
locale,
|
|
86
|
+
context: {
|
|
87
|
+
function: "getRequestConfig",
|
|
88
|
+
package: "@od-oneapp/internationalization"
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
return {
|
|
93
|
+
locale: "en",
|
|
94
|
+
messages: (await import("./en-BG_-Eo3m.mjs")).default,
|
|
95
|
+
timeZone: LOCALE_TIMEZONE_MAP.en ?? DEFAULT_TIMEZONE,
|
|
96
|
+
now: /* @__PURE__ */ new Date()
|
|
97
|
+
};
|
|
98
|
+
} catch (fallbackError) {
|
|
99
|
+
logError("Failed to load fallback messages", {
|
|
100
|
+
error: fallbackError instanceof Error ? fallbackError.message : typeof fallbackError === "string" ? fallbackError : "Unknown error",
|
|
101
|
+
stack: fallbackError instanceof Error ? fallbackError.stack : void 0,
|
|
102
|
+
context: {
|
|
103
|
+
function: "getRequestConfig",
|
|
104
|
+
package: "@od-oneapp/internationalization"
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
throw new Error("Unable to load any translation messages");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
export { request_default as default };
|
|
114
|
+
//# sourceMappingURL=request.mjs.map
|