i18next-cli 1.48.1 → 1.49.1

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.
@@ -59,7 +59,7 @@ class ASTVisitors {
59
59
  this.scopeManager = new ScopeManager(config);
60
60
  // use shared resolver when provided so captured enums/objects are visible across files
61
61
  this.expressionResolver = expressionResolver ?? new ExpressionResolver(this.hooks);
62
- this.callExpressionHandler = new CallExpressionHandler(config, pluginContext, logger, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode());
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
65
  /**
@@ -271,6 +271,17 @@ class ASTVisitors {
271
271
  // capture enums into resolver symbol table
272
272
  this.expressionResolver.captureEnumDeclaration(node);
273
273
  break;
274
+ // pattern 2: capture type aliases so `declare const x: Alias` can be resolved
275
+ case 'TsTypeAliasDeclaration':
276
+ case 'TSTypeAliasDeclaration':
277
+ case 'TsTypeAliasDecl':
278
+ this.expressionResolver.captureTypeAliasDeclaration(node);
279
+ break;
280
+ // pattern 3: capture function return types so `t(fn())` can be resolved
281
+ case 'FunctionDeclaration':
282
+ case 'FnDecl':
283
+ this.expressionResolver.captureFunctionDeclaration(node);
284
+ break;
274
285
  case 'CallExpression':
275
286
  this.callExpressionHandler.handleCallExpression(node, this.scopeManager.getVarFromScope.bind(this.scopeManager));
276
287
  break;
@@ -288,6 +299,18 @@ class ASTVisitors {
288
299
  }
289
300
  this.hooks.onAfterVisitNode?.(node);
290
301
  // --- END VISIT LOGIC ---
302
+ // Detect array iteration calls (.map / .forEach / .flatMap etc.) on a known
303
+ // as-const array so the callback parameter is bound to the array values while
304
+ // the callback body is walked. We inject the binding BEFORE generic recursion
305
+ // and remove it AFTER, so the whole subtree sees the correct value.
306
+ let arrayCallbackCleanup;
307
+ if (node.type === 'CallExpression') {
308
+ const info = this.tryGetArrayIterationCallbackInfo(node);
309
+ if (info) {
310
+ this.expressionResolver.setTemporaryVariable(info.paramName, info.values);
311
+ arrayCallbackCleanup = () => this.expressionResolver.deleteTemporaryVariable(info.paramName);
312
+ }
313
+ }
291
314
  // --- RECURSION ---
292
315
  // Recurse into the children of the current node
293
316
  for (const key in node) {
@@ -295,32 +318,112 @@ class ASTVisitors {
295
318
  continue;
296
319
  const child = node[key];
297
320
  if (Array.isArray(child)) {
298
- // Pre-scan array children to register VariableDeclarator-based scopes
299
- // (e.g., `const { t } = useTranslation(...)`) before walking the rest
300
- // of the items. This ensures that functions/arrow-functions defined
301
- // earlier in the same block that reference t will resolve to the
302
- // correct scope even if the `useTranslation` declarator appears later.
321
+ // Pre-scan array children in THREE passes:
322
+ // Pass 1 variables WITH init (arrays, objects, strings, fns) + enums
323
+ // Pass 2 type aliases + functions (may depend on pass-1 arrays)
324
+ // Pass 3 `declare const x: Type` (no init; depends on pass-2 type aliases)
325
+ // This ordering ensures e.g.:
326
+ // const OPTS = ['a','b'] as const → pass 1
327
+ // type T = (typeof OPTS)[number] → pass 2 (resolves OPTS)
328
+ // declare const v: T → pass 3 (resolves T)
329
+ // ── Pass 1: variables with init ──────────────────────────────────────
303
330
  for (const item of child) {
304
331
  if (!item || typeof item !== 'object')
305
332
  continue;
306
- // Direct declarator present in arrays (rare)
307
- if (item.type === 'VariableDeclarator') {
333
+ // Direct declarator (rare)
334
+ if (item.type === 'VariableDeclarator' && item.init) {
308
335
  this.scopeManager.handleVariableDeclarator(item);
309
336
  this.expressionResolver.captureVariableDeclarator(item);
310
337
  continue;
311
338
  }
312
- // enum declarations can appear as ExportDeclaration.declaration earlier; be permissive
313
- if (item && item.id && Array.isArray(item.members)) {
339
+ // enum declarations
340
+ if (item.id && Array.isArray(item.members)) {
314
341
  this.expressionResolver.captureEnumDeclaration(item);
315
- // continue to allow further traversal
316
342
  }
317
- // Common case: VariableDeclaration which contains .declarations (VariableDeclarator[])
343
+ // Bare VariableDeclaration only declarators that have an init
344
+ if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
345
+ for (const decl of item.declarations) {
346
+ if (decl?.type === 'VariableDeclarator' && decl.init) {
347
+ this.scopeManager.handleVariableDeclarator(decl);
348
+ this.expressionResolver.captureVariableDeclarator(decl);
349
+ }
350
+ }
351
+ }
352
+ // ExportDeclaration wrapping VariableDeclaration — only inited declarators
353
+ if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
354
+ const inner = item.declaration;
355
+ if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
356
+ for (const vd of inner.declarations) {
357
+ if (vd?.type === 'VariableDeclarator' && vd.init) {
358
+ this.scopeManager.handleVariableDeclarator(vd);
359
+ this.expressionResolver.captureVariableDeclarator(vd);
360
+ }
361
+ }
362
+ }
363
+ }
364
+ }
365
+ // ── Pass 2: type aliases + functions ─────────────────────────────────
366
+ for (const item of child) {
367
+ if (!item || typeof item !== 'object')
368
+ continue;
369
+ if (item.type === 'TsTypeAliasDeclaration' || item.type === 'TSTypeAliasDeclaration' || item.type === 'TsTypeAliasDecl') {
370
+ this.expressionResolver.captureTypeAliasDeclaration(item);
371
+ }
372
+ if (item.type === 'FunctionDeclaration' || item.type === 'FnDecl') {
373
+ this.expressionResolver.captureFunctionDeclaration(item);
374
+ }
375
+ if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
376
+ const inner = item.declaration;
377
+ if (inner.type === 'TsTypeAliasDeclaration' || inner.type === 'TSTypeAliasDeclaration' || inner.type === 'TsTypeAliasDecl') {
378
+ this.expressionResolver.captureTypeAliasDeclaration(inner);
379
+ }
380
+ if (inner.type === 'FunctionDeclaration' || inner.type === 'FnDecl') {
381
+ this.expressionResolver.captureFunctionDeclaration(inner);
382
+ }
383
+ }
384
+ }
385
+ // ── Pass 3: `declare const x: Type` — no init, depends on type aliases ─
386
+ // Also re-processes ArrayPattern destructuring (e.g. useState<T>) whose
387
+ // type argument resolution failed in Pass 1 because typeAliasTable was empty.
388
+ for (const item of child) {
389
+ if (!item || typeof item !== 'object')
390
+ continue;
391
+ // Direct declarator with no init
392
+ if (item.type === 'VariableDeclarator' && !item.init) {
393
+ this.scopeManager.handleVariableDeclarator(item);
394
+ this.expressionResolver.captureVariableDeclarator(item);
395
+ continue;
396
+ }
397
+ // ArrayPattern destructuring with init — re-run now that type aliases are populated
398
+ if (item.type === 'VariableDeclarator' && item.init && item.id?.type === 'ArrayPattern') {
399
+ this.expressionResolver.captureVariableDeclarator(item);
400
+ continue;
401
+ }
402
+ // VariableDeclaration — process no-init declarators and re-process ArrayPattern ones
318
403
  if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
319
404
  for (const decl of item.declarations) {
320
- if (decl && typeof decl === 'object' && decl.type === 'VariableDeclarator') {
405
+ if (!decl.init) {
321
406
  this.scopeManager.handleVariableDeclarator(decl);
322
407
  this.expressionResolver.captureVariableDeclarator(decl);
323
408
  }
409
+ else if (decl.id?.type === 'ArrayPattern') {
410
+ this.expressionResolver.captureVariableDeclarator(decl);
411
+ }
412
+ }
413
+ }
414
+ // ExportDeclaration wrapping — same logic
415
+ if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
416
+ const inner = item.declaration;
417
+ if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
418
+ for (const vd of inner.declarations) {
419
+ if (!vd.init) {
420
+ this.scopeManager.handleVariableDeclarator(vd);
421
+ this.expressionResolver.captureVariableDeclarator(vd);
422
+ }
423
+ else if (vd.id?.type === 'ArrayPattern') {
424
+ this.expressionResolver.captureVariableDeclarator(vd);
425
+ }
426
+ }
324
427
  }
325
428
  }
326
429
  }
@@ -340,11 +443,60 @@ class ASTVisitors {
340
443
  }
341
444
  }
342
445
  // --- END RECURSION ---
446
+ // Remove temporary callback param binding if one was injected for this node
447
+ arrayCallbackCleanup?.();
343
448
  // LEAVE SCOPE for functions
344
449
  if (isNewScope) {
345
450
  this.scopeManager.exitScope();
346
451
  }
347
452
  }
453
+ /**
454
+ * If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
455
+ * string-array constant, returns the callback's first parameter name and the
456
+ * array values so the caller can inject a temporary variable binding.
457
+ */
458
+ tryGetArrayIterationCallbackInfo(node) {
459
+ try {
460
+ const callee = node.callee;
461
+ if (callee?.type !== 'MemberExpression')
462
+ return undefined;
463
+ const prop = callee.property;
464
+ if (prop?.type !== 'Identifier')
465
+ return undefined;
466
+ if (!['map', 'forEach', 'flatMap', 'filter', 'find', 'some', 'every'].includes(prop.value))
467
+ return undefined;
468
+ // The object must be an identifier whose value is a known string array
469
+ 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 };
495
+ }
496
+ catch {
497
+ return undefined;
498
+ }
499
+ }
348
500
  /**
349
501
  * Retrieves variable information from the scope chain.
350
502
  * Searches from innermost to outermost scope.
@@ -10,13 +10,15 @@ class CallExpressionHandler {
10
10
  objectKeys = new Set();
11
11
  getCurrentFile;
12
12
  getCurrentCode;
13
- constructor(config, pluginContext, logger, expressionResolver, getCurrentFile, getCurrentCode) {
13
+ resolveIdentifier;
14
+ constructor(config, pluginContext, logger, expressionResolver, getCurrentFile, getCurrentCode, resolveIdentifier = () => undefined) {
14
15
  this.config = config;
15
16
  this.pluginContext = pluginContext;
16
17
  this.logger = logger;
17
18
  this.expressionResolver = expressionResolver;
18
19
  this.getCurrentFile = getCurrentFile;
19
20
  this.getCurrentCode = getCurrentCode;
21
+ this.resolveIdentifier = resolveIdentifier;
20
22
  }
21
23
  /**
22
24
  * Computes line and column from a node's normalised span.
@@ -151,8 +153,8 @@ class CallExpressionHandler {
151
153
  // Determine namespace (explicit ns > ns:key > scope ns > default)
152
154
  // See https://www.i18next.com/overview/api#getfixedt
153
155
  if (options) {
154
- const nsVal = getObjectPropValue(options, 'ns');
155
- if (typeof nsVal === 'string')
156
+ const nsVal = getObjectPropValue(options, 'ns', this.resolveIdentifier);
157
+ if (typeof nsVal === 'string' && nsVal !== '')
156
158
  ns = nsVal;
157
159
  }
158
160
  const nsSeparator = this.config.extract.nsSeparator ?? ':';
@@ -7,6 +7,18 @@ class ExpressionResolver {
7
7
  variableTable = new Map();
8
8
  // Shared (cross-file) table for enums / exported object maps that should persist
9
9
  sharedEnumTable = new Map();
10
+ // Per-file table for type aliases: Maps typeName -> string[]
11
+ // e.g. `type ChangeType = 'all' | 'next' | 'this'` -> { ChangeType: ['all', 'next', 'this'] }
12
+ typeAliasTable = new Map();
13
+ // Shared (cross-file) table for string-array constants (e.g. `as const` arrays).
14
+ // Persists across resetFileSymbols() so exported arrays are visible to importers.
15
+ sharedVariableTable = new Map();
16
+ // Shared (cross-file) table for type aliases — populated alongside typeAliasTable.
17
+ // Persists across resetFileSymbols() so exported type aliases are visible to importers.
18
+ sharedTypeAliasTable = new Map();
19
+ // Temporary per-scope variable overrides, used to inject .map() / .forEach()
20
+ // callback parameters while the callback body is being walked.
21
+ temporaryVariables = new Map();
10
22
  constructor(hooks) {
11
23
  this.hooks = hooks;
12
24
  }
@@ -15,6 +27,8 @@ class ExpressionResolver {
15
27
  */
