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