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.
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,172 @@ 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-namespace-messages.ts
132
+ function isPlainObject(value) {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
135
+ function isNamespaceMessages(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
148
  }
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
- }
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
+
152
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/json-reader.ts
153
+ async function jsonReader(filePath, readFile = fs.readFile) {
154
+ const raw = await readFile(filePath, "utf8");
155
+ const parsed = JSON.parse(raw);
156
+ if (!isNamespaceMessages(parsed)) {
157
+ throw new Error("JSON file does not match NamespaceMessages structure");
158
+ }
159
+ return parsed;
160
+ }
161
+
162
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/nest-object-from-path.ts
163
+ function nestObjectFromPath(path4, value) {
164
+ let obj = value;
165
+ for (let i = path4.length - 1; i >= 0; i--) {
166
+ obj = { [path4[i]]: obj };
204
167
  }
205
- };
168
+ return obj;
169
+ }
206
170
 
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" };
171
+ // src/modules/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.ts
172
+ async function parseFileEntries({
173
+ fileEntries,
174
+ limit,
175
+ extraOptions: { messageFileReader, loggerOptions } = {}
176
+ }) {
217
177
  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
- });
239
- }
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,
178
+ const logger = baseLogger.child({ scope: "parse-file-entries" });
179
+ const parsedFileEntries = [];
180
+ const tasks = fileEntries.map(
181
+ ({ namespace, segments, basename, fullPath }) => limit(async () => {
182
+ try {
183
+ const segsWithoutNs = segments.slice(1);
184
+ const json = await (messageFileReader ? messageFileReader(fullPath) : jsonReader(fullPath));
185
+ const isIndex = basename === "index";
186
+ const keyPath = isIndex ? segsWithoutNs.slice(0, -1) : segsWithoutNs;
187
+ const namespaceMessages = nestObjectFromPath(keyPath, json);
188
+ parsedFileEntries.push({ namespace, namespaceMessages });
189
+ logger.trace("Parsed file.", { path: fullPath });
190
+ } catch (error) {
191
+ logger.error("Failed to read or parse file.", {
192
+ path: fullPath,
245
193
  error
246
194
  });
247
- })
248
- );
249
- await Promise.all(dirPromises);
250
- } catch (error) {
251
- logger.warn(`Error reading directory: ${currentDirPath}`, { error });
195
+ }
196
+ })
197
+ );
198
+ await Promise.all(tasks);
199
+ const result = {};
200
+ for (const { namespace, namespaceMessages } of parsedFileEntries) {
201
+ if (namespace === "index") {
202
+ merge(result, namespaceMessages);
203
+ } else {
204
+ result[namespace] = merge(
205
+ result[namespace] ?? {},
206
+ namespaceMessages
207
+ );
208
+ }
252
209
  }
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
- };
210
+ return result;
211
+ }
267
212
 
268
- // src/modules/messages/load-local-messages/load-single-locale/load-single-locale.ts
269
- var loadSingleLocale = async ({
270
- basePath,
213
+ // src/modules/messages/load-local-messages/read-locale-messages/read-locale-messages.ts
214
+ var readLocaleMessages = async ({
215
+ limit,
216
+ rootDir = "messages",
271
217
  locale,
272
218
  namespaces,
273
- messages,
274
- limit,
275
- logger: loggerOptions = { id: "default" }
219
+ extraOptions: { exts, messageFileReader, loggerOptions } = {}
276
220
  }) => {
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,
221
+ const fileEntries = await collectFileEntries({
222
+ rootDir: path.resolve(process.cwd(), rootDir, locale),
223
+ namespaces,
296
224
  limit,
297
- namespaces: new Set(namespaces || []),
298
- logger: loggerOptions
299
- });
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
- }))
225
+ extraOptions: { exts, loggerOptions }
314
226
  });
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
227
+ const namespaceMessages = await parseFileEntries({
228
+ fileEntries,
229
+ limit,
230
+ extraOptions: { messageFileReader, loggerOptions }
364
231
  });
365
- return;
232
+ const localeMessages = { [locale]: namespaceMessages };
233
+ return localeMessages;
366
234
  };
