i18next-cli 1.49.3 → 1.49.5

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/cjs/cli.js CHANGED
@@ -31,7 +31,7 @@ const program = new commander.Command();
31
31
  program
32
32
  .name('i18next-cli')
33
33
  .description('A unified, high-performance i18next CLI.')
34
- .version('1.49.3'); // This string is replaced with the actual version at build time by rollup
34
+ .version('1.49.5'); // This string is replaced with the actual version at build time by rollup
35
35
  // new: global config override option
36
36
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
37
37
  program
@@ -64,6 +64,65 @@ class ASTVisitors {
64
64
  this.callExpressionHandler = new callExpressionHandler.CallExpressionHandler(config, pluginContext, logger, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode(), (name) => this.scopeManager.resolveSimpleStringIdentifier(name));
65
65
  this.jsxHandler = new jsxHandler.JSXHandler(config, pluginContext, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode());
66
66
  }
67
+ /**
68
+ * Lightweight pre-scan pass: populates the shared constant / type-alias / array tables
69
+ * (`sharedConstants` in ScopeManager; `sharedVariableTable` and `sharedTypeAliasTable`
70
+ * in ExpressionResolver) WITHOUT performing any key extraction.
71
+ *
72
+ * Callers should invoke this for ALL source files before calling `visit()` for any file,
73
+ * so that cross-file identifier references — e.g. `useTranslation(NS_CALENDAR)` where
74
+ * `NS_CALENDAR` is exported from a separate constants file — are already resolved when
75
+ * the hook call is encountered during the extraction pass.
76
+ *
77
+ * The per-file tables (variableTable, typeAliasTable) are reset on each call so that
78
+ * local bindings from one file do not bleed into another; the shared tables accumulate
79
+ * across all calls and are intentionally NOT cleared here.
80
+ */
81
+ preScanForConstants(node) {
82
+ this.expressionResolver.resetFileSymbols();
83
+ this._walkForConstants(node);
84
+ }
85
+ /**
86
+ * Recursive walker used exclusively by `preScanForConstants`.
87
+ * Dispatches only to constant-capturing handlers; never extracts translation keys.
88
+ */
89
+ _walkForConstants(node) {
90
+ if (!node || typeof node !== 'object')
91
+ return;
92
+ switch (node.type) {
93
+ case 'VariableDeclarator':
94
+ // String constants → ScopeManager.sharedConstants
95
+ // as-const arrays/objects → ExpressionResolver.sharedVariableTable
96
+ this.scopeManager.handleVariableDeclarator(node);
97
+ this.expressionResolver.captureVariableDeclarator(node);
98
+ break;
99
+ case 'TsTypeAliasDeclaration':
100
+ case 'TSTypeAliasDeclaration':
101
+ case 'TsTypeAliasDecl':
102
+ // Type aliases → ExpressionResolver.sharedTypeAliasTable
103
+ this.expressionResolver.captureTypeAliasDeclaration(node);
104
+ break;
105
+ case 'FunctionDeclaration':
106
+ case 'FnDecl':
107
+ // Return-type annotations for t(fn()) patterns
108
+ this.expressionResolver.captureFunctionDeclaration(node);
109
+ break;
110
+ }
111
+ for (const key in node) {
112
+ if (key === 'span')
113
+ continue;
114
+ const child = node[key];
115
+ if (Array.isArray(child)) {
116
+ for (const item of child) {
117
+ if (item && typeof item === 'object')
118
+ this._walkForConstants(item);
119
+ }
120
+ }
121
+ else if (child && typeof child === 'object') {
122
+ this._walkForConstants(child);
123
+ }
124
+ }
125
+ }
67
126
  /**
68
127
  * Main entry point for AST traversal.
69
128
  * Creates a root scope and begins the recursive walk through the syntax tree.
@@ -456,6 +515,10 @@ class ASTVisitors {
456
515
  * If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
457
516
  * string-array constant, returns the callback's first parameter name and the
458
517
  * array values so the caller can inject a temporary variable binding.
518
+ *
519
+ * Also handles:
520
+ * `Object.keys(MAP).map/forEach(k => ...)` → param bound to MAP's keys
521
+ * `Object.values(MAP).map/forEach(v => ...)` → param bound to MAP's values
459
522
  */
