intor 2.2.1 → 2.2.2

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.
@@ -7,6 +7,7 @@ var path = require('path');
7
7
  var perf_hooks = require('perf_hooks');
8
8
  var pLimit = require('p-limit');
9
9
  var fs = require('fs/promises');
10
+ var merge = require('lodash.merge');
10
11
  var Keyv = require('keyv');
11
12
 
12
13
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -14,6 +15,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
15
  var path__default = /*#__PURE__*/_interopDefault(path);
15
16
  var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
16
17
  var fs__default = /*#__PURE__*/_interopDefault(fs);
18
+ var merge__default = /*#__PURE__*/_interopDefault(merge);
17
19
  var Keyv__default = /*#__PURE__*/_interopDefault(Keyv);
18
20
 
19
21
  // src/adapters/next/server/get-i18n-context.ts
@@ -34,17 +36,18 @@ var DEFAULT_FORMATTER_CONFIG = {
34
36
  node: { meta: { compact: true }, lineBreaksAfter: 1 }
35
37
  };
36
38
  function getLogger({
37
- id,
39
+ id = "default",
38
40
  formatterConfig,
39
41
  preset,
40
42
  ...options
41
43
  }) {
42
44
  const pool = getGlobalLoggerPool();
43
45
  let logger = pool.get(id);
46
+ const useDefault = !formatterConfig && !preset;
44
47
  if (!logger) {
45
48
  logger = logry.logry({
46
49
  id,
47
- formatterConfig: !formatterConfig && !preset ? DEFAULT_FORMATTER_CONFIG : formatterConfig,
50
+ formatterConfig: useDefault ? DEFAULT_FORMATTER_CONFIG : formatterConfig,
48
51
  preset,
49
52
  ...options
50
53
  });
@@ -86,6 +89,8 @@ var resolveNamespaces = ({
86
89
  }) => {
87
90
  const { loader } = config;
88
91
  const { routeNamespaces = {}, namespaces } = loader || {};
92
+ if (Object.keys(routeNamespaces).length === 0 && !namespaces)
93
+ return void 0;
89
94
  const standardizedPathname = standardizePathname({ config, pathname });
90
95
  const placeholderRemovedPathname = standardizedPathname.replace(
91
96
  `/${PREFIX_PLACEHOLDER}`,
@@ -242,307 +247,171 @@ var DEFAULT_CACHE_OPTIONS = {
242
247
  ttl: 60 * 60 * 1e3
243
248
  // 1 hour
244
249
  };
245
-
246
- // src/shared/error/intor-error.ts
247
- var IntorError = class extends Error {
248
- constructor({ message, code, id }) {
249
- const fullMessage = id ? `[${id}] ${message}` : message;
250
- super(fullMessage);
251
- this.name = "IntorError";
252
- this.id = id;
253
- this.code = code;
254
- Object.setPrototypeOf(this, new.target.prototype);
255
- }
256
- };
257
-
258
- // src/modules/messages/load-local-messages/load-namespace-group/parse-message-file.ts
259
- var MAX_PATH_LENGTH = 260;
260
- var parseMessageFile = async (filePath, loggerOptions) => {
250
+ async function collectFileEntries({
251
+ readdir = fs__default.default.readdir,
252
+ limit,
253
+ rootDir,
254
+ namespaces,
255
+ extraOptions: { exts = [".json"], loggerOptions } = {}
256
+ }) {
261
257
  const baseLogger = getLogger({ ...loggerOptions });
262
- const logger = baseLogger.child({ scope: "parse-message-file" });
263
- const trimmedPath = filePath.trim();
264
- if (!trimmedPath) {
265
- logger.warn("File path is empty.", { filePath: trimmedPath });
266
- return null;
267
- }
268
- if (trimmedPath.length > MAX_PATH_LENGTH) {
269
- logger.warn("File path exceeds maximum length.", { filePath: trimmedPath });
270
- return null;
271
- }
272
- const fileName = path__default.default.basename(trimmedPath);
273
- if (!fileName.toLowerCase().endsWith(".json")) {
274
- logger.trace("Skipped non-JSON file.", { filePath: trimmedPath });
275
- return null;
276
- }
277
- try {
278
- const content = await fs__default.default.readFile(trimmedPath, "utf8");
279
- const parsed = JSON.parse(content);
280
- if (typeof parsed !== "object" || parsed === null) {
281
- throw new IntorError({
282
- id: loggerOptions.id,
283
- code: "INTOR_INVALID_MESSAGE_FORMAT" /* INVALID_MESSAGE_FORMAT */,
284
- message: "Invalid message format"
285
- });
258
+ const logger = baseLogger.child({ scope: "collect-file-entries" });
259
+ const results = [];
260
+ const walk = async (currentDir) => {
261
+ let entries = [];
262
+ try {
263
+ entries = await readdir(currentDir, { withFileTypes: true });
264
+ } catch (error) {
265
+ logger.error(`Error reading directory: ${currentDir}`, { error });
266
+ return;
286
267
  }
287
- logger.trace("Message file loaded.", { filePath: trimmedPath });
288
- return parsed;
289
- } catch (error) {
290
- logger.warn("Failed to parse message file.", {
291
- filePath: trimmedPath,
292
- error
268
+ const tasks = entries.map(
269
+ (entry) => limit(async () => {
270
+ const fullPath = path__default.default.join(currentDir, entry.name);
271
+ if (entry.isDirectory()) {
272
+ await walk(fullPath);
273
+ return;
274
+ }
275
+ if (!exts.some((ext2) => entry.name.endsWith(ext2))) return;
276
+ const relativePath = path__default.default.relative(rootDir, fullPath);
277
+ const ext = path__default.default.extname(relativePath);
278
+ const withoutExt = relativePath.slice(0, -ext.length);
279
+ const segments = withoutExt.split(path__default.default.sep).filter(Boolean);
280
+ const namespace = segments.at(0);
281
+ if (!namespace) return;
282
+ if (namespaces && namespace !== "index") {
283
+ if (!namespaces.includes(namespace)) return;
284
+ }
285
+ results.push({
286
+ namespace,
287
+ fullPath,
288
+ relativePath,
289
+ segments,
290
+ basename: path__default.default.basename(entry.name, ext)
291
+ });
292
+ })
293
+ );
294
+ await Promise.all(tasks);
295
+ };
296
+ await walk(rootDir);
297
+ if (logger.core.level === "debug") {
298
+ logger.debug("Local message files collected.", {
299
+ count: results.length
293
300
  });
294
- return null;
295
301
  }
296
- };
302
+ logger.trace("Local message files collected.", {
303
+ count: results.length,
304
+ fileEntries: results.map(({ namespace, relativePath }) => ({
305
+ namespace: namespace === "index" ? null : namespace,
306
+ relativePath
307
+ }))
308
+ });
309
+ return results;
310
+ }
297
311
 
298
- // src/modules/messages/load-local-messages/load-namespace-group/merge-namespace-messages.ts
299
- var mergeNamespaceMessages = async (filePaths, isAtRoot, loggerOptions) => {
300
- const baseContent = {};
301
- const subEntries = {};
302
- await Promise.all(
303
- filePaths.map(async (filePath) => {
304
- const fileName = path__default.default.basename(filePath);
305
- const content = await parseMessageFile(filePath, loggerOptions);
306
- if (!content) {
307
- return;
308
- }
309
- if (fileName === "index.json" || isAtRoot) {
310
- Object.assign(baseContent, content);
312
+ // src/modules/messages/shared/utils/is-namespace-messages.ts
313
+ function isPlainObject(value) {
314
+ return typeof value === "object" && value !== null && !Array.isArray(value);
315
+ }
316
+ function isNamespaceMessages(value) {
317
+ if (!isPlainObject(value)) return false;
318
+ const stack = [value];
319
+ while (stack.length > 0) {
320
+ const current = stack.pop();
321
+ for (const v of Object.values(current)) {
322
+ if (typeof v === "string") continue;
323
+ if (isPlainObject(v)) {
324
+ stack.push(v);
311
325
  } else {
312
- const name = fileName.replace(/\.json$/, "");
313
- subEntries[name] = content;
326
+ return false;
314
327
  }
315
- })
316
- );
317
- return { base: baseContent, sub: subEntries };
318
- };
319
-
320
- // src/modules/messages/load-local-messages/load-namespace-group/load-namespace-group.ts
321
- var loadNamespaceGroup = async ({
322
- locale,
323
- namespace,
324
- messages,
325
- namespaceGroupValue,
326
- limit,
327
- logger: loggerOptions = { id: "default" }
328
- }) => {
329
- const baseLogger = getLogger({ ...loggerOptions });
330
- const logger = baseLogger.child({ scope: "load-namespace-group" });
331
- const { isAtRoot, filePaths } = namespaceGroupValue;
332
- if (filePaths.length === 0) {
333
- logger.trace(
334
- `Skipped merging ${locale}/${namespace} because filePaths is empty`
335
- );
336
- return;
337
- }
338
- return limit(async () => {
339
- const { base, sub } = await mergeNamespaceMessages(
340
- filePaths,
341
- isAtRoot,
342
- loggerOptions
343
- );
344
- if (!messages[locale]) {
345
- messages[locale] = {};
346
- }
347
- if (isAtRoot && filePaths.length === 1 && path__default.default.basename(filePaths[0]) === "index.json") {
348
- messages[locale] = { ...messages[locale], ...base };
349
- return;
350
- }
351
- const finalContent = isAtRoot ? base : { ...base, ...sub };
352
- messages[locale][namespace] = finalContent;
353
- if (!isAtRoot && Object.keys(finalContent).length > 0) {
354
- logger.trace(
355
- `Merged ${locale}/${namespace} from ${filePaths.length} file(s)`,
356
- { namespace }
357
- );
358
328
  }
359
- });
360
- };
361
- var addToNamespaceGroup = ({
362
- options: { namespaces },
363
- filePath,
364
- namespaceGroups,
365
- namespacePathSegments
366
- }) => {
367
- if (!filePath) {
368
- return;
369
329
  }
370
- const isAtRoot = namespacePathSegments.length === 0;
371
- const nsKey = isAtRoot ? path__default.default.basename(filePath, ".json") : namespacePathSegments.join(".");
372
- if (namespaces && namespaces.size > 0 && !namespaces.has(nsKey)) {
373
- return;
330
+ return true;
331
+ }
332
+
333
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/json-reader.ts
334
+ async function jsonReader(filePath, readFile = fs__default.default.readFile) {
335
+ const raw = await readFile(filePath, "utf8");
336
+ const parsed = JSON.parse(raw);
337
+ if (!isNamespaceMessages(parsed)) {
338
+ throw new Error("JSON file does not match NamespaceMessages structure");
374
339
  }
375
- const group = namespaceGroups.get(nsKey) || {
376
- isAtRoot,
377
- filePaths: []
378
- };
379
- const filePathsSet = new Set(group.filePaths);
380
- if (!filePathsSet.has(filePath)) {
381
- filePathsSet.add(filePath);
382
- group.filePaths = [...filePathsSet];
383
- namespaceGroups.set(nsKey, group);
340
+ return parsed;
341
+ }
342
+
343
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/nest-object-from-path.ts
344
+ function nestObjectFromPath(path4, value) {
345
+ let obj = value;
346
+ for (let i = path4.length - 1; i >= 0; i--) {
347
+ obj = { [path4[i]]: obj };
384
348
  }
385
- };
349
+ return obj;
350
+ }
386
351
 
387
- // src/modules/messages/load-local-messages/prepare-namespace-groups/traverse-directory.ts
388
- var traverseDirectory = async ({
389
- options,
390
- currentDirPath,
391
- namespaceGroups,
392
- namespacePathSegments,
393
- readdir = fs__default.default.readdir
394
- }) => {
395
- const { limit } = options;
396
- const loggerOptions = options.logger || { id: "default" };
352
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.ts
353
+ async function parseFileEntries({
354
+ fileEntries,
355
+ limit,
356
+ extraOptions: { messageFileReader, loggerOptions } = {}
357
+ }) {
397
358
  const baseLogger = getLogger({ ...loggerOptions });
398
- const logger = baseLogger.child({ scope: "traverse-directory" });
399
- try {
400
- const dirents = await readdir(currentDirPath, { withFileTypes: true });
401
- const dirPromises = dirents.map(
402
- (dirent) => limit(async () => {
403
- const filePath = path__default.default.join(currentDirPath, dirent.name);
404
- if (dirent.isFile() && dirent.name.endsWith(".json")) {
405
- addToNamespaceGroup({
406
- namespaceGroups,
407
- filePath,
408
- namespacePathSegments,
409
- options
410
- });
411
- } else if (dirent.isDirectory()) {
412
- await traverseDirectory({
413
- namespaceGroups,
414
- currentDirPath: filePath,
415
- namespacePathSegments: [...namespacePathSegments, dirent.name],
416
- options,
417
- readdir
418
- });
419
- }
420
- }).catch((error) => {
421
- logger.warn("Failed to process a locale file or directory.", {
422
- name: dirent.name,
423
- type: dirent.isFile() ? "file" : "directory",
424
- path: currentDirPath,
359
+ const logger = baseLogger.child({ scope: "parse-file-entries" });
360
+ const parsedFileEntries = [];
361
+ const tasks = fileEntries.map(
362
+ ({ namespace, segments, basename, fullPath }) => limit(async () => {
363
+ try {
364
+ const segsWithoutNs = segments.slice(1);
365
+ const json = await (messageFileReader ? messageFileReader(fullPath) : jsonReader(fullPath));
366
+ const isIndex = basename === "index";
367
+ const keyPath = isIndex ? segsWithoutNs.slice(0, -1) : segsWithoutNs;
368
+ const namespaceMessages = nestObjectFromPath(keyPath, json);
369
+ parsedFileEntries.push({ namespace, namespaceMessages });
370
+ logger.trace("Parsed file.", { path: fullPath });
371
+ } catch (error) {
372
+ logger.error("Failed to read or parse file.", {
373
+ path: fullPath,
425
374
  error
426
375
  });
427
- })
428
- );
429
- await Promise.all(dirPromises);
430
- } catch (error) {
431
- logger.warn(`Error reading directory: ${currentDirPath}`, { error });
376
+ }
377
+ })
378
+ );
379
+ await Promise.all(tasks);
380
+ const result = {};
381
+ for (const { namespace, namespaceMessages } of parsedFileEntries) {
382
+ if (namespace === "index") {
383
+ merge__default.default(result, namespaceMessages);
384
+ } else {
385
+ result[namespace] = merge__default.default(
386
+ result[namespace] ?? {},
387
+ namespaceMessages
388
+ );
389
+ }
432
390
  }
433
- };
434
-
435
- // src/modules/messages/load-local-messages/prepare-namespace-groups/prepare-namespace-groups.ts
436
- var prepareNamespaceGroups = async (options) => {
437
- const { basePath } = options;
438
- const namespaceGroups = /* @__PURE__ */ new Map();
439
- await traverseDirectory({
440
- options,
441
- currentDirPath: basePath,
442
- namespaceGroups,
443
- namespacePathSegments: []
444
- });
445
- return namespaceGroups;
446
- };
391
+ return result;
392
+ }
447
393
 
448
- // src/modules/messages/load-local-messages/load-single-locale/load-single-locale.ts
449
- var loadSingleLocale = async ({
450
- basePath,
394
+ // src/modules/messages/load-local-messages/read-locale-messages/read-locale-messages.ts
395
+ var readLocaleMessages = async ({
396
+ limit,
397
+ rootDir = "messages",
451
398
  locale,
452
399
  namespaces,
453
- messages,
454
- limit,
455
- logger: loggerOptions = { id: "default" }
400
+ extraOptions: { exts, messageFileReader, loggerOptions } = {}
456
401
  }) => {
457
- const baseLogger = getLogger({ ...loggerOptions });
458
- const logger = baseLogger.child({ scope: "load-single-locale" });
459
- const localePath = path__default.default.join(basePath, locale);
460
- const validNamespaces = [];
461
- try {
462
- const stat = await fs__default.default.stat(localePath);
463
- if (!stat.isDirectory()) {
464
- logger.warn("Locale path is not a directory.", {
465
- locale,
466
- path: localePath
467
- });
468
- return;
469
- }
470
- } catch (error) {
471
- logger.warn("Error checking locale path.", { locale, error });
472
- return;
473
- }
474
- const namespaceGroups = await prepareNamespaceGroups({
475
- basePath: localePath,
402
+ const fileEntries = await collectFileEntries({
403
+ rootDir: path__default.default.resolve(process.cwd(), rootDir, locale),
404
+ namespaces,
476
405
  limit,
477
- namespaces: new Set(namespaces || []),
478
- logger: loggerOptions
406
+ extraOptions: { exts, loggerOptions }
479
407
  });
480
- if (namespaceGroups.size === 0) {
481
- logger.warn("No namespace groups found.", {
482
- locale,
483
- basePath,
484
- namespaces
485
- });
486
- return;
487
- }
488
- logger.trace("Prepared namespace groups from scanning local files.", {
489
- namespaceGroups: [...namespaceGroups.entries()].map(([ns, val]) => ({
490
- namespace: ns,
491
- isAtRoot: val.isAtRoot,
492
- fileCount: val.filePaths.length
493
- }))
494
- });
495
- const namespaceGroupTasks = [...namespaceGroups.entries()].filter(
496
- ([ns]) => !namespaces || namespaces.length === 0 || namespaces.includes(ns)
497
- ).map(
498
- ([namespace, namespaceGroupValue]) => loadNamespaceGroup({
499
- locale,
500
- namespace,
501
- messages,
502
- namespaceGroupValue,
503
- limit,
504
- logger: loggerOptions
505
- }).then(() => validNamespaces.push(namespace))
506
- );
507
- await Promise.all(namespaceGroupTasks);
508
- return validNamespaces;
509
- };
510
-
511
- // src/modules/messages/load-local-messages/load-locale-with-fallback/load-locale-with-fallback.ts
512
- var loadLocaleWithFallback = async ({
513
- basePath,
514
- locale: targetLocale,
515
- fallbackLocales = [],
516
- namespaces,
517
- messages,
518
- limit,
519
- logger: loggerOptions = { id: "default" }
520
- }) => {
521
- const baseLogger = getLogger({ ...loggerOptions });
522
- const logger = baseLogger.child({ scope: "load-locale-with-fallback" });
523
- const candidateLocales = [targetLocale, ...fallbackLocales];
524
- for (const locale of candidateLocales) {
525
- try {
526
- const validNamespaces = await loadSingleLocale({
527
- basePath,
528
- locale,
529
- namespaces,
530
- messages,
531
- limit,
532
- logger: loggerOptions
533
- });
534
- return validNamespaces;
535
- } catch (error) {
536
- logger.warn("Error occurred while processing the locale.", {
537
- locale,
538
- error
539
- });
540
- }
541
- }
542
- logger.warn("All fallback locales failed.", {
543
- attemptedLocales: candidateLocales
408
+ const namespaceMessages = await parseFileEntries({
409
+ fileEntries,
410
+ limit,
411
+ extraOptions: { messageFileReader, loggerOptions }
544
412
  });
545
- return;
413
+ const localeMessages = { [locale]: namespaceMessages };
414
+ return localeMessages;
546
415
  };
547
416
  function getGlobalMessagesPool() {
548
417
  if (!globalThis.__INTOR_MESSAGES_POOL__) {
@@ -553,43 +422,35 @@ function getGlobalMessagesPool() {
553
422
 
554
423
  // src/modules/messages/load-local-messages/load-local-messages.ts
555
424
  var loadLocalMessages = async ({
556
- basePath,
425
+ pool = getGlobalMessagesPool(),
426
+ rootDir = "messages",
557
427
  locale,
558
428
  fallbackLocales,
559
429
  namespaces,
560
- concurrency = 10,
561
- cache = DEFAULT_CACHE_OPTIONS,
562
- logger: loggerOptions = { id: "default" }
430
+ extraOptions: {
431
+ concurrency = 10,
432
+ cacheOptions = DEFAULT_CACHE_OPTIONS,
433
+ loggerOptions = { id: "default" },
434
+ exts,
435
+ messageFileReader
436
+ } = {}
563
437
  }) => {
564
- basePath = basePath ?? "messages";
565
- if (!locale || locale.trim() === "") return {};
566
438
  const baseLogger = getLogger({ ...loggerOptions });
567
- const logger = baseLogger.child({ scope: "load-locale-messages" });
568
- const messages = {};
569
- const resolvedBasePath = path__default.default.resolve(
570
- process.cwd(),
571
- normalizePathname(basePath, { removeLeadingSlash: true })
572
- );
439
+ const logger = baseLogger.child({ scope: "load-local-messages" });
573
440
  const start = perf_hooks.performance.now();
574
- logger.trace("Starting to load local messages with configuration.", {
575
- path: { basePath, resolvedBasePath },
576
- locale,
577
- fallbackLocales,
578
- namespaces: namespaces && namespaces.length > 0 ? { count: namespaces?.length, list: [...namespaces] } : "All Namespaces",
579
- concurrency
441
+ logger.debug("Loading local messages from directory.", {
442
+ rootDir,
443
+ resolvedRootDir: path__default.default.resolve(process.cwd(), rootDir)
580
444
  });
581
- let pool;
582
- if (cache.enabled) {
583
- pool = getGlobalMessagesPool();
584
- }
585
445
  const key = normalizeCacheKey([
586
446
  loggerOptions.id,
587
- resolvedBasePath,
447
+ "loaderType:local",
448
+ rootDir,
588
449
  locale,
589
- (fallbackLocales ?? []).toSorted().join(","),
590
- (namespaces ?? []).toSorted().join(",")
450
+ (fallbackLocales || []).toSorted().join(","),
451
+ (namespaces || []).toSorted().join(",")
591
452
  ]);
592
- if (cache.enabled && key) {
453
+ if (cacheOptions.enabled && key) {
593
454
  const cached = await pool?.get(key);
594
455
  if (cached) {
595
456
  logger.debug("Messages cache hit.", { key });
@@ -597,50 +458,57 @@ var loadLocalMessages = async ({
597
458
  }
598
459
  }
599
460
  const limit = pLimit__default.default(concurrency);
600
- const validNamespaces = await loadLocaleWithFallback({
601
- basePath: resolvedBasePath,
602
- locale,
603
- fallbackLocales,
604
- namespaces,
605
- messages,
606
- limit,
607
- logger: loggerOptions
608
- });
609
- if (cache.enabled && key) {
610
- await pool?.set(key, messages, cache.ttl);
461
+ const candidateLocales = [locale, ...fallbackLocales || []];
462
+ let messages;
463
+ for (const candidateLocale of candidateLocales) {
464
+ try {
465
+ const result = await readLocaleMessages({
466
+ limit,
467
+ rootDir,
468
+ locale: candidateLocale,
469
+ namespaces,
470
+ extraOptions: { loggerOptions, exts, messageFileReader }
471
+ });
472
+ if (result && Object.values(result[candidateLocale] || {}).length > 0) {
473
+ messages = result;
474
+ break;
475
+ }
476
+ } catch (error) {
477
+ logger.error("Failed to read locale messages", {
478
+ locale: candidateLocale,
479
+ error
480
+ });
481
+ }
482
+ }
483
+ if (cacheOptions.enabled && key && messages) {
484
+ await pool?.set(key, messages, cacheOptions.ttl);
611
485
  }
612
486
  const end = perf_hooks.performance.now();
613
487
  const duration = Math.round(end - start);
614
488
  logger.trace("Finished loading local messages.", {
615
- locale,
616
- validNamespaces,
489
+ loadedLocale: messages ? Object.keys(messages)[0] : void 0,
617
490
  duration: `${duration} ms`
618
491
  });
619
492
  return messages;
620
493
  };
621
494
 
622
- // src/modules/messages/create-load-local-messages.ts
623
- var createLoadLocalMessages = (basePath) => {
624
- return (options) => loadLocalMessages({ basePath, ...options });
625
- };
626
-
627
- // src/modules/messages/load-api-messages/fetch-messages.ts
628
- var fetchMessages = async ({
629
- apiUrl,
630
- apiHeaders,
631
- locale,
495
+ // src/modules/messages/load-remote-messages/fetch-locale-messages/fetch-locale-messages.ts
496
+ var fetchLocaleMessages = async ({
497
+ remoteUrl,
498
+ remoteHeaders,
632
499
  searchParams,
633
- logger: loggerOptions = { id: "default" }
500
+ locale,
501
+ extraOptions: { loggerOptions } = {}
634
502
  }) => {
635
503
  const baseLogger = getLogger({ ...loggerOptions });
636
- const logger = baseLogger.child({ scope: "fetch-messages" });
504
+ const logger = baseLogger.child({ scope: "fetch-locale-messages" });
637
505
  try {
638
506
  const params = new URLSearchParams(searchParams);
639
507
  params.append("locale", locale);
640
- const url = `${apiUrl}?${params.toString()}`;
508
+ const url = `${remoteUrl}?${params.toString()}`;
641
509
  const headers2 = {
642
510
  "Content-Type": "application/json",
643
- ...apiHeaders
511
+ ...remoteHeaders
644
512
  };
645
513
  const response = await fetch(url, {
646
514
  method: "GET",
@@ -648,17 +516,17 @@ var fetchMessages = async ({
648
516
  cache: "no-store"
649
517
  });
650
518
  if (!response.ok) {
651
- throw new Error(`Fetch failed: ${locale} (${response.status})`);
519
+ throw new Error(`HTTP error ${response.status} ${response.statusText}`);
652
520
  }
653
521
  const data = await response.json();
654
- if (data == null || typeof data === "object" && Object.keys(data).length === 0) {
655
- throw new Error(`Invalid messages: ${locale}`);
522
+ if (!isNamespaceMessages(data[locale])) {
523
+ throw new Error("JSON file does not match NamespaceMessages structure");
656
524
  }
657
525
  return data;
658
526
  } catch (error) {
659
- logger.warn(`Failed to fetch messages for locale "${locale}".`, {
527
+ logger.warn("Fetching locale messages failed.", {
660
528
  locale,
661
- apiUrl,
529
+ remoteUrl,
662
530
  searchParams: decodeURIComponent(searchParams.toString()),
663
531
  error
664
532
  });
@@ -666,30 +534,7 @@ var fetchMessages = async ({
666
534
  }
667
535
  };
668
536
 
669
- // src/modules/messages/load-api-messages/fetch-fallback-messages.ts
670
- var fetchFallbackMessages = async ({
671
- apiUrl,
672
- apiHeaders,
673
- searchParams,
674
- fallbackLocales,
675
- logger
676
- }) => {
677
- for (const fallbackLocale of fallbackLocales) {
678
- const result = await fetchMessages({
679
- apiUrl,
680
- searchParams,
681
- locale: fallbackLocale,
682
- apiHeaders,
683
- logger
684
- });
685
- if (result) {
686
- return { locale: fallbackLocale, messages: result };
687
- }
688
- }
689
- return;
690
- };
691
-
692
- // src/modules/messages/load-api-messages/utils/build-search-params.ts
537
+ // src/modules/messages/load-remote-messages/fetch-locale-messages/utils/build-search-params.ts
693
538
  var buildSearchParams = (params) => {
694
539
  const searchParams = new URLSearchParams();
695
540
  const appendParam = (key, value) => {
@@ -707,119 +552,130 @@ var buildSearchParams = (params) => {
707
552
  return searchParams;
708
553
  };
709
554
 
710
- // src/modules/messages/load-api-messages/load-api-messages.ts
711
- var loadApiMessages = async ({
712
- apiUrl,
713
- apiHeaders,
714
- basePath,
555
+ // src/modules/messages/load-remote-messages/load-remote-messages.ts
556
+ var loadRemoteMessages = async ({
557
+ pool = getGlobalMessagesPool(),
558
+ rootDir,
559
+ remoteUrl,
560
+ remoteHeaders,
715
561
  locale,
716
562
  fallbackLocales = [],
717
563
  namespaces = [],
718
- cache = DEFAULT_CACHE_OPTIONS,
719
- logger: loggerOptions = { id: "default" }
564
+ extraOptions: {
565
+ cacheOptions = DEFAULT_CACHE_OPTIONS,
566
+ loggerOptions = { id: "default" }
567
+ } = {}
720
568
  }) => {
721
569
  const baseLogger = getLogger({ ...loggerOptions });
722
- const logger = baseLogger.child({ scope: "load-api-messages" });
723
- if (!apiUrl) {
724
- logger.warn("No apiUrl provided. Skipping fetch.");
725
- return;
726
- }
727
- let pool;
728
- if (cache.enabled) {
729
- pool = getGlobalMessagesPool();
730
- }
570
+ const logger = baseLogger.child({ scope: "load-remote-messages" });
571
+ const start = performance.now();
572
+ logger.debug("Loading remote messages from api.", { remoteUrl });
731
573
  const key = normalizeCacheKey([
732
574
  loggerOptions.id,
733
- basePath,
575
+ "loaderType:remote",
576
+ rootDir,
734
577
  locale,
735
578
  (fallbackLocales ?? []).toSorted().join(","),
736
579
  (namespaces ?? []).toSorted().join(",")
737
580
  ]);
738
- if (cache.enabled && key) {
581
+ if (cacheOptions.enabled && key) {
739
582
  const cached = await pool?.get(key);
740
583
  if (cached) {
741
584
  logger.debug("Messages cache hit.", { key });
742
585
  return cached;
743
586
  }
744
587
  }
745
- const searchParams = buildSearchParams({ basePath, namespaces });
746
- const messages = await fetchMessages({
747
- apiUrl,
748
- apiHeaders,
749
- searchParams,
750
- locale,
751
- logger: loggerOptions
752
- });
753
- if (messages) {
754
- if (cache.enabled && key) {
755
- await pool?.set(key, messages, cache.ttl);
588
+ const searchParams = buildSearchParams({ rootDir, namespaces });
589
+ const candidateLocales = [locale, ...fallbackLocales || []];
590
+ let messages;
591
+ for (const candidateLocale of candidateLocales) {
592
+ try {
593
+ const result = await fetchLocaleMessages({
594
+ remoteUrl,
595
+ remoteHeaders,
596
+ searchParams,
597
+ locale: candidateLocale,
598
+ extraOptions: { loggerOptions }
599
+ });
600
+ if (result && Object.values(result[candidateLocale] || {}).length > 0) {
601
+ messages = result;
602
+ break;
603
+ }
604
+ } catch (error) {
605
+ logger.error("Failed to fetch locale messages.", {
606
+ locale: candidateLocale,
607
+ error
608
+ });
756
609
  }
757
- return messages;
758
610
  }
759
- const fallbackResult = await fetchFallbackMessages({
760
- apiUrl,
761
- apiHeaders,
762
- searchParams,
763
- fallbackLocales,
764
- logger: loggerOptions
765
- });
766
- if (fallbackResult) {
767
- logger.info("Fallback locale succeeded.", {
768
- usedLocale: fallbackResult.locale,
769
- apiUrl,
770
- searchParams: decodeURIComponent(searchParams.toString())
771
- });
772
- if (cache.enabled && key) {
773
- await pool?.set(key, fallbackResult.messages, cache.ttl);
774
- }
775
- return fallbackResult.messages;
611
+ if (cacheOptions.enabled && key && messages) {
612
+ await pool?.set(key, messages, cacheOptions.ttl);
776
613
  }
777
- logger.warn("Failed to fetch messages for all locales.", {
778
- locale,
779
- fallbackLocales
614
+ const end = performance.now();
615
+ const duration = Math.round(end - start);
616
+ logger.trace("Finished loading remote messages.", {
617
+ loadedLocale: messages ? Object.keys(messages)[0] : void 0,
618
+ duration: `${duration} ms`
780
619
  });
781
- return;
620
+ return messages;
782
621
  };
783
622
 
784
623
  // src/modules/messages/load-messages.ts
785
624
  var loadMessages = async ({
786
625
  config,
787
626
  locale,
788
- pathname
627
+ pathname = "",
628
+ extraOptions: { exts, messageFileReader } = {}
789
629
  }) => {
790
630
  const baseLogger = getLogger({ id: config.id, ...config.logger });
791
- const logger = baseLogger.child({ scope: "messages-loader" });
631
+ const logger = baseLogger.child({ scope: "load-messages" });
792
632
  if (!config.loader) {
793
633
  logger.warn(
794
634
  "No loader options have been configured in the current config."
795
635
  );
796
636
  return;
797
637
  }
798
- const { id, loader, cache } = config;
638
+ const { type, concurrency, rootDir } = config.loader;
799
639
  const fallbackLocales = config.fallbackLocales[locale] || [];
800
640
  const namespaces = resolveNamespaces({ config, pathname });
801
- logger.debug("Namespaces ready for loading.", {
802
- namespaces: namespaces.length > 0 ? { count: namespaces.length, list: [...namespaces] } : "All Namespaces"
641
+ if (logger.core.level === "debug") {
642
+ logger.debug("Starting to load messages.", { locale });
643
+ }
644
+ logger.trace("Starting to load messages with runtime context.", {
645
+ loaderType: type,
646
+ locale,
647
+ fallbackLocales,
648
+ namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : "[ALL]",
649
+ cache: config.cache,
650
+ concurrency: concurrency ?? 10
803
651
  });
804
- logger.debug("Loader type selected.", { loaderType: loader.type });
805
652
  let loadedMessages;
806
- if (loader.type === "import") {
807
- const loadLocalMessages2 = createLoadLocalMessages(loader.basePath);
808
- loadedMessages = await loadLocalMessages2({
809
- ...loader,
653
+ if (type === "local") {
654
+ loadedMessages = await loadLocalMessages({
655
+ rootDir,
810
656
  locale,
811
657
  fallbackLocales,
812
658
  namespaces,
813
- cache,
814
- logger: { id }
659
+ extraOptions: {
660
+ concurrency,
661
+ cacheOptions: config.cache,
662
+ loggerOptions: { id: config.id, ...config.logger },
663
+ exts,
664
+ messageFileReader
665
+ }
815
666
  });
816
- } else if (loader.type === "api") {
817
- loadedMessages = await loadApiMessages({
818
- ...loader,
667
+ } else if (type === "remote") {
668
+ loadedMessages = await loadRemoteMessages({
669
+ rootDir,
670
+ remoteUrl: config.loader.remoteUrl,
671
+ remoteHeaders: config.loader.remoteHeaders,
819
672
  locale,
820
673
  fallbackLocales,
821
674
  namespaces,
822
- logger: { id }
675
+ extraOptions: {
676
+ cacheOptions: config.cache,
677
+ loggerOptions: { id: config.id, ...config.logger }
678
+ }
823
679
  });
824
680
  }
825
681
  if (!loadedMessages || Object.keys(loadedMessages).length === 0) {
@@ -828,7 +684,7 @@ var loadMessages = async ({
828
684
  return loadedMessages;
829
685
  };
830
686
 
831
- // src/modules/tools/get-translator.ts
687
+ // src/modules/translator/get-translator.ts
832
688
  async function getTranslator(opts) {
833
689
  const { config, locale, pathname = "", preKey, handlers } = opts;
834
690
  const messages = await loadMessages({ config, locale, pathname });