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