460
523
  tryGetArrayIterationCallbackInfo(node) {
461
524
  try {
@@ -469,36 +532,67 @@ class ASTVisitors {
469
532
  return undefined;
470
533
  // The object must be an identifier whose value is a known string array
471
534
  const obj = callee.object;
472
- if (obj?.type !== 'Identifier')
473
- return undefined;
474
- const values = this.expressionResolver.getVariableValues(obj.value);
475
- if (!values || values.length === 0)
476
- return undefined;
477
- // First argument must be a callback with at least one parameter
478
- const callbackArg = node.arguments?.[0]?.expression;
479
- if (!callbackArg)
480
- return undefined;
481
- // Normalise param across SWC shapes: ArrowFunctionExpression / FunctionExpression
482
- const params = callbackArg.params ?? callbackArg.parameters ?? [];
483
- const firstParam = params[0];
484
- if (!firstParam)
485
- return undefined;
486
- // SWC wraps params in `Param { pat: Identifier }` or exposes them directly
487
- const ident = firstParam.type === 'Identifier'
488
- ? firstParam
489
- : firstParam.type === 'Param' && firstParam.pat?.type === 'Identifier'
490
- ? firstParam.pat
491
- : firstParam.type === 'AssignmentPattern' && firstParam.left?.type === 'Identifier'
492
- ? firstParam.left
493
- : null;
494
- if (!ident)
495
- return undefined;
496
- return { paramName: ident.value, values };
535
+ // ── Case 1: KNOWN_ARRAY.map(x => ...) ─────────────────────────────────
536
+ if (obj?.type === 'Identifier') {
537
+ const values = this.expressionResolver.getVariableValues(obj.value);
538
+ if (values && values.length > 0) {
539
+ return this.extractCallbackParam(node, values);
540
+ }
541
+ }
542
+ // ── Case 2: Object.keys(MAP).map(k => ...) ────────────────────────────
543
+ // Object.values(MAP).map(v => ...)
544
+ // callee.object is a CallExpression: Object.keys(...) / Object.values(...)
545
+ if (obj?.type === 'CallExpression') {
546
+ const innerCallee = obj.callee;
547
+ // Must be `Object.keys` or `Object.values`
548
+ if (innerCallee?.type === 'MemberExpression' &&
549
+ innerCallee.object?.type === 'Identifier' &&
550
+ innerCallee.object.value === 'Object' &&
551
+ innerCallee.property?.type === 'Identifier' &&
552
+ (innerCallee.property.value === 'keys' || innerCallee.property.value === 'values')) {
553
+ const isKeys = innerCallee.property.value === 'keys';
554
+ // The single argument to Object.keys/values must be a known identifier
555
+ const mapArg = obj.arguments?.[0]?.expression;
556
+ if (mapArg?.type === 'Identifier') {
557
+ const mapEntry = this.expressionResolver.getObjectMap(mapArg.value);
558
+ if (mapEntry) {
559
+ const values = isKeys ? Object.keys(mapEntry) : Object.values(mapEntry);
560
+ if (values.length > 0) {
561
+ return this.extractCallbackParam(node, values);
562
+ }
563
+ }
564
+ }
565
+ }
566
+ }
567
+ return undefined;
497
568
  }
498
569
  catch {
499
570
  return undefined;
500
571
  }
501
572
  }
573
+ /**
574
+ * Extracts the first callback parameter identifier from an iteration call node
575
+ * and pairs it with the provided values array.
576
+ */
577
+ extractCallbackParam(node, values) {
578
+ const callbackArg = node.arguments?.[0]?.expression;
579
+ if (!callbackArg)
580
+ return undefined;
581
+ const params = callbackArg.params ?? callbackArg.parameters ?? [];
582
+ const firstParam = params[0];
583
+ if (!firstParam)
584
+ return undefined;
585
+ const ident = firstParam.type === 'Identifier'
586
+ ? firstParam
587
+ : firstParam.type === 'Param' && firstParam.pat?.type === 'Identifier'
588
+ ? firstParam.pat
589
+ : firstParam.type === 'AssignmentPattern' && firstParam.left?.type === 'Identifier'
590
+ ? firstParam.left
591
+ : null;
592
+ if (!ident)
593
+ return undefined;
594
+ return { paramName: ident.value, values };
595
+ }
502
596
  /**
503
597
  * Retrieves variable information from the scope chain.
504
598
  * Searches from innermost to outermost scope.
@@ -236,7 +236,80 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo
236
236
  }
237
237
  }
238
238
  /**
239
- * Simplified extraction function that returns translation results without file writing.
239
+ * Lightweight pre-scan pass for a single file.
240
+ *
241
+ * Parses the file and calls `astVisitors.preScanForConstants()` to populate
242
+ * cross-file shared constant / type-alias / array tables WITHOUT extracting
243
+ * any translation keys or running plugin hooks.
244
+ *
245
+ * Intended to be called for ALL files in a first pass before `processFile` is
246
+ * called for any file, ensuring that exported identifier references such as
247
+ * `NS_CALENDAR` from `@core/translations/ns` are resolved in all files
248
+ * regardless of processing order.
249
+ *
250
+ * @param file - Absolute or CWD-relative path of the source file to pre-scan
251
+ * @param astVisitors - Shared visitor instance (holds the shared constant tables)
252
+ * @param config - Extractor configuration (without plugins)
253
+ * @param logger - Logger for warnings/errors
254
+ * @param fileErrors - Optional array to collect per-file error messages
255
+ */
256
+ async function preScanFile(file, astVisitors, config, logger$1 = new logger.ConsoleLogger(), fileErrors) {
257
+ try {
258
+ const code = await promises.readFile(file, 'utf-8');
259
+ const fileExt = node_path.extname(file).toLowerCase();
260
+ const isTypeScriptFile = fileExt === '.ts' || fileExt === '.tsx' || fileExt === '.mts' || fileExt === '.cts';
261
+ const isTSX = fileExt === '.tsx';
262
+ const isJSX = fileExt === '.jsx';
263
+ let ast;
264
+ try {
265
+ ast = await core.parse(code, {
266
+ syntax: isTypeScriptFile ? 'typescript' : 'ecmascript',
267
+ tsx: isTSX,
268
+ jsx: isJSX,
269
+ decorators: true,
270
+ dynamicImport: true,
271
+ comments: true,
272
+ });
273
+ }
274
+ catch (err) {
275
+ if (fileExt === '.ts' && !isTSX) {
276
+ try {
277
+ ast = await core.parse(code, { syntax: 'typescript', tsx: true, decorators: true, dynamicImport: true, comments: true });
278
+ }
279
+ catch (err2) {
280
+ throw new validation.ExtractorError('Failed to pre-scan file', file, err2);
281
+ }
282
+ }
283
+ else if (fileExt === '.js' && !isJSX) {
284
+ try {
285
+ ast = await core.parse(code, { syntax: 'ecmascript', jsx: true, decorators: true, dynamicImport: true, comments: true });
286
+ }
287
+ catch (err2) {
288
+ throw new validation.ExtractorError('Failed to pre-scan file', file, err2);
289
+ }
290
+ }
291
+ else {
292
+ throw new validation.ExtractorError('Failed to pre-scan file', file, err);
293
+ }
294
+ }
295
+ const firstTokenIdx = astUtils.findFirstTokenIndex(code);
296
+ astUtils.normalizeASTSpans(ast, ast.span.start - firstTokenIdx);
297
+ astVisitors.setCurrentFile(file, code);
298
+ astVisitors.preScanForConstants(ast);
299
+ }
300
+ catch (error) {
301
+ if (error instanceof pluginManager.ConflictError)
302
+ throw error;
303
+ logger$1.warn(`${node_util.styleText('yellow', 'Skipping file in constants pre-scan due to error:')} ${file}`);
304
+ const err = error;
305
+ const msg = typeof err?.message === 'string' && err.message.trim().length > 0
306
+ ? err.message
307
+ : (typeof err === 'string' ? err : '') || err?.toString?.() || 'Unknown error';
308
+ if (fileErrors)
309
+ fileErrors.push(`${file}: ${msg}`);
310
+ }
311
+ }
312
+ /**
240
313
  * Used primarily for testing and programmatic access.
241
314
  *
242
315
  * @param config - The i18next toolkit configuration object
@@ -287,5 +360,6 @@ async function printLocizeFunnel(logger$1, force) {
287
360
  }
288
361
 
289
362
  exports.extract = extract;
363
+ exports.preScanFile = preScanFile;
290
364
  exports.processFile = processFile;
291
365
  exports.runExtractor = runExtractor;
@@ -86,7 +86,16 @@ async function findKeys(config, logger$1 = new logger.ConsoleLogger(), fileError
86
86
  pluginContext.getVarFromScope = astVisitors$1.getVarFromScope.bind(astVisitors$1);
87
87
  // 5. Initialize plugins
88
88
  await pluginManager.initializePlugins(plugins);
89
- // 6. Process each file
89
+ // 6. Pre-scan all files to populate cross-file shared constant tables BEFORE any
90
+ // key extraction begins. This ensures that identifier-based namespace references
91
+ // such as `useTranslation(NS_CALENDAR)` or `t('key', { ns: NS_SETTINGS })` resolve
92
+ // correctly even when the defining constants file is processed after the file that
93
+ // uses the identifier.
94
+ for (const file of sourceFiles) {
95
+ await extractor.preScanFile(file, astVisitors$1, otherConfig, logger$1, fileErrors);
96
+ }
97
+ // 7. Extraction pass: all shared tables are now fully populated, so every
98
+ // identifier reference can be resolved regardless of file order.
90
99
  for (const file of sourceFiles) {
91
100
  await extractor.processFile(file, plugins, astVisitors$1, pluginContext, otherConfig, logger$1, fileErrors);
92
101
  }
@@ -272,6 +272,20 @@ class ExpressionResolver {
272
272
  return v;
273
273
  return this.sharedVariableTable.get(name);
274
274
  }
275
+ /**
276
+ * Return the as-const object map stored for a variable name.
277
+ * Returns undefined if the name is not a known object map.
278
+ * Checks per-file variableTable first, then sharedEnumTable (for enums).
279
+ */
280
+ getObjectMap(name) {
281
+ const v = this.variableTable.get(name);
282
+ if (v && !Array.isArray(v) && typeof v === 'object')
283
+ return v;
284
+ const ev = this.sharedEnumTable.get(name);
285
+ if (ev)
286
+ return ev;
287
+ return undefined;
288
+ }
275
289
  /**
276
290
  * Capture a TypeScript enum declaration so members can be resolved later.
277
291
  * Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
@@ -557,14 +571,25 @@ class ExpressionResolver {
557
571
  // pattern 2:
558
572
  // Resolve a named type alias reference: `declare const x: ChangeType`
559
573
  // where `type ChangeType = 'all' | 'next' | 'this'` was captured earlier.
574
+ // Also handles `declare const d: SomeEnum` where SomeEnum is a TS enum with string values.
560
575
  if (type.type === 'TsTypeReference') {
561
576
  const typeName = type.typeName?.type === 'Identifier'
562
577
  ? type.typeName.value
563
578
  : undefined;
564
579
  if (typeName) {
580
+ // 1. Check type alias table first (exact match for string-literal unions)
565
581
  const aliasVals = this.typeAliasTable.get(typeName) ?? this.sharedTypeAliasTable.get(typeName);
566
582
  if (aliasVals && aliasVals.length > 0)
567
583
  return aliasVals;
584
+ // 2. Fall back to enum: `declare const d: Direction` where Direction is a string enum.
585
+ // sharedEnumTable maps enum-name → { MemberName: value }.
586
+ // A variable typed as the enum can take any of the enum's string values.
587
+ const enumMap = this.sharedEnumTable.get(typeName);
588
+ if (enumMap) {
589
+ const enumVals = Object.values(enumMap);
590
+ if (enumVals.length > 0)
591
+ return enumVals;
592
+ }
568
593
  }
569
594
  }
570
595
  // `(typeof ACCESS_OPTIONS)[number]` — resolve through the shared array variable table.
@@ -594,6 +619,40 @@ class ExpressionResolver {
594
619
  }
595
620
  catch { }
596
621
  }
622
+ // `keyof typeof MAP` — resolve to the keys of a known as-const object map.
623
+ // SWC emits: TsTypeOperator {
624
+ // operator: 'keyof',
625
+ // typeAnnotation: TsTypeQuery { exprName: Identifier }
626
+ // }
627
+ // This is the type of a variable that iterates over map keys:
628
+ // declare const k: keyof typeof LABELS; t(LABELS[k])
629
+ // Object.keys(MAP).forEach(k => t(MAP[k]))
630
+ if (type.type === 'TsTypeOperator') {
631
+ try {
632
+ const op = type.operator;
633
+ if (op === 'keyof') {
634
+ let inner = type.typeAnnotation;
635
+ while (inner?.type === 'TsParenthesizedType')
636
+ inner = inner.typeAnnotation;
637
+ if (inner?.type === 'TsTypeQuery' || inner?.type === 'TSTypeQuery') {
638
+ const exprName = inner.exprName ?? inner.expr ?? inner.entityName;
639
+ const varName = exprName?.value ?? exprName?.name;
640
+ if (varName) {
641
+ // Look up in variableTable (local) or sharedVariableTable (cross-file) for object maps
642
+ const v = this.variableTable.get(varName) ?? this.sharedVariableTable.get(varName);
643
+ if (v && !Array.isArray(v) && typeof v === 'object') {
644
+ return Object.keys(v);
645
+ }
646
+ // Also check sharedEnumTable (enum keys)
647
+ const ev = this.sharedEnumTable.get(varName);
648
+ if (ev)
649
+ return Object.keys(ev);
650
+ }
651
+ }
652
+ }
653
+ }
654
+ catch { }
655
+ }
597
656
  // We can't statically determine the value of other expressions (e.g., variables, function calls)
598
657
  return [];
599
658
  }
@@ -27,10 +27,10 @@ async function loadFile(file) {
27
27
  type: 'commonjs'
28
28
  }
29
29
  });
30
- const exports = {};
31
- const module = { exports };
30
+ const exports$1 = {};
31
+ const module = { exports: exports$1 };
32
32
  const context = vm.createContext({
33
- exports,
33
+ exports: exports$1,
34
34
  module,
35
35
  require: (id) => require(id),
36
36
  console,
package/dist/esm/cli.js CHANGED
@@ -29,7 +29,7 @@ const program = new Command();
29
29
  program
30
30
  .name('i18next-cli')
31
31
  .description('A unified, high-performance i18next CLI.')
32
- .version('1.49.3'); // This string is replaced with the actual version at build time by rollup
32
+ .version('1.49.5'); // This string is replaced with the actual version at build time by rollup
33
33
  // new: global config override option
34
34
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
35
35
  program
@@ -62,6 +62,65 @@ class ASTVisitors {
62
62
  this.callExpressionHandler = new CallExpressionHandler(config, pluginContext, logger, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode(), (name) => this.scopeManager.resolveSimpleStringIdentifier(name));
63
63
  this.jsxHandler = new JSXHandler(config, pluginContext, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode());
64
64
  }
65
+ /**
66
+ * Lightweight pre-scan pass: populates the shared constant / type-alias / array tables
67
+ * (`sharedConstants` in ScopeManager; `sharedVariableTable` and `sharedTypeAliasTable`
68
+ * in ExpressionResolver) WITHOUT performing any key extraction.
69
+ *
70
+ * Callers should invoke this for ALL source files before calling `visit()` for any file,
71
+ * so that cross-file identifier references — e.g. `useTranslation(NS_CALENDAR)` where
72
+ * `NS_CALENDAR` is exported from a separate constants file — are already resolved when
73
+ * the hook call is encountered during the extraction pass.
74
+ *
75
+ * The per-file tables (variableTable, typeAliasTable) are reset on each call so that
76
+ * local bindings from one file do not bleed into another; the shared tables accumulate
77
+ * across all calls and are intentionally NOT cleared here.
78
+ */
79
+ preScanForConstants(node) {
80
+ this.expressionResolver.resetFileSymbols();
81
+ this._walkForConstants(node);
82
+ }
83
+ /**
84
+ * Recursive walker used exclusively by `preScanForConstants`.
85
+ * Dispatches only to constant-capturing handlers; never extracts translation keys.
86
+ */
87
+ _walkForConstants(node) {
88
+ if (!node || typeof node !== 'object')
89
+ return;
90
+ switch (node.type) {
91
+ case 'VariableDeclarator':
92
+ // String constants → ScopeManager.sharedConstants
93
+ // as-const arrays/objects → ExpressionResolver.sharedVariableTable
94
+ this.scopeManager.handleVariableDeclarator(node);
95
+ this.expressionResolver.captureVariableDeclarator(node);
96
+ break;
97
+ case 'TsTypeAliasDeclaration':
98
+ case 'TSTypeAliasDeclaration':
99
+ case 'TsTypeAliasDecl':
100
+ // Type aliases → ExpressionResolver.sharedTypeAliasTable
101
+ this.expressionResolver.captureTypeAliasDeclaration(node);
102
+ break;
103
+ case 'FunctionDeclaration':
104
+ case 'FnDecl':
105
+ // Return-type annotations for t(fn()) patterns
106
+ this.expressionResolver.captureFunctionDeclaration(node);
107
+ break;
108
+ }
109
+ for (const key in node) {
110
+ if (key === 'span')
111
+ continue;
112
+ const child = node[key];
113
+ if (Array.isArray(child)) {
114
+ for (const item of child) {
115
+ if (item && typeof item === 'object')
116
+ this._walkForConstants(item);
117
+ }
118
+ }
119
+ else if (child && typeof child === 'object') {
120
+ this._walkForConstants(child);
121
+ }
122
+ }
123
+ }
65
124
  /**
66
125
  * Main entry point for AST traversal.
67
126
  * Creates a root scope and begins the recursive walk through the syntax tree.
@@ -454,6 +513,10 @@ class ASTVisitors {
454
513
  * If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
455
514
  * string-array constant, returns the callback's first parameter name and the
456
515
  * array values so the caller can inject a temporary variable binding.
516
+ *
517
+ * Also handles:
518
+ * `Object.keys(MAP).map/forEach(k => ...)` → param bound to MAP's keys
519
+ * `Object.values(MAP).map/forEach(v => ...)` → param bound to MAP's values
457
520
  */
458
521
  tryGetArrayIterationCallbackInfo(node) {
459
522
  try {
@@ -467,36 +530,67 @@ class ASTVisitors {
467
530
  return undefined;
468
531
  // The object must be an identifier whose value is a known string array
469
532
  const obj = callee.object;
470
- if (obj?.type !== 'Identifier')
471
- return undefined;
472
- const values = this.expressionResolver.getVariableValues(obj.value);
473
- if (!values || values.length === 0)
474
- return undefined;
475
- // First argument must be a callback with at least one parameter
476
- const callbackArg = node.arguments?.[0]?.expression;
477
- if (!callbackArg)
478
- return undefined;
479
- // Normalise param across SWC shapes: ArrowFunctionExpression / FunctionExpression
480
- const params = callbackArg.params ?? callbackArg.parameters ?? [];
481
- const firstParam = params[0];
482
- if (!firstParam)
483
- return undefined;
484
- // SWC wraps params in `Param { pat: Identifier }` or exposes them directly
485
- const ident = firstParam.type === 'Identifier'
486
- ? firstParam
487
- : firstParam.type === 'Param' && firstParam.pat?.type === 'Identifier'
488
- ? firstParam.pat
489
- : firstParam.type === 'AssignmentPattern' && firstParam.left?.type === 'Identifier'
490
- ? firstParam.left
491
- : null;
492
- if (!ident)
493
- return undefined;
494
- return { paramName: ident.value, values };
533
+ // ── Case 1: KNOWN_ARRAY.map(x => ...) ─────────────────────────────────
534
+ if (obj?.type === 'Identifier') {
535
+ const values = this.expressionResolver.getVariableValues(obj.value);
536
+ if (values && values.length > 0) {
537
+ return this.extractCallbackParam(node, values);
538
+ }
539
+ }
540
+ // ── Case 2: Object.keys(MAP).map(k => ...) ────────────────────────────
541
+ // Object.values(MAP).map(v => ...)
542
+ // callee.object is a CallExpression: Object.keys(...) / Object.values(...)
543
+ if (obj?.type === 'CallExpression') {
544
+ const innerCallee = obj.callee;
545
+ // Must be `Object.keys` or `Object.values`
546
+ if (innerCallee?.type === 'MemberExpression' &&
547
+ innerCallee.object?.type === 'Identifier' &&
548
+ innerCallee.object.value === 'Object' &&
549
+ innerCallee.property?.type === 'Identifier' &&
550
+ (innerCallee.property.value === 'keys' || innerCallee.property.value === 'values')) {
551
+ const isKeys = innerCallee.property.value === 'keys';
552
+ // The single argument to Object.keys/values must be a known identifier
553
+ const mapArg = obj.arguments?.[0]?.expression;
554
+ if (mapArg?.type === 'Identifier') {
555
+ const mapEntry = this.expressionResolver.getObjectMap(mapArg.value);
556
+ if (mapEntry) {
557
+ const values = isKeys ? Object.keys(mapEntry) : Object.values(mapEntry);
558
+ if (values.length > 0) {
559
+ return this.extractCallbackParam(node, values);
560
+ }
561
+ }
562
+ }
563
+ }
564
+ }
565
+ return undefined;
495
566
  }
496
567
  catch {
497
568
  return undefined;
498
569
  }
499
570
  }
571
+ /**
572
+ * Extracts the first callback parameter identifier from an iteration call node
573
+ * and pairs it with the provided values array.
574
+ */
575
+ extractCallbackParam(node, values) {
576
+ const callbackArg = node.arguments?.[0]?.expression;
577
+ if (!callbackArg)
578
+ return undefined;
579
+ const params = callbackArg.params ?? callbackArg.parameters ?? [];
580
+ const firstParam = params[0];
581
+ if (!firstParam)
582
+ return undefined;
583
+ const ident = firstParam.type === 'Identifier'
584
+ ? firstParam
585
+ : firstParam.type === 'Param' && firstParam.pat?.type === 'Identifier'
586
+ ? firstParam.pat
587
+ : firstParam.type === 'AssignmentPattern' && firstParam.left?.type === 'Identifier'
588
+ ? firstParam.left
589
+ : null;
590
+ if (!ident)
591
+ return undefined;
592
+ return { paramName: ident.value, values };
593
+ }
500
594
  /**
501
595
  * Retrieves variable information from the scope chain.
502
596
  * Searches from innermost to outermost scope.
@@ -234,7 +234,80 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo
234
234
  }
235
235
  }
236
236
  /**
237
- * Simplified extraction function that returns translation results without file writing.
237
+ * Lightweight pre-scan pass for a single file.
238
+ *
239
+ * Parses the file and calls `astVisitors.preScanForConstants()` to populate
240
+ * cross-file shared constant / type-alias / array tables WITHOUT extracting
241
+ * any translation keys or running plugin hooks.
242
+ *
243
+ * Intended to be called for ALL files in a first pass before `processFile` is
244
+ * called for any file, ensuring that exported identifier references such as
245
+ * `NS_CALENDAR` from `@core/translations/ns` are resolved in all files
246
+ * regardless of processing order.
247
+ *
248
+ * @param file - Absolute or CWD-relative path of the source file to pre-scan
249
+ * @param astVisitors - Shared visitor instance (holds the shared constant tables)
250
+ * @param config - Extractor configuration (without plugins)
251
+ * @param logger - Logger for warnings/errors
252
+ * @param fileErrors - Optional array to collect per-file error messages
253
+ */
254
+ async function preScanFile(file, astVisitors, config, logger = new ConsoleLogger(), fileErrors) {
255
+ try {
256
+ const code = await readFile(file, 'utf-8');
257
+ const fileExt = extname(file).toLowerCase();
258
+ const isTypeScriptFile = fileExt === '.ts' || fileExt === '.tsx' || fileExt === '.mts' || fileExt === '.cts';
259
+ const isTSX = fileExt === '.tsx';
260
+ const isJSX = fileExt === '.jsx';
261
+ let ast;
262
+ try {
263
+ ast = await parse(code, {
264
+ syntax: isTypeScriptFile ? 'typescript' : 'ecmascript',
265
+ tsx: isTSX,
266
+ jsx: isJSX,
267
+ decorators: true,
268
+ dynamicImport: true,
269
+ comments: true,
270
+ });
271
+ }
272
+ catch (err) {
273
+ if (fileExt === '.ts' && !isTSX) {
274
+ try {
275
+ ast = await parse(code, { syntax: 'typescript', tsx: true, decorators: true, dynamicImport: true, comments: true });
276
+ }
277
+ catch (err2) {
278
+ throw new ExtractorError('Failed to pre-scan file', file, err2);
279
+ }
280
+ }
281
+ else if (fileExt === '.js' && !isJSX) {
282
+ try {
283
+ ast = await parse(code, { syntax: 'ecmascript', jsx: true, decorators: true, dynamicImport: true, comments: true });
284
+ }
285
+ catch (err2) {
286
+ throw new ExtractorError('Failed to pre-scan file', file, err2);
287
+ }
288
+ }
289
+ else {
290
+ throw new ExtractorError('Failed to pre-scan file', file, err);
291
+ }
292
+ }
293
+ const firstTokenIdx = findFirstTokenIndex(code);
294
+ normalizeASTSpans(ast, ast.span.start - firstTokenIdx);
295
+ astVisitors.setCurrentFile(file, code);
296
+ astVisitors.preScanForConstants(ast);
297
+ }
298
+ catch (error) {
299
+ if (error instanceof ConflictError)
300
+ throw error;
301
+ logger.warn(`${styleText('yellow', 'Skipping file in constants pre-scan due to error:')} ${file}`);
302
+ const err = error;
303
+ const msg = typeof err?.message === 'string' && err.message.trim().length > 0
304
+ ? err.message
305
+ : (typeof err === 'string' ? err : '') || err?.toString?.() || 'Unknown error';
306
+ if (fileErrors)
307
+ fileErrors.push(`${file}: ${msg}`);
308
+ }
309
+ }
310
+ /**
238
311
  * Used primarily for testing and programmatic access.
239
312
  *
240
313
  * @param config - The i18next toolkit configuration object
@@ -284,4 +357,4 @@ async function printLocizeFunnel(logger, force) {
284
357
  return recordFunnelShown('extract');
285
358
  }
286
359
 
287
- export { extract, processFile, runExtractor };
360
+ export { extract, preScanFile, processFile, runExtractor };
@@ -1,5 +1,5 @@
1
1
  import { glob } from 'glob';
2
- import { processFile } from './extractor.js';
2
+ import { preScanFile, processFile } from './extractor.js';
3
3
  import { ConsoleLogger } from '../../utils/logger.js';
4
4
  import { createPluginContext, initializePlugins } from '../plugin-manager.js';
5
5
  import { ASTVisitors } from './ast-visitors.js';
@@ -84,7 +84,16 @@ async function findKeys(config, logger = new ConsoleLogger(), fileErrors) {
84
84
  pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors);
85
85
  // 5. Initialize plugins
86
86
  await initializePlugins(plugins);
87
- // 6. Process each file
87
+ // 6. Pre-scan all files to populate cross-file shared constant tables BEFORE any
88
+ // key extraction begins. This ensures that identifier-based namespace references
89
+ // such as `useTranslation(NS_CALENDAR)` or `t('key', { ns: NS_SETTINGS })` resolve
90
+ // correctly even when the defining constants file is processed after the file that
91
+ // uses the identifier.
92
+ for (const file of sourceFiles) {
93
+ await preScanFile(file, astVisitors, otherConfig, logger, fileErrors);
94
+ }
95
+ // 7. Extraction pass: all shared tables are now fully populated, so every
96
+ // identifier reference can be resolved regardless of file order.
88
97
  for (const file of sourceFiles) {
89
98
  await processFile(file, plugins, astVisitors, pluginContext, otherConfig, logger, fileErrors);
90
99
  }
@@ -270,6 +270,20 @@ class ExpressionResolver {
270
270
  return v;
271
271
  return this.sharedVariableTable.get(name);
272
272
  }
273
+ /**
274
+ * Return the as-const object map stored for a variable name.
275
+ * Returns undefined if the name is not a known object map.
276
+ * Checks per-file variableTable first, then sharedEnumTable (for enums).
277
+ */
278
+ getObjectMap(name) {
279
+ const v = this.variableTable.get(name);
280
+ if (v && !Array.isArray(v) && typeof v === 'object')
281
+ return v;
282
+ const ev = this.sharedEnumTable.get(name);
283
+ if (ev)
284
+ return ev;
285
+ return undefined;
286
+ }
273
287
  /**
274
288
  * Capture a TypeScript enum declaration so members can be resolved later.
275
289
  * Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
@@ -555,14 +569,25 @@ class ExpressionResolver {
555
569
  // pattern 2:
556
570
  // Resolve a named type alias reference: `declare const x: ChangeType`
557
571
  // where `type ChangeType = 'all' | 'next' | 'this'` was captured earlier.
572
+ // Also handles `declare const d: SomeEnum` where SomeEnum is a TS enum with string values.
558
573
  if (type.type === 'TsTypeReference') {
559
574
  const typeName = type.typeName?.type === 'Identifier'
560
575
  ? type.typeName.value
561
576
  : undefined;
562
577
  if (typeName) {
578
+ // 1. Check type alias table first (exact match for string-literal unions)
563
579
  const aliasVals = this.typeAliasTable.get(typeName) ?? this.sharedTypeAliasTable.get(typeName);
564
580
  if (aliasVals && aliasVals.length > 0)
565
581
  return aliasVals;
582
+ // 2. Fall back to enum: `declare const d: Direction` where Direction is a string enum.
583
+ // sharedEnumTable maps enum-name → { MemberName: value }.
584
+ // A variable typed as the enum can take any of the enum's string values.
585
+ const enumMap = this.sharedEnumTable.get(typeName);
586
+ if (enumMap) {
587
+ const enumVals = Object.values(enumMap);
588
+ if (enumVals.length > 0)
589
+ return enumVals;
590
+ }
566
591
  }
567
592
  }
568
593
  // `(typeof ACCESS_OPTIONS)[number]` — resolve through the shared array variable table.
@@ -592,6 +617,40 @@ class ExpressionResolver {
592
617
  }
593
618
  catch { }
594
619
  }
620
+ // `keyof typeof MAP` — resolve to the keys of a known as-const object map.
621
+ // SWC emits: TsTypeOperator {
622
+ // operator: 'keyof',
623
+ // typeAnnotation: TsTypeQuery { exprName: Identifier }
624
+ // }
625
+ // This is the type of a variable that iterates over map keys:
626
+ // declare const k: keyof typeof LABELS; t(LABELS[k])
627
+ // Object.keys(MAP).forEach(k => t(MAP[k]))
628
+ if (type.type === 'TsTypeOperator') {
629
+ try {
630
+ const op = type.operator;
631
+ if (op === 'keyof') {
632
+ let inner = type.typeAnnotation;
633
+ while (inner?.type === 'TsParenthesizedType')
634
+ inner = inner.typeAnnotation;
635
+ if (inner?.type === 'TsTypeQuery' || inner?.type === 'TSTypeQuery') {
636
+ const exprName = inner.exprName ?? inner.expr ?? inner.entityName;
637
+ const varName = exprName?.value ?? exprName?.name;
638
+ if (varName) {
639
+ // Look up in variableTable (local) or sharedVariableTable (cross-file) for object maps
640
+ const v = this.variableTable.get(varName) ?? this.sharedVariableTable.get(varName);
641
+ if (v && !Array.isArray(v) && typeof v === 'object') {
642
+ return Object.keys(v);
643
+ }
644
+ // Also check sharedEnumTable (enum keys)
645
+ const ev = this.sharedEnumTable.get(varName);
646
+ if (ev)
647
+ return Object.keys(ev);
648
+ }
649
+ }
650
+ }
651
+ }
652
+ catch { }
653
+ }
595
654
  // We can't statically determine the value of other expressions (e.g., variables, function calls)
596
655
  return [];
597
656
  }
@@ -25,10 +25,10 @@ async function loadFile(file) {
25
25
  type: 'commonjs'
26
26
  }
27
27
  });
28
- const exports = {};
29
- const module = { exports };
28
+ const exports$1 = {};
29
+ const module = { exports: exports$1 };
30
30
  const context = vm.createContext({
31
- exports,
31
+ exports: exports$1,
32
32
  module,
33
33
  require: (id) => require(id),
34
34
  console,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.49.3",
3
+ "version": "1.49.5",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,39 +52,39 @@
52
52
  "url": "https://github.com/i18next/i18next-cli/issues"
53
53
  },
54
54
  "devDependencies": {
55
- "@rollup/plugin-replace": "6.0.3",
56
- "@rollup/plugin-terser": "0.4.4",
57
- "@types/inquirer": "9.0.9",
58
- "@types/node": "25.3.3",
59
- "@types/react": "19.2.14",
60
- "@typescript-eslint/parser": "^8.56.1",
61
- "@vitest/coverage-v8": "4.0.18",
62
- "eslint": "9.39.2",
55
+ "@rollup/plugin-replace": "^6.0.3",
56
+ "@rollup/plugin-terser": "^1.0.0",
57
+ "@types/inquirer": "^9.0.9",
58
+ "@types/node": "^25.4.0",
59
+ "@types/react": "^19.2.14",
60
+ "@typescript-eslint/parser": "^8.57.0",
61
+ "@vitest/coverage-v8": "^4.0.18",
62
+ "eslint": "^9.39.2",
63
63
  "eslint-import-resolver-typescript": "^4.4.4",
64
- "eslint-plugin-import": "2.32.0",
65
- "memfs": "4.56.11",
66
- "neostandard": "0.13.0",
67
- "rollup-plugin-typescript2": "0.36.0",
68
- "typescript": "5.9.3",
69
- "unplugin-swc": "1.5.9",
70
- "vitest": "4.0.18"
64
+ "eslint-plugin-import": "^2.32.0",
65
+ "memfs": "^4.56.11",
66
+ "neostandard": "^0.13.0",
67
+ "rollup-plugin-typescript2": "^0.36.0",
68
+ "typescript": "^5.9.3",
69
+ "unplugin-swc": "^1.5.9",
70
+ "vitest": "^4.0.18"
71
71
  },
72
72
  "dependencies": {
73
- "@croct/json5-parser": "0.2.2",
74
- "@swc/core": "1.15.18",
75
- "chokidar": "5.0.0",
76
- "commander": "14.0.3",
77
- "execa": "9.6.1",
78
- "glob": "13.0.6",
79
- "i18next-resources-for-ts": "2.0.0",
80
- "inquirer": "13.3.0",
81
- "jiti": "2.6.1",
82
- "jsonc-parser": "3.3.1",
83
- "magic-string": "0.30.21",
84
- "minimatch": "10.2.4",
85
- "ora": "9.3.0",
73
+ "@croct/json5-parser": "^0.2.2",
74
+ "@swc/core": "^1.15.18",
75
+ "chokidar": "^5.0.0",
76
+ "commander": "^14.0.3",
77
+ "execa": "^9.6.1",
78
+ "glob": "^13.0.6",
79
+ "i18next-resources-for-ts": "^2.0.1",
80
+ "inquirer": "^13.3.0",
81
+ "jiti": "^2.6.1",
82
+ "jsonc-parser": "^3.3.1",
83
+ "magic-string": "^0.30.21",
84
+ "minimatch": "^10.2.4",
85
+ "ora": "^9.3.0",
86
86
  "react": "^19.2.4",
87
- "react-i18next": "^16.5.5",
88
- "yaml": "2.8.2"
87
+ "react-i18next": "^16.5.6",
88
+ "yaml": "^2.8.2"
89
89
  }
90
90
  }
@@ -44,6 +44,26 @@ export declare class ASTVisitors {
44
44
  * @param logger - Logger for warnings and debug information
45
45
  */
46
46
  constructor(config: Omit<I18nextToolkitConfig, 'plugins'>, pluginContext: PluginContext, logger: Logger, hooks?: ASTVisitorHooks, expressionResolver?: ExpressionResolver);
47
+ /**
48
+ * Lightweight pre-scan pass: populates the shared constant / type-alias / array tables
49
+ * (`sharedConstants` in ScopeManager; `sharedVariableTable` and `sharedTypeAliasTable`
50
+ * in ExpressionResolver) WITHOUT performing any key extraction.
51
+ *
52
+ * Callers should invoke this for ALL source files before calling `visit()` for any file,
53
+ * so that cross-file identifier references — e.g. `useTranslation(NS_CALENDAR)` where
54
+ * `NS_CALENDAR` is exported from a separate constants file — are already resolved when
55
+ * the hook call is encountered during the extraction pass.
56
+ *
57
+ * The per-file tables (variableTable, typeAliasTable) are reset on each call so that
58
+ * local bindings from one file do not bleed into another; the shared tables accumulate
59
+ * across all calls and are intentionally NOT cleared here.
60
+ */
61
+ preScanForConstants(node: Module): void;
62
+ /**
63
+ * Recursive walker used exclusively by `preScanForConstants`.
64
+ * Dispatches only to constant-capturing handlers; never extracts translation keys.
65
+ */
66
+ private _walkForConstants;
47
67
  /**
48
68
  * Main entry point for AST traversal.
49
69
  * Creates a root scope and begins the recursive walk through the syntax tree.
@@ -69,8 +89,17 @@ export declare class ASTVisitors {
69
89
  * If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
70
90
  * string-array constant, returns the callback's first parameter name and the
71
91
  * array values so the caller can inject a temporary variable binding.
92
+ *
93
+ * Also handles:
94
+ * `Object.keys(MAP).map/forEach(k => ...)` → param bound to MAP's keys
95
+ * `Object.values(MAP).map/forEach(v => ...)` → param bound to MAP's values
72
96
  */
73
97
  private tryGetArrayIterationCallbackInfo;
98
+ /**
99
+ * Extracts the first callback parameter identifier from an iteration call node
100
+ * and pairs it with the provided values array.
101
+ */
102
+ private extractCallbackParam;
74
103
  /**
75
104
  * Retrieves variable information from the scope chain.
76
105
  * Searches from innermost to outermost scope.
@@ -1 +1 @@
1
- {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC7G,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAItE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,SAAgB,YAAY,EAAE,YAAY,CAAA;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAAa;IAEhC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,kBAAkB;IAiCzC;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IAgWZ;;;;OAIG;IACH,OAAO,CAAC,gCAAgC;IAwCxC;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI5D;;OAEG;IACI,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKxD;;;;;;OAMG;IACI,cAAc,IAAK,MAAM;IAIhC;;OAEG;IACI,cAAc,IAAK,MAAM;CAGjC"}
1
+ {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC7G,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAItE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,SAAgB,YAAY,EAAE,YAAY,CAAA;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAAa;IAEhC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,kBAAkB;IAiCzC;;;;;;;;;;;;;OAaG;IACI,mBAAmB,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAK/C;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAoCzB;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IAgWZ;;;;;;;;OAQG;IACH,OAAO,CAAC,gCAAgC;IAqDxC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAqB5B;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI5D;;OAEG;IACI,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKxD;;;;;;OAMG;IACI,cAAc,IAAK,MAAM;IAIhC;;OAEG;IACI,cAAc,IAAK,MAAM;CAGjC"}
@@ -59,7 +59,25 @@ export declare function runExtractor(config: I18nextToolkitConfig, options?: {
59
59
  */
60
60
  export declare function processFile(file: string, plugins: Plugin[], astVisitors: ASTVisitors, pluginContext: PluginContext, config: Omit<I18nextToolkitConfig, 'plugins'>, logger?: Logger, fileErrors?: string[]): Promise<void>;
61
61
  /**
62
- * Simplified extraction function that returns translation results without file writing.
62
+ * Lightweight pre-scan pass for a single file.
63
+ *
64
+ * Parses the file and calls `astVisitors.preScanForConstants()` to populate
65
+ * cross-file shared constant / type-alias / array tables WITHOUT extracting
66
+ * any translation keys or running plugin hooks.
67
+ *
68
+ * Intended to be called for ALL files in a first pass before `processFile` is
69
+ * called for any file, ensuring that exported identifier references such as
70
+ * `NS_CALENDAR` from `@core/translations/ns` are resolved in all files
71
+ * regardless of processing order.
72
+ *
73
+ * @param file - Absolute or CWD-relative path of the source file to pre-scan
74
+ * @param astVisitors - Shared visitor instance (holds the shared constant tables)
75
+ * @param config - Extractor configuration (without plugins)
76
+ * @param logger - Logger for warnings/errors
77
+ * @param fileErrors - Optional array to collect per-file error messages
78
+ */
79
+ export declare function preScanFile(file: string, astVisitors: ASTVisitors, config: Omit<I18nextToolkitConfig, 'plugins'>, logger?: Logger, fileErrors?: string[]): Promise<void>;
80
+ /**
63
81
  * Used primarily for testing and programmatic access.
64
82
  *
65
83
  * @param config - The i18next toolkit configuration object
@@ -1 +1 @@
1
- {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAO5G,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAK/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CA6E1D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAkHf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAO1K"}
1
+ {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAO5G,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAK/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CA6E1D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAkHf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAO1K"}
@@ -1 +1 @@
1
- {"version":3,"file":"key-finder.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/key-finder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,oBAAoB,EAAmB,MAAM,gBAAgB,CAAA;AAOjG;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CA6E1E"}
1
+ {"version":3,"file":"key-finder.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/key-finder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,oBAAoB,EAAmB,MAAM,gBAAgB,CAAA;AAOjG;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAuF1E"}
@@ -64,6 +64,12 @@ export declare class ExpressionResolver {
64
64
  * Returns undefined if the name is not a known string array.
65
65
  */
66
66
  getVariableValues(name: string): string[] | undefined;
67
+ /**
68
+ * Return the as-const object map stored for a variable name.
69
+ * Returns undefined if the name is not a known object map.
70
+ * Checks per-file variableTable first, then sharedEnumTable (for enums).
71
+ */
72
+ getObjectMap(name: string): Record<string, string> | undefined;
67
73
  /**
68
74
  * Capture a TypeScript enum declaration so members can be resolved later.
69
75
  * Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
@@ -1 +1 @@
1
- {"version":3,"file":"expression-resolver.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/expression-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,WAAW,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAiB;IAK9B,OAAO,CAAC,aAAa,CAA4D;IAGjF,OAAO,CAAC,eAAe,CAAiD;IAIxE,OAAO,CAAC,cAAc,CAAmC;IAIzD,OAAO,CAAC,mBAAmB,CAAmC;IAI9D,OAAO,CAAC,oBAAoB,CAAmC;IAI/D,OAAO,CAAC,kBAAkB,CAAmC;gBAEhD,KAAK,EAAE,eAAe;IAInC;;OAEG;IACI,gBAAgB,IAAK,IAAI;IAMhC;;;;;;;;;OASG;IACH,yBAAyB,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAwI3C;;;;;;;OAOG;IACH,2BAA2B,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAkB7C;;;;;;;;;OASG;IACH,0BAA0B,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAoB5C;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;;OAIG;IACI,oBAAoB,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI;IAIlE;;OAEG;IACI,uBAAuB,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAInD;;;OAGG;IACI,iBAAiB,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAQ7D;;;;;OAKG;IACH,sBAAsB,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAwBxC;;;;;;;OAOG;IACH,kCAAkC,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKrE;;;;;;;OAOG;IACH,8BAA8B,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKjE;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,yCAAyC;IAgMjD,OAAO,CAAC,mCAAmC;IAsE3C;;;;;;OAMG;IACH,OAAO,CAAC,6CAA6C;IAyBrD;;;;;;OAMG;IACH,OAAO,CAAC,kDAAkD;CAwB3D"}
1
+ {"version":3,"file":"expression-resolver.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/expression-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,WAAW,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAiB;IAK9B,OAAO,CAAC,aAAa,CAA4D;IAGjF,OAAO,CAAC,eAAe,CAAiD;IAIxE,OAAO,CAAC,cAAc,CAAmC;IAIzD,OAAO,CAAC,mBAAmB,CAAmC;IAI9D,OAAO,CAAC,oBAAoB,CAAmC;IAI/D,OAAO,CAAC,kBAAkB,CAAmC;gBAEhD,KAAK,EAAE,eAAe;IAInC;;OAEG;IACI,gBAAgB,IAAK,IAAI;IAMhC;;;;;;;;;OASG;IACH,yBAAyB,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAwI3C;;;;;;;OAOG;IACH,2BAA2B,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAkB7C;;;;;;;;;OASG;IACH,0BAA0B,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAoB5C;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;;OAIG;IACI,oBAAoB,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI;IAIlE;;OAEG;IACI,uBAAuB,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAInD;;;OAGG;IACI,iBAAiB,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAQ7D;;;;OAIG;IACI,YAAY,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS;IAQtE;;;;;OAKG;IACH,sBAAsB,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAwBxC;;;;;;;OAOG;IACH,kCAAkC,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKrE;;;;;;;OAOG;IACH,8BAA8B,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKjE;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,yCAAyC;IAgMjD,OAAO,CAAC,mCAAmC;IAiH3C;;;;;;OAMG;IACH,OAAO,CAAC,6CAA6C;IAyBrD;;;;;;OAMG;IACH,OAAO,CAAC,kDAAkD;CAwB3D"}