intor 2.2.1 → 2.2.3

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/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import path from 'path';
2
- import { performance } from 'perf_hooks';
2
+ import { performance as performance$1 } from 'perf_hooks';
3
3
  import pLimit from 'p-limit';
4
4
  import fs from 'fs/promises';
5
5
  import { logry } from 'logry';
6
+ import merge from 'lodash.merge';
6
7
  import Keyv from 'keyv';
7
8
  import { Translator } from 'intor-translator';
8
9
  export { Translator } from 'intor-translator';
@@ -11,7 +12,7 @@ export { Translator } from 'intor-translator';
11
12
  var shouldLoadMessages = (loader) => {
12
13
  if (!loader) return false;
13
14
  const { type, lazyLoad } = loader;
14
- if (type === "import") return true;
15
+ if (type === "local") return true;
15
16
  if (lazyLoad) return false;
16
17
  return true;
17
18
  };
@@ -40,17 +41,18 @@ var DEFAULT_FORMATTER_CONFIG = {
40
41
  node: { meta: { compact: true }, lineBreaksAfter: 1 }
41
42
  };
42
43
  function getLogger({
43
- id,
44
+ id = "default",
44
45
  formatterConfig,
45
46
  preset,
46
47
  ...options
47
48
  }) {
48
49
  const pool = getGlobalLoggerPool();
49
50
  let logger = pool.get(id);
51
+ const useDefault = !formatterConfig && !preset;
50
52
  if (!logger) {
51
53
  logger = logry({
52
54
  id,
53
- formatterConfig: !formatterConfig && !preset ? DEFAULT_FORMATTER_CONFIG : formatterConfig,
55
+ formatterConfig: useDefault ? DEFAULT_FORMATTER_CONFIG : formatterConfig,
54
56
  preset,
55
57
  ...options
56
58
  });
@@ -63,306 +65,173 @@ function getLogger({
63
65
  return logger;
64
66
  }
65
67
 
66
- // src/shared/error/intor-error.ts
67
- var IntorError = class extends Error {
68
- constructor({ message, code, id }) {
69
- const fullMessage = id ? `[${id}] ${message}` : message;
70
- super(fullMessage);
71
- this.name = "IntorError";
72
- this.id = id;
73
- this.code = code;
74
- Object.setPrototypeOf(this, new.target.prototype);
75
- }
76
- };
77
-
78
- // src/modules/messages/load-local-messages/load-namespace-group/parse-message-file.ts
79
- var MAX_PATH_LENGTH = 260;
80
- var parseMessageFile = async (filePath, loggerOptions) => {
68
+ // src/modules/messages/load-local-messages/read-locale-messages/collect-file-entries/collect-file-entries.ts
69
+ async function collectFileEntries({
70
+ readdir = fs.readdir,
71
+ limit,
72
+ rootDir,
73
+ namespaces,
74
+ extraOptions: { exts = [".json"], loggerOptions } = {}
75
+ }) {
81
76
  const baseLogger = getLogger({ ...loggerOptions });
82
- const logger = baseLogger.child({ scope: "parse-message-file" });
83
- const trimmedPath = filePath.trim();
84
- if (!trimmedPath) {
85
- logger.warn("File path is empty.", { filePath: trimmedPath });
86
- return null;
87
- }
88
- if (trimmedPath.length > MAX_PATH_LENGTH) {
89
- logger.warn("File path exceeds maximum length.", { filePath: trimmedPath });
90
- return null;
91
- }
92
- const fileName = path.basename(trimmedPath);
93
- if (!fileName.toLowerCase().endsWith(".json")) {
94
- logger.trace("Skipped non-JSON file.", { filePath: trimmedPath });
95
- return null;
96
- }
97
- try {
98
- const content = await fs.readFile(trimmedPath, "utf8");
99
- const parsed = JSON.parse(content);
100
- if (typeof parsed !== "object" || parsed === null) {
101
- throw new IntorError({
102
- id: loggerOptions.id,
103
- code: "INTOR_INVALID_MESSAGE_FORMAT" /* INVALID_MESSAGE_FORMAT */,
104
- message: "Invalid message format"
105
- });
77
+ const logger = baseLogger.child({ scope: "collect-file-entries" });
78
+ const results = [];
79
+ const walk = async (currentDir) => {
80
+ let entries = [];
81
+ try {
82
+ entries = await readdir(currentDir, { withFileTypes: true });
83
+ } catch (error) {
84
+ logger.error(`Error reading directory: ${currentDir}`, { error });
85
+ return;
106
86
  }
107
- logger.trace("Message file loaded.", { filePath: trimmedPath });
108
- return parsed;
109
- } catch (error) {
110
- logger.warn("Failed to parse message file.", {
111
- filePath: trimmedPath,
112
- error
87
+ const tasks = entries.map(
88
+ (entry) => limit(async () => {
89
+ const fullPath = path.join(currentDir, entry.name);
90
+ if (entry.isDirectory()) {
91
+ await walk(fullPath);
92
+ return;
93
+ }
94
+ if (!exts.some((ext2) => entry.name.endsWith(ext2))) return;
95
+ const relativePath = path.relative(rootDir, fullPath);
96
+ const ext = path.extname(relativePath);
97
+ const withoutExt = relativePath.slice(0, -ext.length);
98
+ const segments = withoutExt.split(path.sep).filter(Boolean);
99
+ const namespace = segments.at(0);
100
+ if (!namespace) return;
101
+ if (namespaces && namespace !== "index") {
102
+ if (!namespaces.includes(namespace)) return;
103
+ }
104
+ results.push({
105
+ namespace,
106
+ fullPath,
107
+ relativePath,
108
+ segments,
109
+ basename: path.basename(entry.name, ext)
110
+ });
111
+ })
112
+ );
113
+ await Promise.all(tasks);
114
+ };
115
+ await walk(rootDir);
116
+ if (logger.core.level === "debug") {
117
+ logger.debug("Local message files collected.", {
118
+ count: results.length
113
119
  });
114
- return null;
115
120
  }
116
- };
121
+ logger.trace("Local message files collected.", {
122
+ count: results.length,
123
+ fileEntries: results.map(({ namespace, relativePath }) => ({
124
+ namespace: namespace === "index" ? null : namespace,
125
+ relativePath
126
+ }))
127
+ });
128
+ return results;
129
+ }
117
130
 
118
- // src/modules/messages/load-local-messages/load-namespace-group/merge-namespace-messages.ts
119
- var mergeNamespaceMessages = async (filePaths, isAtRoot, loggerOptions) => {
120
- const baseContent = {};
121
- const subEntries = {};
122
- await Promise.all(
123
- filePaths.map(async (filePath) => {
124
- const fileName = path.basename(filePath);
125
- const content = await parseMessageFile(filePath, loggerOptions);
126
- if (!content) {
127
- return;
128
- }
129
- if (fileName === "index.json" || isAtRoot) {
130
- Object.assign(baseContent, content);
131
+ // src/modules/messages/shared/utils/is-valid-messages.ts
132
+ function isPlainObject(value) {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
135
+ function isValidMessages(value) {
136
+ if (!isPlainObject(value)) return false;
137
+ const stack = [value];
138
+ while (stack.length > 0) {
139
+ const current = stack.pop();
140
+ for (const v of Object.values(current)) {
141
+ if (typeof v === "string") continue;
142
+ if (isPlainObject(v)) {
143
+ stack.push(v);
131
144
  } else {
132
- const name = fileName.replace(/\.json$/, "");
133
- subEntries[name] = content;
145
+ return false;
134
146
  }
135
- })
136
- );
137
- return { base: baseContent, sub: subEntries };
138
- };
139
-
140
- // src/modules/messages/load-local-messages/load-namespace-group/load-namespace-group.ts
141
- var loadNamespaceGroup = async ({
142
- locale,
143
- namespace,
144
- messages,
145
- namespaceGroupValue,
146
- limit,
147
- logger: loggerOptions = { id: "default" }
148
- }) => {
149
- const baseLogger = getLogger({ ...loggerOptions });
150
- const logger = baseLogger.child({ scope: "load-namespace-group" });
151
- const { isAtRoot, filePaths } = namespaceGroupValue;
152
- if (filePaths.length === 0) {
153
- logger.trace(
154
- `Skipped merging ${locale}/${namespace} because filePaths is empty`
155
- );
156
- return;
157
- }
158
- return limit(async () => {
159
- const { base, sub } = await mergeNamespaceMessages(
160
- filePaths,
161
- isAtRoot,
162
- loggerOptions
163
- );
164
- if (!messages[locale]) {
165
- messages[locale] = {};
166
- }
167
- if (isAtRoot && filePaths.length === 1 && path.basename(filePaths[0]) === "index.json") {
168
- messages[locale] = { ...messages[locale], ...base };
169
- return;
170
147
  }
171
- const finalContent = isAtRoot ? base : { ...base, ...sub };
172
- messages[locale][namespace] = finalContent;
173
- if (!isAtRoot && Object.keys(finalContent).length > 0) {
174
- logger.trace(
175
- `Merged ${locale}/${namespace} from ${filePaths.length} file(s)`,
176
- { namespace }
177
- );
178
- }
179
- });
180
- };
181
- var addToNamespaceGroup = ({
182
- options: { namespaces },
183
- filePath,
184
- namespaceGroups,
185
- namespacePathSegments
186
- }) => {
187
- if (!filePath) {
188
- return;
189
- }
190
- const isAtRoot = namespacePathSegments.length === 0;
191
- const nsKey = isAtRoot ? path.basename(filePath, ".json") : namespacePathSegments.join(".");
192
- if (namespaces && namespaces.size > 0 && !namespaces.has(nsKey)) {
193
- return;
194
148
  }
195
- const group = namespaceGroups.get(nsKey) || {
196
- isAtRoot,
197
- filePaths: []
198
- };
199
- const filePathsSet = new Set(group.filePaths);
200
- if (!filePathsSet.has(filePath)) {
201
- filePathsSet.add(filePath);
202
- group.filePaths = [...filePathsSet];
203
- namespaceGroups.set(nsKey, group);
149
+ return true;
150
+ }
151
+ async function jsonReader(filePath, readFile = fs.readFile) {
152
+ const raw = await readFile(filePath, "utf8");
153
+ const parsed = JSON.parse(raw);
154
+ return parsed;
155
+ }
156
+
157
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/nest-object-from-path.ts
158
+ function nestObjectFromPath(path5, value) {
159
+ let obj = value;
160
+ for (let i = path5.length - 1; i >= 0; i--) {
161
+ obj = { [path5[i]]: obj };
204
162
  }
205
- };
163
+ return obj;
164
+ }
206
165
 
207
- // src/modules/messages/load-local-messages/prepare-namespace-groups/traverse-directory.ts
208
- var traverseDirectory = async ({
209
- options,
210
- currentDirPath,
211
- namespaceGroups,
212
- namespacePathSegments,
213
- readdir = fs.readdir
214
- }) => {
215
- const { limit } = options;
216
- const loggerOptions = options.logger || { id: "default" };
166
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.ts
167
+ async function parseFileEntries({
168
+ fileEntries,
169
+ limit,
170
+ extraOptions: { messagesReader, loggerOptions } = {}
171
+ }) {
217
172
  const baseLogger = getLogger({ ...loggerOptions });
218
- const logger = baseLogger.child({ scope: "traverse-directory" });
219
- try {
220
- const dirents = await readdir(currentDirPath, { withFileTypes: true });
221
- const dirPromises = dirents.map(
222
- (dirent) => limit(async () => {
223
- const filePath = path.join(currentDirPath, dirent.name);
224
- if (dirent.isFile() && dirent.name.endsWith(".json")) {
225
- addToNamespaceGroup({
226
- namespaceGroups,
227
- filePath,
228
- namespacePathSegments,
229
- options
230
- });
231
- } else if (dirent.isDirectory()) {
232
- await traverseDirectory({
233
- namespaceGroups,
234
- currentDirPath: filePath,
235
- namespacePathSegments: [...namespacePathSegments, dirent.name],
236
- options,
237
- readdir
238
- });
173
+ const logger = baseLogger.child({ scope: "parse-file-entries" });
174
+ const parsedFileEntries = [];
175
+ const tasks = fileEntries.map(
176
+ ({ namespace, segments, basename, fullPath }) => limit(async () => {
177
+ try {
178
+ const segsWithoutNs = segments.slice(1);
179
+ const ext = path.extname(fullPath);
180
+ const json = ext !== ".json" && messagesReader ? await messagesReader(fullPath) : await jsonReader(fullPath);
181
+ if (!isValidMessages(json)) {
182
+ throw new Error(
183
+ "JSON file does not match NamespaceMessages structure"
184
+ );
239
185
  }
240
- }).catch((error) => {
241
- logger.warn("Failed to process a locale file or directory.", {
242
- name: dirent.name,
243
- type: dirent.isFile() ? "file" : "directory",
244
- path: currentDirPath,
186
+ const isIndex = basename === "index";
187
+ const keyPath = isIndex ? segsWithoutNs.slice(0, -1) : segsWithoutNs;
188
+ const nested = nestObjectFromPath(keyPath, json);
189
+ parsedFileEntries.push({ namespace, messages: nested });
190
+ logger.trace("Parsed file.", { path: fullPath });
191
+ } catch (error) {
192
+ logger.error("Failed to read or parse file.", {
193
+ path: fullPath,
245
194
  error
246
195
  });
247
- })
248
- );
249
- await Promise.all(dirPromises);
250
- } catch (error) {
251
- logger.warn(`Error reading directory: ${currentDirPath}`, { error });
196
+ }
197
+ })
198
+ );
199
+ await Promise.all(tasks);
200
+ const result = {};
201
+ for (const { namespace, messages } of parsedFileEntries) {
202
+ if (namespace === "index") {
203
+ merge(result, messages);
204
+ } else {
205
+ result[namespace] = merge(
206
+ result[namespace] ?? {},
207
+ messages
208
+ );
209
+ }
252
210
  }
253
- };
254
-
255
- // src/modules/messages/load-local-messages/prepare-namespace-groups/prepare-namespace-groups.ts
256
- var prepareNamespaceGroups = async (options) => {
257
- const { basePath } = options;
258
- const namespaceGroups = /* @__PURE__ */ new Map();
259
- await traverseDirectory({
260
- options,
261
- currentDirPath: basePath,
262
- namespaceGroups,
263
- namespacePathSegments: []
264
- });
265
- return namespaceGroups;
266
- };
211
+ return result;
212
+ }
267
213
 
268
- // src/modules/messages/load-local-messages/load-single-locale/load-single-locale.ts
269
- var loadSingleLocale = async ({
270
- basePath,
214
+ // src/modules/messages/load-local-messages/read-locale-messages/read-locale-messages.ts
215
+ var readLocaleMessages = async ({
216
+ limit,
217
+ rootDir = "messages",
271
218
  locale,
272
219
  namespaces,
273
- messages,
274
- limit,
275
- logger: loggerOptions = { id: "default" }
220
+ extraOptions: { exts, messagesReader, loggerOptions } = {}
276
221
  }) => {
277
- const baseLogger = getLogger({ ...loggerOptions });
278
- const logger = baseLogger.child({ scope: "load-single-locale" });
279
- const localePath = path.join(basePath, locale);
280
- const validNamespaces = [];
281
- try {
282
- const stat = await fs.stat(localePath);
283
- if (!stat.isDirectory()) {
284
- logger.warn("Locale path is not a directory.", {
285
- locale,
286
- path: localePath
287
- });
288
- return;
289
- }
290
- } catch (error) {
291
- logger.warn("Error checking locale path.", { locale, error });
292
- return;
293
- }
294
- const namespaceGroups = await prepareNamespaceGroups({
295
- basePath: localePath,
222
+ const fileEntries = await collectFileEntries({
223
+ rootDir: path.resolve(process.cwd(), rootDir, locale),
224
+ namespaces,
296
225
  limit,
297
- namespaces: new Set(namespaces || []),
298
- logger: loggerOptions
226
+ extraOptions: { exts, loggerOptions }
299
227
  });
300
- if (namespaceGroups.size === 0) {
301
- logger.warn("No namespace groups found.", {
302
- locale,
303
- basePath,
304
- namespaces
305
- });
306
- return;
307
- }
308
- logger.trace("Prepared namespace groups from scanning local files.", {
309
- namespaceGroups: [...namespaceGroups.entries()].map(([ns, val]) => ({
310
- namespace: ns,
311
- isAtRoot: val.isAtRoot,
312
- fileCount: val.filePaths.length
313
- }))
314
- });
315
- const namespaceGroupTasks = [...namespaceGroups.entries()].filter(
316
- ([ns]) => !namespaces || namespaces.length === 0 || namespaces.includes(ns)
317
- ).map(
318
- ([namespace, namespaceGroupValue]) => loadNamespaceGroup({
319
- locale,
320
- namespace,
321
- messages,
322
- namespaceGroupValue,
323
- limit,
324
- logger: loggerOptions
325
- }).then(() => validNamespaces.push(namespace))
326
- );
327
- await Promise.all(namespaceGroupTasks);
328
- return validNamespaces;
329
- };
330
-
331
- // src/modules/messages/load-local-messages/load-locale-with-fallback/load-locale-with-fallback.ts
332
- var loadLocaleWithFallback = async ({
333
- basePath,
334
- locale: targetLocale,
335
- fallbackLocales = [],
336
- namespaces,
337
- messages,
338
- limit,
339
- logger: loggerOptions = { id: "default" }
340
- }) => {
341
- const baseLogger = getLogger({ ...loggerOptions });
342
- const logger = baseLogger.child({ scope: "load-locale-with-fallback" });
343
- const candidateLocales = [targetLocale, ...fallbackLocales];
344
- for (const locale of candidateLocales) {
345
- try {
346
- const validNamespaces = await loadSingleLocale({
347
- basePath,
348
- locale,
349
- namespaces,
350
- messages,
351
- limit,
352
- logger: loggerOptions
353
- });
354
- return validNamespaces;
355
- } catch (error) {
356
- logger.warn("Error occurred while processing the locale.", {
357
- locale,
358
- error
359
- });
360
- }
361
- }
362
- logger.warn("All fallback locales failed.", {
363
- attemptedLocales: candidateLocales
228
+ const namespaceMessages = await parseFileEntries({
229
+ fileEntries,
230
+ limit,
231
+ extraOptions: { messagesReader, loggerOptions }
364
232
  });
365
- return;
233
+ const localeMessages = { [locale]: namespaceMessages };
234
+ return localeMessages;
366
235
  };
367
236
  function getGlobalMessagesPool() {
368
237
  if (!globalThis.__INTOR_MESSAGES_POOL__) {
@@ -374,22 +243,9 @@ function clearMessagesPool() {
374
243
  const pool = getGlobalMessagesPool();
375
244
  pool.clear();
376
245
  }
377
-
378
- // src/shared/utils/merge-messages.ts
379
246
  var mergeMessages = (staticMessages = {}, loadedMessages = {}) => {
380
- const result = Object.keys(staticMessages).length > 0 ? { ...staticMessages } : {};
381
- for (const locale in loadedMessages) {
382
- const loaded = loadedMessages[locale];
383
- if (!result[locale]) {
384
- result[locale] = loaded;
385
- continue;
386
- }
387
- result[locale] = {
388
- ...result[locale],
389
- ...loaded
390
- };
391
- }
392
- return result;
247
+ if (!loadedMessages) return { ...staticMessages };
248
+ return merge({}, staticMessages, loadedMessages);
393
249
  };
394
250
 
395
251
  // src/shared/utils/normalize-cache-key.ts
@@ -421,6 +277,8 @@ var resolveNamespaces = ({
421
277
  }) => {
422
278
  const { loader } = config;
423
279
  const { routeNamespaces = {}, namespaces } = loader || {};
280
+ if (Object.keys(routeNamespaces).length === 0 && !namespaces)
281
+ return void 0;
424
282
  const standardizedPathname = standardizePathname({ config, pathname });
425
283
  const placeholderRemovedPathname = standardizedPathname.replace(
426
284
  `/${PREFIX_PLACEHOLDER}`,
@@ -578,43 +436,35 @@ var standardizePathname = ({
578
436
 
579
437
  // src/modules/messages/load-local-messages/load-local-messages.ts
580
438
  var loadLocalMessages = async ({
581
- basePath,
439
+ pool = getGlobalMessagesPool(),
440
+ rootDir = "messages",
582
441
  locale,
583
442
  fallbackLocales,
584
443
  namespaces,
585
- concurrency = 10,
586
- cache = DEFAULT_CACHE_OPTIONS,
587
- logger: loggerOptions = { id: "default" }
444
+ extraOptions: {
445
+ concurrency = 10,
446
+ cacheOptions = DEFAULT_CACHE_OPTIONS,
447
+ loggerOptions = { id: "default" },
448
+ exts,
449
+ messagesReader
450
+ } = {}
588
451
  }) => {
589
- basePath = basePath ?? "messages";
590
- if (!locale || locale.trim() === "") return {};
591
452
  const baseLogger = getLogger({ ...loggerOptions });
592
- const logger = baseLogger.child({ scope: "load-locale-messages" });
593
- const messages = {};
594
- const resolvedBasePath = path.resolve(
595
- process.cwd(),
596
- normalizePathname(basePath, { removeLeadingSlash: true })
597
- );
598
- const start = performance.now();
599
- logger.trace("Starting to load local messages with configuration.", {
600
- path: { basePath, resolvedBasePath },
601
- locale,
602
- fallbackLocales,
603
- namespaces: namespaces && namespaces.length > 0 ? { count: namespaces?.length, list: [...namespaces] } : "All Namespaces",
604
- concurrency
453
+ const logger = baseLogger.child({ scope: "load-local-messages" });
454
+ const start = performance$1.now();
455
+ logger.debug("Loading local messages from directory.", {
456
+ rootDir,
457
+ resolvedRootDir: path.resolve(process.cwd(), rootDir)
605
458
  });
606
- let pool;
607
- if (cache.enabled) {
608
- pool = getGlobalMessagesPool();
609
- }
610
459
  const key = normalizeCacheKey([
611
460
  loggerOptions.id,
612
- resolvedBasePath,
461
+ "loaderType:local",
462
+ rootDir,
613
463
  locale,
614
- (fallbackLocales ?? []).toSorted().join(","),
615
- (namespaces ?? []).toSorted().join(",")
464
+ (fallbackLocales || []).toSorted().join(","),
465
+ (namespaces || []).toSorted().join(",")
616
466
  ]);
617
- if (cache.enabled && key) {
467
+ if (cacheOptions.enabled && key) {
618
468
  const cached = await pool?.get(key);
619
469
  if (cached) {
620
470
  logger.debug("Messages cache hit.", { key });
@@ -622,50 +472,57 @@ var loadLocalMessages = async ({
622
472
  }
623
473
  }
624
474
  const limit = pLimit(concurrency);
625
- const validNamespaces = await loadLocaleWithFallback({
626
- basePath: resolvedBasePath,
627
- locale,
628
- fallbackLocales,
629
- namespaces,
630
- messages,
631
- limit,
632
- logger: loggerOptions
633
- });
634
- if (cache.enabled && key) {
635
- await pool?.set(key, messages, cache.ttl);
475
+ const candidateLocales = [locale, ...fallbackLocales || []];
476
+ let messages;
477
+ for (const candidateLocale of candidateLocales) {
478
+ try {
479
+ const result = await readLocaleMessages({
480
+ limit,
481
+ rootDir,
482
+ locale: candidateLocale,
483
+ namespaces,
484
+ extraOptions: { loggerOptions, exts, messagesReader }
485
+ });
486
+ if (result && Object.values(result[candidateLocale] || {}).length > 0) {
487
+ messages = result;
488
+ break;
489
+ }
490
+ } catch (error) {
491
+ logger.error("Failed to read locale messages", {
492
+ locale: candidateLocale,
493
+ error
494
+ });
495
+ }
636
496
  }
637
- const end = performance.now();
497
+ if (cacheOptions.enabled && key && messages) {
498
+ await pool?.set(key, messages, cacheOptions.ttl);
499
+ }
500
+ const end = performance$1.now();
638
501
  const duration = Math.round(end - start);
639
502
  logger.trace("Finished loading local messages.", {
640
- locale,
641
- validNamespaces,
503
+ loadedLocale: messages ? Object.keys(messages)[0] : void 0,
642
504
  duration: `${duration} ms`
643
505
  });
644
506
  return messages;
645
507
  };
646
508
 
647
- // src/modules/messages/create-load-local-messages.ts
648
- var createLoadLocalMessages = (basePath) => {
649
- return (options) => loadLocalMessages({ basePath, ...options });
650
- };
651
-
652
- // src/modules/messages/load-api-messages/fetch-messages.ts
653
- var fetchMessages = async ({
654
- apiUrl,
655
- apiHeaders,
656
- locale,
509
+ // src/modules/messages/load-remote-messages/fetch-locale-messages/fetch-locale-messages.ts
510
+ var fetchLocaleMessages = async ({
511
+ remoteUrl,
512
+ remoteHeaders,
657
513
  searchParams,
658
- logger: loggerOptions = { id: "default" }
514
+ locale,
515
+ extraOptions: { loggerOptions } = {}
659
516
  }) => {
660
517
  const baseLogger = getLogger({ ...loggerOptions });
661
- const logger = baseLogger.child({ scope: "fetch-messages" });
518
+ const logger = baseLogger.child({ scope: "fetch-locale-messages" });
662
519
  try {
663
520
  const params = new URLSearchParams(searchParams);
664
521
  params.append("locale", locale);
665
- const url = `${apiUrl}?${params.toString()}`;
522
+ const url = `${remoteUrl}?${params.toString()}`;
666
523
  const headers = {
667
524
  "Content-Type": "application/json",
668
- ...apiHeaders
525
+ ...remoteHeaders
669
526
  };
670
527
  const response = await fetch(url, {
671
528
  method: "GET",
@@ -673,17 +530,17 @@ var fetchMessages = async ({
673
530
  cache: "no-store"
674
531
  });
675
532
  if (!response.ok) {
676
- throw new Error(`Fetch failed: ${locale} (${response.status})`);
533
+ throw new Error(`HTTP error ${response.status} ${response.statusText}`);
677
534
  }
678
535
  const data = await response.json();
679
- if (data == null || typeof data === "object" && Object.keys(data).length === 0) {
680
- throw new Error(`Invalid messages: ${locale}`);
536
+ if (!isValidMessages(data[locale])) {
537
+ throw new Error("JSON file does not match NamespaceMessages structure");
681
538
  }
682
539
  return data;
683
540
  } catch (error) {
684
- logger.warn(`Failed to fetch messages for locale "${locale}".`, {
541
+ logger.warn("Fetching locale messages failed.", {
685
542
  locale,
686
- apiUrl,
543
+ remoteUrl,
687
544
  searchParams: decodeURIComponent(searchParams.toString()),
688
545
  error
689
546
  });
@@ -691,30 +548,7 @@ var fetchMessages = async ({
691
548
  }
692
549
  };
693
550
 
694
- // src/modules/messages/load-api-messages/fetch-fallback-messages.ts
695
- var fetchFallbackMessages = async ({
696
- apiUrl,
697
- apiHeaders,
698
- searchParams,
699
- fallbackLocales,
700
- logger
701
- }) => {
702
- for (const fallbackLocale of fallbackLocales) {
703
- const result = await fetchMessages({
704
- apiUrl,
705
- searchParams,
706
- locale: fallbackLocale,
707
- apiHeaders,
708
- logger
709
- });
710
- if (result) {
711
- return { locale: fallbackLocale, messages: result };
712
- }
713
- }
714
- return;
715
- };
716
-
717
- // src/modules/messages/load-api-messages/utils/build-search-params.ts
551
+ // src/modules/messages/load-remote-messages/fetch-locale-messages/utils/build-search-params.ts
718
552
  var buildSearchParams = (params) => {
719
553
  const searchParams = new URLSearchParams();
720
554
  const appendParam = (key, value) => {
@@ -732,119 +566,130 @@ var buildSearchParams = (params) => {
732
566
  return searchParams;
733
567
  };
734
568
 
735
- // src/modules/messages/load-api-messages/load-api-messages.ts
736
- var loadApiMessages = async ({
737
- apiUrl,
738
- apiHeaders,
739
- basePath,
569
+ // src/modules/messages/load-remote-messages/load-remote-messages.ts
570
+ var loadRemoteMessages = async ({
571
+ pool = getGlobalMessagesPool(),
572
+ rootDir,
573
+ remoteUrl,
574
+ remoteHeaders,
740
575
  locale,
741
576
  fallbackLocales = [],
742
577
  namespaces = [],
743
- cache = DEFAULT_CACHE_OPTIONS,
744
- logger: loggerOptions = { id: "default" }
578
+ extraOptions: {
579
+ cacheOptions = DEFAULT_CACHE_OPTIONS,
580
+ loggerOptions = { id: "default" }
581
+ } = {}
745
582
  }) => {
746
583
  const baseLogger = getLogger({ ...loggerOptions });
747
- const logger = baseLogger.child({ scope: "load-api-messages" });
748
- if (!apiUrl) {
749
- logger.warn("No apiUrl provided. Skipping fetch.");
750
- return;
751
- }
752
- let pool;
753
- if (cache.enabled) {
754
- pool = getGlobalMessagesPool();
755
- }
584
+ const logger = baseLogger.child({ scope: "load-remote-messages" });
585
+ const start = performance.now();
586
+ logger.debug("Loading remote messages from api.", { remoteUrl });
756
587
  const key = normalizeCacheKey([
757
588
  loggerOptions.id,
758
- basePath,
589
+ "loaderType:remote",
590
+ rootDir,
759
591
  locale,
760
592
  (fallbackLocales ?? []).toSorted().join(","),
761
593
  (namespaces ?? []).toSorted().join(",")
762
594
  ]);
763
- if (cache.enabled && key) {
595
+ if (cacheOptions.enabled && key) {
764
596
  const cached = await pool?.get(key);
765
597
  if (cached) {
766
598
  logger.debug("Messages cache hit.", { key });
767
599
  return cached;
768
600
  }
769
601
  }
770
- const searchParams = buildSearchParams({ basePath, namespaces });
771
- const messages = await fetchMessages({
772
- apiUrl,
773
- apiHeaders,
774
- searchParams,
775
- locale,
776
- logger: loggerOptions
777
- });
778
- if (messages) {
779
- if (cache.enabled && key) {
780
- await pool?.set(key, messages, cache.ttl);
602
+ const searchParams = buildSearchParams({ rootDir, namespaces });
603
+ const candidateLocales = [locale, ...fallbackLocales || []];
604
+ let messages;
605
+ for (const candidateLocale of candidateLocales) {
606
+ try {
607
+ const result = await fetchLocaleMessages({
608
+ remoteUrl,
609
+ remoteHeaders,
610
+ searchParams,
611
+ locale: candidateLocale,
612
+ extraOptions: { loggerOptions }
613
+ });
614
+ if (result && Object.values(result[candidateLocale] || {}).length > 0) {
615
+ messages = result;
616
+ break;
617
+ }
618
+ } catch (error) {
619
+ logger.error("Failed to fetch locale messages.", {
620
+ locale: candidateLocale,
621
+ error
622
+ });
781
623
  }
782
- return messages;
783
624
  }
784
- const fallbackResult = await fetchFallbackMessages({
785
- apiUrl,
786
- apiHeaders,
787
- searchParams,
788
- fallbackLocales,
789
- logger: loggerOptions
790
- });
791
- if (fallbackResult) {
792
- logger.info("Fallback locale succeeded.", {
793
- usedLocale: fallbackResult.locale,
794
- apiUrl,
795
- searchParams: decodeURIComponent(searchParams.toString())
796
- });
797
- if (cache.enabled && key) {
798
- await pool?.set(key, fallbackResult.messages, cache.ttl);
799
- }
800
- return fallbackResult.messages;
625
+ if (cacheOptions.enabled && key && messages) {
626
+ await pool?.set(key, messages, cacheOptions.ttl);
801
627
  }
802
- logger.warn("Failed to fetch messages for all locales.", {
803
- locale,
804
- fallbackLocales
628
+ const end = performance.now();
629
+ const duration = Math.round(end - start);
630
+ logger.trace("Finished loading remote messages.", {
631
+ loadedLocale: messages ? Object.keys(messages)[0] : void 0,
632
+ duration: `${duration} ms`
805
633
  });
806
- return;
634
+ return messages;
807
635
  };
808
636
 
809
637
  // src/modules/messages/load-messages.ts
810
638
  var loadMessages = async ({
811
639
  config,
812
640
  locale,
813
- pathname
641
+ pathname = "",
642
+ extraOptions: { exts, messagesReader } = {}
814
643
  }) => {
815
644
  const baseLogger = getLogger({ id: config.id, ...config.logger });
816
- const logger = baseLogger.child({ scope: "messages-loader" });
645
+ const logger = baseLogger.child({ scope: "load-messages" });
817
646
  if (!config.loader) {
818
647
  logger.warn(
819
648
  "No loader options have been configured in the current config."
820
649
  );
821
650
  return;
822
651
  }
823
- const { id, loader, cache } = config;
652
+ const { type, concurrency, rootDir } = config.loader;
824
653
  const fallbackLocales = config.fallbackLocales[locale] || [];
825
654
  const namespaces = resolveNamespaces({ config, pathname });
826
- logger.debug("Namespaces ready for loading.", {
827
- namespaces: namespaces.length > 0 ? { count: namespaces.length, list: [...namespaces] } : "All Namespaces"
655
+ if (logger.core.level === "debug") {
656
+ logger.debug("Starting to load messages.", { locale });
657
+ }
658
+ logger.trace("Starting to load messages with runtime context.", {
659
+ loaderType: type,
660
+ locale,
661
+ fallbackLocales,
662
+ namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : "[ALL]",
663
+ cache: config.cache,
664
+ concurrency: concurrency ?? 10
828
665
  });
829
- logger.debug("Loader type selected.", { loaderType: loader.type });
830
666
  let loadedMessages;
831
- if (loader.type === "import") {
832
- const loadLocalMessages2 = createLoadLocalMessages(loader.basePath);
833
- loadedMessages = await loadLocalMessages2({
834
- ...loader,
667
+ if (type === "local") {
668
+ loadedMessages = await loadLocalMessages({
669
+ rootDir,
835
670
  locale,
836
671
  fallbackLocales,
837
672
  namespaces,
838
- cache,
839
- logger: { id }
673
+ extraOptions: {
674
+ concurrency,
675
+ cacheOptions: config.cache,
676
+ loggerOptions: { id: config.id, ...config.logger },
677
+ exts,
678
+ messagesReader
679
+ }
840
680
  });
841
- } else if (loader.type === "api") {
842
- loadedMessages = await loadApiMessages({
843
- ...loader,
681
+ } else if (type === "remote") {
682
+ loadedMessages = await loadRemoteMessages({
683
+ rootDir,
684
+ remoteUrl: config.loader.remoteUrl,
685
+ remoteHeaders: config.loader.remoteHeaders,
844
686
  locale,
845
687
  fallbackLocales,
846
688
  namespaces,
847
- logger: { id }
689
+ extraOptions: {
690
+ cacheOptions: config.cache,
691
+ loggerOptions: { id: config.id, ...config.logger }
692
+ }
848
693
  });
849
694
  }
850
695
  if (!loadedMessages || Object.keys(loadedMessages).length === 0) {
@@ -854,7 +699,7 @@ var loadMessages = async ({
854
699
  };
855
700
 
856
701
  // src/modules/intor/intor.ts
857
- var intor = async (config, i18nContext) => {
702
+ var intor = async (config, i18nContext, loadMessagesOptions = {}) => {
858
703
  const baseLogger = getLogger({ id: config.id, ...config.logger });
859
704
  const logger = baseLogger.child({ scope: "intor" });
860
705
  logger.info("Start Intor initialization.");
@@ -865,20 +710,28 @@ var intor = async (config, i18nContext) => {
865
710
  pathname: i18nContext?.pathname || ""
866
711
  };
867
712
  const { locale, pathname } = context;
868
- const source = isI18nContextFunction ? "[function]" : "[static object]";
869
- logger.debug(`Context resolved via ${source}.`, context);
713
+ const source = isI18nContextFunction ? i18nContext.name : "static fallback";
714
+ logger.debug(`I18n context resolved via ${source}.`, context);
870
715
  let loadedMessages;
871
716
  if (shouldLoadMessages(loader)) {
872
- loadedMessages = await loadMessages({ config, locale, pathname });
717
+ loadedMessages = await loadMessages({
718
+ config,
719
+ locale,
720
+ pathname,
721
+ extraOptions: {
722
+ exts: loadMessagesOptions.exts,
723
+ messagesReader: loadMessagesOptions.messagesReader
724
+ }
725
+ });
873
726
  }
874
727
  const mergedMessages = mergeMessages(messages, loadedMessages);
875
- logger.info("Messages initialized.", {
876
- staticMessages: { enabled: !!messages },
877
- loadedMessages: {
728
+ logger.info("Messages successfully initialized.", {
729
+ static: { enabled: !!messages },
730
+ loaded: {
878
731
  enabled: !!loadedMessages,
879
732
  ...loader ? { loaderType: loader.type, lazyLoad: !!loader.lazyLoad } : null
880
733
  },
881
- mergedMessages
734
+ merged: mergedMessages
882
735
  });
883
736
  return {
884
737
  config,
@@ -907,4 +760,4 @@ async function getTranslator(opts) {
907
760
  };
908
761
  }
909
762
 
910
- export { PREFIX_PLACEHOLDER, clearLoggerPool, clearMessagesPool, extractPathname, getTranslator, intor, loadApiMessages, loadLocalMessages, loadMessages, mergeMessages, normalizeCacheKey, normalizeLocale, normalizePathname, resolveNamespaces, resolvePreferredLocale, standardizePathname };
763
+ export { PREFIX_PLACEHOLDER, clearLoggerPool, clearMessagesPool, extractPathname, getTranslator, intor, loadLocalMessages, loadMessages, loadRemoteMessages, mergeMessages, normalizeCacheKey, normalizeLocale, normalizePathname, resolveNamespaces, resolvePreferredLocale, standardizePathname };