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 +1 -1
- package/dist/cjs/extractor/core/ast-visitors.js +165 -13
- package/dist/cjs/extractor/parsers/call-expression-handler.js +5 -3
- package/dist/cjs/extractor/parsers/expression-resolver.js +290 -4
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/ast-visitors.js +165 -13
- package/dist/esm/extractor/parsers/call-expression-handler.js +5 -3
- package/dist/esm/extractor/parsers/expression-resolver.js +290 -4
- package/package.json +1 -1
- package/types/extractor/core/ast-visitors.d.ts +6 -0
- package/types/extractor/core/ast-visitors.d.ts.map +1 -1
- package/types/extractor/parsers/call-expression-handler.d.ts +2 -1
- package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
- package/types/extractor/parsers/expression-resolver.d.ts +44 -0
- package/types/extractor/parsers/expression-resolver.d.ts.map +1 -1
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.
|
|
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
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
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
|
|
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
|
|
315
|
-
if (item
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
112
|
+
if (unwrappedInit.type === 'ObjectExpression' && Array.isArray(unwrappedInit.properties)) {
|
|
42
113
|
const map = {};
|
|
43
|
-
for (const p of
|
|
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.
|
|
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
|