16
28
  resetFileSymbols() {
17
29
  this.variableTable.clear();
30
+ this.typeAliasTable.clear();
31
+ this.temporaryVariables.clear();
18
32
  }
19
33
  /**
20
34
  * Capture a VariableDeclarator node to record simple statically analyzable
@@ -28,17 +42,74 @@ class ExpressionResolver {
28
42
  */
29
43
  captureVariableDeclarator(node) {
30
44
  try {
31
- if (!node || !node.id || !node.init)
45
+ if (!node || !node.id)
32
46
  return;
47
+ // ── ArrayPattern id: `const [x, y] = fn<T>(...)` ────────────────────────
48
+ // Handles `const [state] = useState<'a'|'b'>('a')` or similar generic calls
49
+ // where the type argument is a finite string-literal union.
50
+ if (node.id.type === 'ArrayPattern' && node.init) {
51
+ const init = node.init;
52
+ // Unwrap await / as-expressions
53
+ let callExpr = init;
54
+ while (callExpr?.type === 'AwaitExpression')
55
+ callExpr = callExpr.argument;
56
+ while (callExpr?.type === 'TsConstAssertion' ||
57
+ callExpr?.type === 'TsAsExpression' ||
58
+ callExpr?.type === 'TsSatisfiesExpression')
59
+ callExpr = callExpr.expression;
60
+ if (callExpr?.type === 'CallExpression') {
61
+ const typeArgs = callExpr.typeArguments?.params ??
62
+ callExpr.typeParameters?.params ??
63
+ [];
64
+ if (typeArgs.length > 0) {
65
+ const vals = this.resolvePossibleStringValuesFromType(typeArgs[0]);
66
+ if (vals.length > 0) {
67
+ // Bind each array-pattern element: first element is the state variable
68
+ for (const el of node.id.elements) {
69
+ if (!el)
70
+ continue;
71
+ const ident = el.type === 'Identifier' ? el : (el.type === 'AssignmentPattern' && el.left?.type === 'Identifier' ? el.left : null);
72
+ if (ident) {
73
+ this.variableTable.set(ident.value, vals);
74
+ }
75
+ break; // only bind the first element (the state value, not the setter)
76
+ }
77
+ }
78
+ }
79
+ }
80
+ return;
81
+ }
33
82
  // only handle simple identifier bindings like `const x = ...`
34
83
  if (node.id.type !== 'Identifier')
35
84
  return;
36
85
  const name = node.id.value;
86
+ // pattern 1:
87
+ // Handle `declare const x: 'a' | 'b'` and `declare const x: SomeUnion`
88
+ // where there is no initializer but a TypeScript type annotation.
89
+ if (!node.init) {
90
+ const typeAnnotation = this.extractTypeAnnotation(node.id);
91
+ if (typeAnnotation) {
92
+ const vals = this.resolvePossibleStringValuesFromType(typeAnnotation);
93
+ if (vals.length > 0) {
94
+ this.variableTable.set(name, vals);
95
+ }
96
+ }
97
+ return;
98
+ }
37
99
  const init = node.init;
100
+ // Unwrap TS type assertion wrappers before inspecting the shape of the initializer.
101
+ // `{ ... } as const` → TsConstAssertion; `x as Type` → TsAsExpression; etc.
102
+ // We need the raw expression to detect ObjectExpression and ArrowFunctionExpression.
103
+ let unwrappedInit = init;
104
+ while (unwrappedInit?.type === 'TsConstAssertion' ||
105
+ unwrappedInit?.type === 'TsAsExpression' ||
106
+ unwrappedInit?.type === 'TsSatisfiesExpression') {
107
+ unwrappedInit = unwrappedInit.expression;
108
+ }
38
109
  // ObjectExpression -> map of string props
39
- if (init.type === 'ObjectExpression' && Array.isArray(init.properties)) {
110
+ if (unwrappedInit.type === 'ObjectExpression' && Array.isArray(unwrappedInit.properties)) {
40
111
  const map = {};
41
- for (const p of init.properties) {
112
+ for (const p of unwrappedInit.properties) {
42
113
  if (!p || p.type !== 'KeyValueProperty')
43
114
  continue;
44
115
  const keyNode = p.key;
@@ -58,16 +129,147 @@ class ExpressionResolver {
58
129
  return;
59
130
  }
60
131
  }
132
+ // ArrayExpression -> list of string values
133
+ // Handles `const OPTS = ['a', 'b', 'c'] as const`
134
+ if (unwrappedInit.type === 'ArrayExpression' && Array.isArray(unwrappedInit.elements)) {
135
+ const vals = [];
136
+ for (const elem of unwrappedInit.elements) {
137
+ if (!elem || !elem.expression)
138
+ continue;
139
+ const resolved = this.resolvePossibleStringValuesFromExpression(elem.expression);
140
+ if (resolved.length === 1)
141
+ vals.push(resolved[0]);
142
+ }
143
+ if (vals.length > 0) {
144
+ this.variableTable.set(name, vals);
145
+ // Also share so importing files can see this array
146
+ this.sharedVariableTable.set(name, vals);
147
+ return;
148
+ }
149
+ }
61
150
  // For other initializers, try to resolve to one-or-more strings
62
151
  const vals = this.resolvePossibleStringValuesFromExpression(init);
63
152
  if (vals.length > 0) {
64
153
  this.variableTable.set(name, vals);
154
+ return;
155
+ }
156
+ // pattern 3 (arrow function variant):
157
+ // `const fn = (): 'a' | 'b' => ...` — capture the explicit return type annotation.
158
+ if (unwrappedInit.type === 'ArrowFunctionExpression' || unwrappedInit.type === 'FunctionExpression') {
159
+ const rawReturnType = unwrappedInit.returnType ?? unwrappedInit.typeAnnotation;
160
+ if (rawReturnType) {
161
+ const tsType = rawReturnType.typeAnnotation ?? rawReturnType;
162
+ const returnVals = this.resolvePossibleStringValuesFromType(tsType);
163
+ if (returnVals.length > 0) {
164
+ this.variableTable.set(name, returnVals);
165
+ }
166
+ }
65
167
  }
66
168
  }
67
169
  catch {
68
170
  // be silent - conservative only
69
171
  }
70
172
  }
173
+ /**
174
+ * Capture a TypeScript type alias so that `declare const x: AliasName` can
175
+ * be resolved to its string union members later.
176
+ *
177
+ * Handles: `type Foo = 'a' | 'b' | 'c'`
178
+ *
179
+ * SWC node shapes: `TsTypeAliasDeclaration` / `TsTypeAliasDecl`
180
+ */
181
+ captureTypeAliasDeclaration(node) {
182
+ try {
183
+ const name = node?.id?.type === 'Identifier' ? node.id.value : undefined;
184
+ if (!name)
185
+ return;
186
+ // SWC puts the actual type in `.typeAnnotation`
187
+ const tsType = node.typeAnnotation ?? node.typeAnn;
188
+ if (!tsType)
189
+ return;
190
+ const vals = this.resolvePossibleStringValuesFromType(tsType);
191
+ if (vals.length > 0) {
192
+ this.typeAliasTable.set(name, vals);
193
+ // Also share so importing files can resolve this alias by name
194
+ this.sharedTypeAliasTable.set(name, vals);
195
+ }
196
+ }
197
+ catch {
198
+ // noop
199
+ }
200
+ }
201
+ /**
202
+ * Capture the return-type annotation of a function declaration so that
203
+ * `t(fn())` calls can be expanded to all union members.
204
+ *
205
+ * Handles both `function f(): 'a' | 'b' { ... }` and
206
+ * `const f = (): 'a' | 'b' => ...` (the arrow-function form is captured
207
+ * via captureVariableDeclarator when the init is an ArrowFunctionExpression).
208
+ *
209
+ * SWC node shapes: `FunctionDeclaration` / `FnDecl`
210
+ */
211
+ captureFunctionDeclaration(node) {
212
+ try {
213
+ const name = node?.identifier?.value ?? node?.id?.value;
214
+ if (!name)
215
+ return;
216
+ // SWC places the return type annotation in `.function.returnType` (FunctionDeclaration)
217
+ // or directly in `.returnType` (FunctionExpression / ArrowFunctionExpression).
218
+ const fn = node.function ?? node;
219
+ const rawReturnType = fn.returnType ?? fn.typeAnnotation;
220
+ if (!rawReturnType)
221
+ return;
222
+ // Unwrap TsTypeAnnotation wrapper if present
223
+ const tsType = rawReturnType.typeAnnotation ?? rawReturnType;
224
+ const vals = this.resolvePossibleStringValuesFromType(tsType);
225
+ if (vals.length > 0) {
226
+ this.variableTable.set(name, vals);
227
+ }
228
+ }
229
+ catch {
230
+ // noop
231
+ }
232
+ }
233
+ /**
234
+ * Extract a raw TsType node from an identifier's type annotation.
235
+ * SWC may wrap it in a `TsTypeAnnotation` node — this unwraps it.
236
+ */
237
+ extractTypeAnnotation(idNode) {
238
+ const raw = idNode?.typeAnnotation;
239
+ if (!raw)
240
+ return undefined;
241
+ // TsTypeAnnotation wrapper -> .typeAnnotation holds the actual TsType
242
+ if (raw.type === 'TsTypeAnnotation')
243
+ return raw.typeAnnotation;
244
+ return raw;
245
+ }
246
+ /**
247
+ * Temporarily bind a variable name to a set of string values.
248
+ * Used by ast-visitors to inject .map()/.forEach() callback parameters.
249
+ * Call deleteTemporaryVariable() after walking the callback body.
250
+ */
251
+ setTemporaryVariable(name, values) {
252
+ this.temporaryVariables.set(name, values);
253
+ }
254
+ /**
255
+ * Remove a previously-injected temporary variable binding.
256
+ */
257
+ deleteTemporaryVariable(name) {
258
+ this.temporaryVariables.delete(name);
259
+ }
260
+ /**
261
+ * Return the array values stored for a variable name, checking all tables.
262
+ * Returns undefined if the name is not a known string array.
263
+ */
264
+ getVariableValues(name) {
265
+ const tmp = this.temporaryVariables.get(name);
266
+ if (tmp)
267
+ return tmp;
268
+ const v = this.variableTable.get(name);
269
+ if (Array.isArray(v))
270
+ return v;
271
+ return this.sharedVariableTable.get(name);
272
+ }
71
273
  /**
72
274
  * Capture a TypeScript enum declaration so members can be resolved later.
73
275
  * Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
@@ -218,11 +420,36 @@ class ExpressionResolver {
218
420
  if (propName && base[propName] !== undefined) {
219
421
  return [base[propName]];
220
422
  }
423
+ // pattern 4:
424
+ // `map[identifierVar]` where identifierVar resolves to a known set of keys.
425
+ // Try to enumerate which map values are reachable.
426
+ if (prop.type === 'Computed' && prop.expression) {
427
+ const keyVals = this.resolvePossibleStringValuesFromExpression(prop.expression, returnEmptyStrings);
428
+ if (keyVals.length > 0) {
429
+ // Return only the map values for the known keys (subset access)
430
+ return keyVals.map(k => base[k]).filter((v) => v !== undefined);
431
+ }
432
+ // Cannot narrow the key at all — return all map values as a conservative fallback
433
+ return Object.values(base);
434
+ }
221
435
  }
222
436
  }
223
437
  }
224
438
  catch { }
225
439
  }
440
+ // pattern 3:
441
+ // `t(fn())` — resolve to the function's known return-type union when captured.
442
+ if (expression.type === 'CallExpression') {
443
+ try {
444
+ const callee = expression.callee;
445
+ if (callee?.type === 'Identifier') {
446
+ const v = this.variableTable.get(callee.value);
447
+ if (Array.isArray(v) && v.length > 0)
448
+ return v;
449
+ }
450
+ }
451
+ catch { }
452
+ }
226
453
  // Binary concatenation support (e.g., a + '_' + b)
227
454
  // SWC binary expr can be represented as `BinExpr` with left/right; be permissive:
228
455
  if (expression.left && expression.right) {
@@ -277,11 +504,25 @@ class ExpressionResolver {
277
504
  const annotation = expression.typeAnnotation;
278
505
  return this.resolvePossibleStringValuesFromType(annotation, returnEmptyStrings);
279
506
  }
507
+ // `expr as const` — delegate to the underlying expression (the type annotation is
508
+ // just `const`, which carries no union information, so we want the value side).
509
+ if (expression.type === 'TsConstAssertion') {
510
+ return this.resolvePossibleStringValuesFromExpression(expression.expression, returnEmptyStrings);
511
+ }
280
512
  // Identifier resolution via captured per-file variable table only
281
513
  if (expression.type === 'Identifier') {
514
+ // Check temporary (callback param) overrides first
515
+ const tmp = this.temporaryVariables.get(expression.value);
516
+ if (tmp)
517
+ return tmp;
282
518
  const v = this.variableTable.get(expression.value);
283
- if (!v)
519
+ if (!v) {
520
+ // Fall back to shared cross-file array table
521
+ const sv = this.sharedVariableTable.get(expression.value);
522
+ if (sv)
523
+ return sv;
284
524
  return [];
525
+ }
285
526
  if (Array.isArray(v))
286
527
  return v;
287
528
  // object map - cannot be used directly as key, so return empty
@@ -291,6 +532,11 @@ class ExpressionResolver {
291
532
  return [];
292
533
  }
293
534
  resolvePossibleStringValuesFromType(type, returnEmptyStrings = false) {
535
+ // Unwrap TsParenthesizedType — SWC explicitly emits these for grouped types like
536
+ // `(typeof X)[number]` where `(typeof X)` becomes TsParenthesizedType { typeAnnotation: TsTypeQuery }
537
+ if (type.type === 'TsParenthesizedType') {
538
+ return this.resolvePossibleStringValuesFromType(type.typeAnnotation, returnEmptyStrings);
539
+ }
294
540
  if (type.type === 'TsUnionType') {
295
541
  return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings));
296
542
  }
@@ -306,6 +552,46 @@ class ExpressionResolver {
306
552
  return [`${type.literal.value}`]; // Handle literals like 5 or true
307
553
  }
308
554
  }
555
+ // pattern 2:
556
+ // Resolve a named type alias reference: `declare const x: ChangeType`
557
+ // where `type ChangeType = 'all' | 'next' | 'this'` was captured earlier.
558
+ if (type.type === 'TsTypeReference') {
559
+ const typeName = type.typeName?.type === 'Identifier'
560
+ ? type.typeName.value
561
+ : undefined;
562
+ if (typeName) {
563
+ const aliasVals = this.typeAliasTable.get(typeName) ?? this.sharedTypeAliasTable.get(typeName);
564
+ if (aliasVals && aliasVals.length > 0)
565
+ return aliasVals;
566
+ }
567
+ }
568
+ // `(typeof ACCESS_OPTIONS)[number]` — resolve through the shared array variable table.
569
+ // SWC emits: TsIndexedAccessType {
570
+ // objectType: TsParenthesizedType { typeAnnotation: TsTypeQuery { exprName: Identifier } }
571
+ // indexType: TsKeywordType
572
+ // }
573
+ // The parens around `typeof X` produce a TsParenthesizedType wrapper that we must unwrap.
574
+ if (type.type === 'TsIndexedAccessType') {
575
+ try {
576
+ let objType = type.objectType;
577
+ // Unwrap TsParenthesizedType wrapper (SWC preserves explicit parens in type positions)
578
+ while (objType?.type === 'TsParenthesizedType') {
579
+ objType = objType.typeAnnotation;
580
+ }
581
+ if (objType?.type === 'TsTypeQuery' || objType?.type === 'TSTypeQuery') {
582
+ // SWC: TsTypeQuery.exprName is TsEntityName (Identifier | TsQualifiedName)
583
+ const exprName = objType.exprName ?? objType.expr ?? objType.entityName;
584
+ // access .value (Identifier) or fall back to .name for alternate SWC builds
585
+ const varName = exprName?.value ?? exprName?.name;
586
+ if (varName) {
587
+ const vals = this.getVariableValues(varName);
588
+ if (vals && vals.length > 0)
589
+ return vals;
590
+ }
591
+ }
592
+ }
593
+ catch { }
594
+ }
309
595
  // We can't statically determine the value of other expressions (e.g., variables, function calls)
310
596
  return [];
311
597
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.48.1",
3
+ "version": "1.49.1",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,6 +65,12 @@ export declare class ASTVisitors {
65
65
  * @private
66
66
  */
67
67
  private walk;
68
+ /**
69
+ * If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
70
+ * string-array constant, returns the callback's first parameter name and the
71
+ * array values so the caller can inject a temporary variable binding.
72
+ */
73
+ private tryGetArrayIterationCallbackInfo;
68
74
  /**
69
75
  * Retrieves variable information from the scope chain.
70
76
  * Searches from innermost to outermost scope.