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