367
235
  function getGlobalMessagesPool() {
368
236
  if (!globalThis.__INTOR_MESSAGES_POOL__) {
@@ -374,22 +242,9 @@ function clearMessagesPool() {
374
242
  const pool = getGlobalMessagesPool();
375
243
  pool.clear();
376
244
  }
377
-
378
- // src/shared/utils/merge-messages.ts
379
245
  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;
246
+ if (!loadedMessages) return { ...staticMessages };
247
+ return merge({}, staticMessages, loadedMessages);
393
248
  };
394
249
 
395
250
  // src/shared/utils/normalize-cache-key.ts
@@ -421,6 +276,8 @@ var resolveNamespaces = ({
421
276
  }) => {
422
277
  const { loader } = config;
423
278
  const { routeNamespaces = {}, namespaces } = loader || {};
279
+ if (Object.keys(routeNamespaces).length === 0 && !namespaces)
280
+ return void 0;
424
281
  const standardizedPathname = standardizePathname({ config, pathname });
425
282
  const placeholderRemovedPathname = standardizedPathname.replace(
426
283
  `/${PREFIX_PLACEHOLDER}`,
@@ -578,43 +435,35 @@ var standardizePathname = ({
578
435
 
579
436
  // src/modules/messages/load-local-messages/load-local-messages.ts
580
437
  var loadLocalMessages = async ({
581
- basePath,
438
+ pool = getGlobalMessagesPool(),
439
+ rootDir = "messages",
582
440
  locale,
583
441
  fallbackLocales,
584
442
  namespaces,
585
- concurrency = 10,
586
- cache = DEFAULT_CACHE_OPTIONS,
587
- logger: loggerOptions = { id: "default" }
443
+ extraOptions: {
444
+ concurrency = 10,
445
+ cacheOptions = DEFAULT_CACHE_OPTIONS,
446
+ loggerOptions = { id: "default" },
447
+ exts,
448
+ messageFileReader
449
+ } = {}
588
450
  }) => {
589
- basePath = basePath ?? "messages";
590
- if (!locale || locale.trim() === "") return {};
591
451
  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
452
+ const logger = baseLogger.child({ scope: "load-local-messages" });
453
+ const start = performance$1.now();
454
+ logger.debug("Loading local messages from directory.", {
455
+ rootDir,
456
+ resolvedRootDir: path.resolve(process.cwd(), rootDir)
605
457
  });
606
- let pool;
607
- if (cache.enabled) {
608
- pool = getGlobalMessagesPool();
609
- }
610
458
  const key = normalizeCacheKey([
611
459
  loggerOptions.id,
612
- resolvedBasePath,
460
+ "loaderType:local",
461
+ rootDir,
613
462
  locale,
614
- (fallbackLocales ?? []).toSorted().join(","),
615
- (namespaces ?? []).toSorted().join(",")
463
+ (fallbackLocales || []).toSorted().join(","),
464
+ (namespaces || []).toSorted().join(",")
616
465
  ]);
617
- if (cache.enabled && key) {
466
+ if (cacheOptions.enabled && key) {
618
467
  const cached = await pool?.get(key);
619
468
  if (cached) {
620
469
  logger.debug("Messages cache hit.", { key });
@@ -622,50 +471,57 @@ var loadLocalMessages = async ({
622
471
  }
623
472
  }
624
473
  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);
474
+ const candidateLocales = [locale, ...fallbackLocales || []];
475
+ let messages;
476
+ for (const candidateLocale of candidateLocales) {
477
+ try {
478
+ const result = await readLocaleMessages({
479
+ limit,
480
+ rootDir,
481
+ locale: candidateLocale,
482
+ namespaces,
483
+ extraOptions: { loggerOptions, exts, messageFileReader }
484
+ });
485
+ if (result && Object.values(result[candidateLocale] || {}).length > 0) {
486
+ messages = result;
487
+ break;
488
+ }
489
+ } catch (error) {
490
+ logger.error("Failed to read locale messages", {
491
+ locale: candidateLocale,
492
+ error
493
+ });
494
+ }
636
495
  }
637
- const end = performance.now();
496
+ if (cacheOptions.enabled && key && messages) {
497
+ await pool?.set(key, messages, cacheOptions.ttl);
498
+ }
499
+ const end = performance$1.now();
638
500
  const duration = Math.round(end - start);
639
501
  logger.trace("Finished loading local messages.", {
640
- locale,
641
- validNamespaces,
502
+ loadedLocale: messages ? Object.keys(messages)[0] : void 0,
642
503
  duration: `${duration} ms`
643
504
  });
644
505
  return messages;
645
506
  };
646
507
 
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,
508
+ // src/modules/messages/load-remote-messages/fetch-locale-messages/fetch-locale-messages.ts
509
+ var fetchLocaleMessages = async ({
510
+ remoteUrl,
511
+ remoteHeaders,
657
512
  searchParams,
658
- logger: loggerOptions = { id: "default" }
513
+ locale,
514
+ extraOptions: { loggerOptions } = {}
659
515
  }) => {
660
516
  const baseLogger = getLogger({ ...loggerOptions });
661
- const logger = baseLogger.child({ scope: "fetch-messages" });
517
+ const logger = baseLogger.child({ scope: "fetch-locale-messages" });
662
518
  try {
663
519
  const params = new URLSearchParams(searchParams);
664
520
  params.append("locale", locale);
665
- const url = `${apiUrl}?${params.toString()}`;
521
+ const url = `${remoteUrl}?${params.toString()}`;
666
522
  const headers = {
667
523
  "Content-Type": "application/json",
668
- ...apiHeaders
524
+ ...remoteHeaders
669
525
  };
670
526
  const response = await fetch(url, {
671
527
  method: "GET",
@@ -673,17 +529,17 @@ var fetchMessages = async ({
673
529
  cache: "no-store"
674
530
  });
675
531
  if (!response.ok) {
676
- throw new Error(`Fetch failed: ${locale} (${response.status})`);
532
+ throw new Error(`HTTP error ${response.status} ${response.statusText}`);
677
533
  }
678
534
  const data = await response.json();
679
- if (data == null || typeof data === "object" && Object.keys(data).length === 0) {
680
- throw new Error(`Invalid messages: ${locale}`);
535
+ if (!isNamespaceMessages(data[locale])) {
536
+ throw new Error("JSON file does not match NamespaceMessages structure");
681
537
  }
682
538
  return data;
683
539
  } catch (error) {
684
- logger.warn(`Failed to fetch messages for locale "${locale}".`, {
540
+ logger.warn("Fetching locale messages failed.", {
685
541
  locale,
686
- apiUrl,
542
+ remoteUrl,
687
543
  searchParams: decodeURIComponent(searchParams.toString()),
688
544
  error
689
545
  });
@@ -691,30 +547,7 @@ var fetchMessages = async ({
691
547
  }
692
548
  };
693
549
 
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
550
+ // src/modules/messages/load-remote-messages/fetch-locale-messages/utils/build-search-params.ts
718
551
  var buildSearchParams = (params) => {
719
552
  const searchParams = new URLSearchParams();
720
553
  const appendParam = (key, value) => {
@@ -732,119 +565,130 @@ var buildSearchParams = (params) => {
732
565
  return searchParams;
733
566
  };
734
567
 
735
- // src/modules/messages/load-api-messages/load-api-messages.ts
736
- var loadApiMessages = async ({
737
- apiUrl,
738
- apiHeaders,
739
- basePath,
568
+ // src/modules/messages/load-remote-messages/load-remote-messages.ts
569
+ var loadRemoteMessages = async ({
570
+ pool = getGlobalMessagesPool(),
571
+ rootDir,
572
+ remoteUrl,
573
+ remoteHeaders,
740
574
  locale,
741
575
  fallbackLocales = [],
742
576
  namespaces = [],
743
- cache = DEFAULT_CACHE_OPTIONS,
744
- logger: loggerOptions = { id: "default" }
577
+ extraOptions: {
578
+ cacheOptions = DEFAULT_CACHE_OPTIONS,
579
+ loggerOptions = { id: "default" }
580
+ } = {}
745
581
  }) => {
746
582
  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
- }
583
+ const logger = baseLogger.child({ scope: "load-remote-messages" });
584
+ const start = performance.now();
585
+ logger.debug("Loading remote messages from api.", { remoteUrl });
756
586
  const key = normalizeCacheKey([
757
587
  loggerOptions.id,
758
- basePath,
588
+ "loaderType:remote",
589
+ rootDir,
759
590
  locale,
760
591
  (fallbackLocales ?? []).toSorted().join(","),
761
592
  (namespaces ?? []).toSorted().join(",")
762
593
  ]);
763
- if (cache.enabled && key) {
594
+ if (cacheOptions.enabled && key) {
764
595
  const cached = await pool?.get(key);
765
596
  if (cached) {
766
597
  logger.debug("Messages cache hit.", { key });
767
598
  return cached;
768
599
  }
769
600
  }
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);
601
+ const searchParams = buildSearchParams({ rootDir, namespaces });
602
+ const candidateLocales = [locale, ...fallbackLocales || []];
603
+ let messages;
604
+ for (const candidateLocale of candidateLocales) {
605
+ try {
606
+ const result = await fetchLocaleMessages({
607
+ remoteUrl,
608
+ remoteHeaders,
609
+ searchParams,
610
+ locale: candidateLocale,
611
+ extraOptions: { loggerOptions }
612
+ });
613
+ if (result && Object.values(result[candidateLocale] || {}).length > 0) {
614
+ messages = result;
615
+ break;
616
+ }
617
+ } catch (error) {
618
+ logger.error("Failed to fetch locale messages.", {
619
+ locale: candidateLocale,
620
+ error
621
+ });
781
622
  }
782
- return messages;
783
623
  }
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;
624
+ if (cacheOptions.enabled && key && messages) {
625
+ await pool?.set(key, messages, cacheOptions.ttl);
801
626
  }
802
- logger.warn("Failed to fetch messages for all locales.", {
803
- locale,
804
- fallbackLocales
627
+ const end = performance.now();
628
+ const duration = Math.round(end - start);
629
+ logger.trace("Finished loading remote messages.", {
630
+ loadedLocale: messages ? Object.keys(messages)[0] : void 0,
631
+ duration: `${duration} ms`
805
632
  });
806
- return;
633
+ return messages;
807
634
  };
808
635
 
809
636
  // src/modules/messages/load-messages.ts
810
637
  var loadMessages = async ({
811
638
  config,
812
639
  locale,
813
- pathname
640
+ pathname = "",
641
+ extraOptions: { exts, messageFileReader } = {}
814
642
  }) => {
815
643
  const baseLogger = getLogger({ id: config.id, ...config.logger });
816
- const logger = baseLogger.child({ scope: "messages-loader" });
644
+ const logger = baseLogger.child({ scope: "load-messages" });
817
645
  if (!config.loader) {
818
646
  logger.warn(
819
647
  "No loader options have been configured in the current config."
820
648
  );
821
649
  return;
822
650
  }
823
- const { id, loader, cache } = config;
651
+ const { type, concurrency, rootDir } = config.loader;
824
652
  const fallbackLocales = config.fallbackLocales[locale] || [];
825
653
  const namespaces = resolveNamespaces({ config, pathname });
826
- logger.debug("Namespaces ready for loading.", {
827
- namespaces: namespaces.length > 0 ? { count: namespaces.length, list: [...namespaces] } : "All Namespaces"
654
+ if (logger.core.level === "debug") {
655
+ logger.debug("Starting to load messages.", { locale });
656
+ }
657
+ logger.trace("Starting to load messages with runtime context.", {
658
+ loaderType: type,
659
+ locale,
660
+ fallbackLocales,
661
+ namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : "[ALL]",
662
+ cache: config.cache,
663
+ concurrency: concurrency ?? 10
828
664
  });
829
- logger.debug("Loader type selected.", { loaderType: loader.type });
830
665
  let loadedMessages;
831
- if (loader.type === "import") {
832
- const loadLocalMessages2 = createLoadLocalMessages(loader.basePath);
833
- loadedMessages = await loadLocalMessages2({
834
- ...loader,
666
+ if (type === "local") {
667
+ loadedMessages = await loadLocalMessages({
668
+ rootDir,
835
669
  locale,
836
670
  fallbackLocales,
837
671
  namespaces,
838
- cache,
839
- logger: { id }
672
+ extraOptions: {
673
+ concurrency,
674
+ cacheOptions: config.cache,
675
+ loggerOptions: { id: config.id, ...config.logger },
676
+ exts,
677
+ messageFileReader
678
+ }
840
679
  });
841
- } else if (loader.type === "api") {
842
- loadedMessages = await loadApiMessages({
843
- ...loader,
680
+ } else if (type === "remote") {
681
+ loadedMessages = await loadRemoteMessages({
682
+ rootDir,
683
+ remoteUrl: config.loader.remoteUrl,
684
+ remoteHeaders: config.loader.remoteHeaders,
844
685
  locale,
845
686
  fallbackLocales,
846
687
  namespaces,
847
- logger: { id }
688
+ extraOptions: {
689
+ cacheOptions: config.cache,
690
+ loggerOptions: { id: config.id, ...config.logger }
691
+ }
848
692
  });
849
693
  }
850
694
  if (!loadedMessages || Object.keys(loadedMessages).length === 0) {
@@ -854,7 +698,7 @@ var loadMessages = async ({
854
698
  };
855
699
 
856
700
  // src/modules/intor/intor.ts
857
- var intor = async (config, i18nContext) => {
701
+ var intor = async (config, i18nContext, loadMessagesOptions = {}) => {
858
702
  const baseLogger = getLogger({ id: config.id, ...config.logger });
859
703
  const logger = baseLogger.child({ scope: "intor" });
860
704
  logger.info("Start Intor initialization.");
@@ -865,20 +709,28 @@ var intor = async (config, i18nContext) => {
865
709
  pathname: i18nContext?.pathname || ""
866
710
  };
867
711
  const { locale, pathname } = context;
868
- const source = isI18nContextFunction ? "[function]" : "[static object]";
869
- logger.debug(`Context resolved via ${source}.`, context);
712
+ const source = isI18nContextFunction ? i18nContext.name : "static fallback";
713
+ logger.debug(`I18n context resolved via ${source}.`, context);
870
714
  let loadedMessages;
871
715
  if (shouldLoadMessages(loader)) {
872
- loadedMessages = await loadMessages({ config, locale, pathname });
716
+ loadedMessages = await loadMessages({
717
+ config,
718
+ locale,
719
+ pathname,
720
+ extraOptions: {
721
+ exts: loadMessagesOptions.exts,
722
+ messageFileReader: loadMessagesOptions.messageFileReader
723
+ }
724
+ });
873
725
  }
874
726
  const mergedMessages = mergeMessages(messages, loadedMessages);
875
- logger.info("Messages initialized.", {
876
- staticMessages: { enabled: !!messages },
877
- loadedMessages: {
727
+ logger.info("Messages successfully initialized.", {
728
+ static: { enabled: !!messages },
729
+ loaded: {
878
730
  enabled: !!loadedMessages,
879
731
  ...loader ? { loaderType: loader.type, lazyLoad: !!loader.lazyLoad } : null
880
732
  },
881
- mergedMessages
733
+ merged: mergedMessages
882
734
  });
883
735
  return {
884
736
  config,
@@ -907,4 +759,4 @@ async function getTranslator(opts) {
907
759
  };
908
760
  }
909
761
 
910
- export { PREFIX_PLACEHOLDER, clearLoggerPool, clearMessagesPool, extractPathname, getTranslator, intor, loadApiMessages, loadLocalMessages, loadMessages, mergeMessages, normalizeCacheKey, normalizeLocale, normalizePathname, resolveNamespaces, resolvePreferredLocale, standardizePathname };
762
+ export { PREFIX_PLACEHOLDER, clearLoggerPool, clearMessagesPool, extractPathname, getTranslator, intor, loadLocalMessages, loadMessages, loadRemoteMessages, mergeMessages, normalizeCacheKey, normalizeLocale, normalizePathname, resolveNamespaces, resolvePreferredLocale, standardizePathname };