i18next-cli 1.70.0 → 1.70.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/ast-visitors.js +51 -8
- package/dist/cjs/extractor/parsers/comment-parser.js +3 -1
- package/dist/cjs/extractor/parsers/scope-manager.js +74 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/ast-visitors.js +51 -8
- package/dist/esm/extractor/parsers/comment-parser.js +3 -1
- package/dist/esm/extractor/parsers/scope-manager.js +74 -1
- package/package.json +1 -1
- package/types/extractor/core/ast-visitors.d.ts +2 -0
- package/types/extractor/core/ast-visitors.d.ts.map +1 -1
- package/types/extractor/parsers/scope-manager.d.ts +7 -0
- package/types/extractor/parsers/scope-manager.d.ts.map +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -37,7 +37,7 @@ const program = new commander.Command();
|
|
|
37
37
|
program
|
|
38
38
|
.name('i18next-cli')
|
|
39
39
|
.description('A unified, high-performance i18next CLI.')
|
|
40
|
-
.version('1.70.
|
|
40
|
+
.version('1.70.2'); // This string is replaced with the actual version at build time by rollup
|
|
41
41
|
// new: global config override option
|
|
42
42
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
43
43
|
program
|
|
@@ -218,6 +218,13 @@ class ASTVisitors {
|
|
|
218
218
|
node.type === 'ObjectMethod') {
|
|
219
219
|
this.scopeManager.enterScope();
|
|
220
220
|
isNewScope = true;
|
|
221
|
+
// `<Ns extends 'a' | 'b'>(t: TFunction<Ns>)` → constraint lookup for type params
|
|
222
|
+
const typeParamConstraints = {};
|
|
223
|
+
for (const tp of (node.typeParameters?.parameters ?? node.typeParameters?.params ?? [])) {
|
|
224
|
+
const name = tp?.name?.value ?? tp?.name?.name;
|
|
225
|
+
if (name && tp.constraint)
|
|
226
|
+
typeParamConstraints[name] = tp.constraint;
|
|
227
|
+
}
|
|
221
228
|
const params = (node.params && Array.isArray(node.params)) ? node.params : (node.params || []);
|
|
222
229
|
for (const p of params) {
|
|
223
230
|
// handle common param shapes: Identifier, AssignmentPattern (default), RestElement ignored
|
|
@@ -241,7 +248,7 @@ class ASTVisitors {
|
|
|
241
248
|
if (!memberName || !localName)
|
|
242
249
|
continue;
|
|
243
250
|
// `({ t }: { t: TFunction<'ns'> })` → bind `t` to that namespace
|
|
244
|
-
this.bindTFunctionParam(localName, this.getMemberTypeNode(patType, memberName));
|
|
251
|
+
this.bindTFunctionParam(localName, this.getMemberTypeNode(patType, memberName), typeParamConstraints);
|
|
245
252
|
if (!members?.[memberName])
|
|
246
253
|
continue;
|
|
247
254
|
this.expressionResolver.setTemporaryVariable(localName, members[memberName]);
|
|
@@ -294,7 +301,11 @@ class ASTVisitors {
|
|
|
294
301
|
else {
|
|
295
302
|
typeAnn = undefined;
|
|
296
303
|
}
|
|
297
|
-
this.bindTFunctionParam(paramKey, typeAnn);
|
|
304
|
+
this.bindTFunctionParam(paramKey, typeAnn, typeParamConstraints);
|
|
305
|
+
// `props: { t: TFunction<'ns'> }` → bind `props.t` so `props.t('key')` resolves
|
|
306
|
+
for (const { name, typeNode } of this.getObjectTypeMembers(typeAnn)) {
|
|
307
|
+
this.bindTFunctionParam(`${paramKey}.${name}`, typeNode, typeParamConstraints);
|
|
308
|
+
}
|
|
298
309
|
// Capture parameter type annotations as temporary variables in the
|
|
299
310
|
// expression resolver so that dynamic bracket expressions like
|
|
300
311
|
// `t(($) => $.table.columns[field])` can resolve `field` to its
|
|
@@ -560,7 +571,7 @@ class ASTVisitors {
|
|
|
560
571
|
* If `typeAnn` is a `TFunction<Ns, KPrefix>` type reference, register
|
|
561
572
|
* `paramKey` in the current scope with that namespace / keyPrefix.
|
|
562
573
|
*/
|
|
563
|
-
bindTFunctionParam(paramKey, typeAnn) {
|
|
574
|
+
bindTFunctionParam(paramKey, typeAnn, typeParamConstraints = {}) {
|
|
564
575
|
// Small helpers to robustly extract the referenced type name and literal string
|
|
565
576
|
const extractTypeName = (ta) => {
|
|
566
577
|
if (!ta)
|
|
@@ -584,6 +595,16 @@ class ASTVisitors {
|
|
|
584
595
|
const extractStringLiteralValue = (node) => {
|
|
585
596
|
if (!node)
|
|
586
597
|
return undefined;
|
|
598
|
+
// Union (`'a' | 'b'`), local alias, or generic constrained to those: use the
|
|
599
|
+
// first member. Mirrors i18next's type-level behaviour (first ns wins).
|
|
600
|
+
if (node.type === 'TsUnionType')
|
|
601
|
+
return extractStringLiteralValue(node.types?.[0]);
|
|
602
|
+
if (node.type === 'TsTypeReference' && node.typeName?.type === 'Identifier') {
|
|
603
|
+
const constraint = typeParamConstraints[node.typeName.value];
|
|
604
|
+
if (constraint)
|
|
605
|
+
return extractStringLiteralValue(constraint);
|
|
606
|
+
return this.expressionResolver.resolveTypeToStringValues(node)[0];
|
|
607
|
+
}
|
|
587
608
|
// Handle: typeof SomeConst → TsTypeQuery { exprName: { value: 'SomeConst' } }
|
|
588
609
|
if (node?.type === 'TsTypeQuery') {
|
|
589
610
|
const name = node.exprName?.value ?? node.exprName?.name;
|
|
@@ -611,6 +632,23 @@ class ASTVisitors {
|
|
|
611
632
|
return extractStringLiteralValue(node.typeParams[0]);
|
|
612
633
|
return undefined;
|
|
613
634
|
};
|
|
635
|
+
// `ReturnType<typeof useTranslation<'ns', 'kp'>>['t']` is `TFunction<'ns', 'kp'>` spelled indirectly.
|
|
636
|
+
if (typeAnn?.type === 'TsIndexedAccessType' && extractStringLiteralValue(typeAnn.indexType) === 't') {
|
|
637
|
+
const obj = typeAnn.objectType;
|
|
638
|
+
const query = obj?.type === 'TsTypeReference' && extractTypeName(obj) === 'ReturnType'
|
|
639
|
+
? (obj.typeParams?.params?.[0] ?? obj.typeArguments?.params?.[0])
|
|
640
|
+
: undefined;
|
|
641
|
+
const hookName = query?.type === 'TsTypeQuery' ? (query.exprName?.value ?? query.exprName?.name) : undefined;
|
|
642
|
+
const hookNames = this.config.extract.useTranslationNames || ['useTranslation', 'getT', 'useT'];
|
|
643
|
+
if (hookName && hookNames.some(h => (typeof h === 'string' ? h : h.name) === hookName)) {
|
|
644
|
+
const params = query.typeArguments?.params ?? query.typeArgs?.params ?? [];
|
|
645
|
+
const ns = extractStringLiteralValue(params[0]);
|
|
646
|
+
const kp = extractStringLiteralValue(params[1]);
|
|
647
|
+
if (ns || kp)
|
|
648
|
+
this.scopeManager.setVarInScope(paramKey, { defaultNs: ns, keyPrefix: kp });
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
614
652
|
// Detect TsTypeReference like: TFunction<"my-custom-namespace">
|
|
615
653
|
if (typeAnn && (typeAnn.type === 'TsTypeReference' || typeAnn.type === 'TsTypeRef' || typeAnn.type === 'TsTypeReference')) {
|
|
616
654
|
const finalTypeName = extractTypeName(typeAnn);
|
|
@@ -665,8 +703,12 @@ class ASTVisitors {
|
|
|
665
703
|
* alias name).
|
|
666
704
|
*/
|
|
667
705
|
getMemberTypeNode(tsType, memberName) {
|
|
706
|
+
return this.getObjectTypeMembers(tsType).find(m => m.name === memberName)?.typeNode;
|
|
707
|
+
}
|
|
708
|
+
/** `{ name, typeNode }` for every property of an object-shaped type annotation. */
|
|
709
|
+
getObjectTypeMembers(tsType) {
|
|
668
710
|
if (!tsType)
|
|
669
|
-
return
|
|
711
|
+
return [];
|
|
670
712
|
let members;
|
|
671
713
|
if (tsType.type === 'TsTypeLiteral')
|
|
672
714
|
members = tsType.members;
|
|
@@ -674,15 +716,16 @@ class ASTVisitors {
|
|
|
674
716
|
members = this.expressionResolver.getObjectTypeMembersRaw(tsType.typeName.value);
|
|
675
717
|
}
|
|
676
718
|
if (!Array.isArray(members))
|
|
677
|
-
return
|
|
719
|
+
return [];
|
|
720
|
+
const out = [];
|
|
678
721
|
for (const m of members) {
|
|
679
722
|
if (m?.type !== 'TsPropertySignature')
|
|
680
723
|
continue;
|
|
681
724
|
const name = m.key?.type === 'Identifier' || m.key?.type === 'StringLiteral' ? m.key.value : undefined;
|
|
682
|
-
if (name
|
|
683
|
-
|
|
725
|
+
if (name)
|
|
726
|
+
out.push({ name, typeNode: m.typeAnnotation?.typeAnnotation ?? m.typeAnnotation });
|
|
684
727
|
}
|
|
685
|
-
return
|
|
728
|
+
return out;
|
|
686
729
|
}
|
|
687
730
|
/**
|
|
688
731
|
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
@@ -312,7 +312,9 @@ function collectCommentTexts(src) {
|
|
|
312
312
|
const commentRegex = /\/\/(.*)|\/\*([\s\S]*?)\*\//g;
|
|
313
313
|
let cmatch;
|
|
314
314
|
while ((cmatch = commentRegex.exec(src)) !== null) {
|
|
315
|
-
|
|
315
|
+
// Strip fenced code blocks (```…```) from doc comments: they are usage
|
|
316
|
+
// examples, not real call sites, and would otherwise pollute the default namespace.
|
|
317
|
+
const content = (cmatch[1] ?? cmatch[2]).replace(/```[\s\S]*?```/g, '');
|
|
316
318
|
const s = content.trim();
|
|
317
319
|
if (s && !seen.has(s)) {
|
|
318
320
|
seen.add(s);
|
|
@@ -15,6 +15,8 @@ class ScopeManager {
|
|
|
15
15
|
simpleConstants = new Map();
|
|
16
16
|
// Track simple local constant objects with string literal property values
|
|
17
17
|
simpleConstantObjects = new Map();
|
|
18
|
+
// `const NS = ['a', 'b']` → used by resolveNsArg for `useTranslation(NS)`.
|
|
19
|
+
simpleConstantArrays = new Map();
|
|
18
20
|
// Shared (cross-file) tables so that exported constants from one file can be
|
|
19
21
|
// resolved when imported in another. These are NOT cleared by reset().
|
|
20
22
|
sharedConstants = new Map();
|
|
@@ -68,6 +70,7 @@ class ScopeManager {
|
|
|
68
70
|
this.scope = new Map();
|
|
69
71
|
this.simpleConstants.clear();
|
|
70
72
|
this.simpleConstantObjects.clear();
|
|
73
|
+
this.simpleConstantArrays.clear();
|
|
71
74
|
this.thisFieldStack = [];
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
@@ -258,6 +261,15 @@ class ScopeManager {
|
|
|
258
261
|
this.sharedConstantObjects.set(node.id.value, map);
|
|
259
262
|
}
|
|
260
263
|
}
|
|
264
|
+
else if (unwrapped?.type === 'ArrayExpression') {
|
|
265
|
+
const arr = unwrapped.elements
|
|
266
|
+
.map((el) => ScopeManager.unwrapTsExpression(el?.expression))
|
|
267
|
+
.filter((e) => e?.type === 'StringLiteral')
|
|
268
|
+
.map((e) => e.value);
|
|
269
|
+
if (arr.length > 0 && arr.length === unwrapped.elements.length) {
|
|
270
|
+
this.simpleConstantArrays.set(node.id.value, arr);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
261
273
|
else {
|
|
262
274
|
const fromType = ScopeManager.extractStringFromTypeAnnotation(node);
|
|
263
275
|
if (fromType !== undefined) {
|
|
@@ -267,6 +279,17 @@ class ScopeManager {
|
|
|
267
279
|
}
|
|
268
280
|
// continue processing; still may be a useTranslation/getFixedT call below
|
|
269
281
|
}
|
|
282
|
+
// Handle: const t = useCallback((key, opts) => tLocal(key, opts), [...]) OR
|
|
283
|
+
// const t = (key, opts) => tLocal(key, opts)
|
|
284
|
+
// A wrapper that forwards its first parameter as the key to an already-scoped
|
|
285
|
+
// translation function inherits that function's scope.
|
|
286
|
+
if (node.id.type === 'Identifier') {
|
|
287
|
+
const wrapped = this.resolveDelegatingWrapperScope(init);
|
|
288
|
+
if (wrapped) {
|
|
289
|
+
this.setVarInScope(node.id.value, wrapped);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
270
293
|
// Handle: const { t } = this.#field OR const { t } = this.#field()
|
|
271
294
|
// Resolve the source `this.<field>` to its previously-registered ScopeInfo
|
|
272
295
|
// and propagate it onto the destructured variables.
|
|
@@ -544,10 +567,15 @@ class ScopeManager {
|
|
|
544
567
|
resolveNsArg(nsNode) {
|
|
545
568
|
if (!nsNode)
|
|
546
569
|
return {};
|
|
570
|
+
nsNode = ScopeManager.unwrapTsExpression(nsNode); // `[...] as const`, `satisfies`, `as X`
|
|
547
571
|
if (nsNode.type === 'StringLiteral')
|
|
548
572
|
return { defaultNs: nsNode.value };
|
|
549
|
-
if (nsNode.type === 'Identifier')
|
|
573
|
+
if (nsNode.type === 'Identifier') {
|
|
574
|
+
const arr = this.simpleConstantArrays.get(nsNode.value);
|
|
575
|
+
if (arr)
|
|
576
|
+
return { defaultNs: arr[0], namespaces: arr };
|
|
550
577
|
return { defaultNs: this.resolveSimpleStringIdentifier(nsNode.value) };
|
|
578
|
+
}
|
|
551
579
|
if (nsNode.type === 'MemberExpression')
|
|
552
580
|
return { defaultNs: this.resolveSimpleMemberExpression(nsNode) };
|
|
553
581
|
// `useTranslation(ns ?? 'fallback')` / `useTranslation(ns || 'fallback')`:
|
|
@@ -579,6 +607,51 @@ class ScopeManager {
|
|
|
579
607
|
}
|
|
580
608
|
return {};
|
|
581
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* If `init` is a function (optionally wrapped in `useCallback(fn, deps)`) whose
|
|
612
|
+
* first parameter is passed as the first argument to a call of an already-scoped
|
|
613
|
+
* variable (e.g. `tLocal(key, opts)`), return that variable's ScopeInfo.
|
|
614
|
+
*/
|
|
615
|
+
resolveDelegatingWrapperScope(init) {
|
|
616
|
+
let fn = ScopeManager.unwrapTsExpression(init);
|
|
617
|
+
if (fn?.type === 'CallExpression' && fn.callee?.type === 'Identifier' && fn.callee.value === 'useCallback') {
|
|
618
|
+
fn = fn.arguments?.[0]?.expression;
|
|
619
|
+
}
|
|
620
|
+
if (fn?.type !== 'ArrowFunctionExpression' && fn?.type !== 'FunctionExpression')
|
|
621
|
+
return undefined;
|
|
622
|
+
const first = fn.params?.[0];
|
|
623
|
+
const pat = first?.pat ?? first;
|
|
624
|
+
const keyParam = pat?.type === 'Identifier' ? pat.value : undefined;
|
|
625
|
+
if (!keyParam)
|
|
626
|
+
return undefined;
|
|
627
|
+
let found;
|
|
628
|
+
const visit = (n) => {
|
|
629
|
+
if (found || !n || typeof n !== 'object')
|
|
630
|
+
return;
|
|
631
|
+
if (Array.isArray(n)) {
|
|
632
|
+
for (const c of n)
|
|
633
|
+
visit(c);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (n.type === 'CallExpression' &&
|
|
637
|
+
n.callee?.type === 'Identifier' &&
|
|
638
|
+
n.arguments?.[0]?.expression?.type === 'Identifier' &&
|
|
639
|
+
n.arguments[0].expression.value === keyParam) {
|
|
640
|
+
const scope = this.getVarFromScope(n.callee.value);
|
|
641
|
+
if (scope) {
|
|
642
|
+
found = scope;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
for (const k of Object.keys(n)) {
|
|
647
|
+
if (k === 'span')
|
|
648
|
+
continue;
|
|
649
|
+
visit(n[k]);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
visit(fn.body);
|
|
653
|
+
return found;
|
|
654
|
+
}
|
|
582
655
|
resolveStringArg(node) {
|
|
583
656
|
if (!node)
|
|
584
657
|
return undefined;
|
package/dist/esm/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ const program = new Command();
|
|
|
31
31
|
program
|
|
32
32
|
.name('i18next-cli')
|
|
33
33
|
.description('A unified, high-performance i18next CLI.')
|
|
34
|
-
.version('1.70.
|
|
34
|
+
.version('1.70.2'); // 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
|
|
@@ -216,6 +216,13 @@ class ASTVisitors {
|
|
|
216
216
|
node.type === 'ObjectMethod') {
|
|
217
217
|
this.scopeManager.enterScope();
|
|
218
218
|
isNewScope = true;
|
|
219
|
+
// `<Ns extends 'a' | 'b'>(t: TFunction<Ns>)` → constraint lookup for type params
|
|
220
|
+
const typeParamConstraints = {};
|
|
221
|
+
for (const tp of (node.typeParameters?.parameters ?? node.typeParameters?.params ?? [])) {
|
|
222
|
+
const name = tp?.name?.value ?? tp?.name?.name;
|
|
223
|
+
if (name && tp.constraint)
|
|
224
|
+
typeParamConstraints[name] = tp.constraint;
|
|
225
|
+
}
|
|
219
226
|
const params = (node.params && Array.isArray(node.params)) ? node.params : (node.params || []);
|
|
220
227
|
for (const p of params) {
|
|
221
228
|
// handle common param shapes: Identifier, AssignmentPattern (default), RestElement ignored
|
|
@@ -239,7 +246,7 @@ class ASTVisitors {
|
|
|
239
246
|
if (!memberName || !localName)
|
|
240
247
|
continue;
|
|
241
248
|
// `({ t }: { t: TFunction<'ns'> })` → bind `t` to that namespace
|
|
242
|
-
this.bindTFunctionParam(localName, this.getMemberTypeNode(patType, memberName));
|
|
249
|
+
this.bindTFunctionParam(localName, this.getMemberTypeNode(patType, memberName), typeParamConstraints);
|
|
243
250
|
if (!members?.[memberName])
|
|
244
251
|
continue;
|
|
245
252
|
this.expressionResolver.setTemporaryVariable(localName, members[memberName]);
|
|
@@ -292,7 +299,11 @@ class ASTVisitors {
|
|
|
292
299
|
else {
|
|
293
300
|
typeAnn = undefined;
|
|
294
301
|
}
|
|
295
|
-
this.bindTFunctionParam(paramKey, typeAnn);
|
|
302
|
+
this.bindTFunctionParam(paramKey, typeAnn, typeParamConstraints);
|
|
303
|
+
// `props: { t: TFunction<'ns'> }` → bind `props.t` so `props.t('key')` resolves
|
|
304
|
+
for (const { name, typeNode } of this.getObjectTypeMembers(typeAnn)) {
|
|
305
|
+
this.bindTFunctionParam(`${paramKey}.${name}`, typeNode, typeParamConstraints);
|
|
306
|
+
}
|
|
296
307
|
// Capture parameter type annotations as temporary variables in the
|
|
297
308
|
// expression resolver so that dynamic bracket expressions like
|
|
298
309
|
// `t(($) => $.table.columns[field])` can resolve `field` to its
|
|
@@ -558,7 +569,7 @@ class ASTVisitors {
|
|
|
558
569
|
* If `typeAnn` is a `TFunction<Ns, KPrefix>` type reference, register
|
|
559
570
|
* `paramKey` in the current scope with that namespace / keyPrefix.
|
|
560
571
|
*/
|
|
561
|
-
bindTFunctionParam(paramKey, typeAnn) {
|
|
572
|
+
bindTFunctionParam(paramKey, typeAnn, typeParamConstraints = {}) {
|
|
562
573
|
// Small helpers to robustly extract the referenced type name and literal string
|
|
563
574
|
const extractTypeName = (ta) => {
|
|
564
575
|
if (!ta)
|
|
@@ -582,6 +593,16 @@ class ASTVisitors {
|
|
|
582
593
|
const extractStringLiteralValue = (node) => {
|
|
583
594
|
if (!node)
|
|
584
595
|
return undefined;
|
|
596
|
+
// Union (`'a' | 'b'`), local alias, or generic constrained to those: use the
|
|
597
|
+
// first member. Mirrors i18next's type-level behaviour (first ns wins).
|
|
598
|
+
if (node.type === 'TsUnionType')
|
|
599
|
+
return extractStringLiteralValue(node.types?.[0]);
|
|
600
|
+
if (node.type === 'TsTypeReference' && node.typeName?.type === 'Identifier') {
|
|
601
|
+
const constraint = typeParamConstraints[node.typeName.value];
|
|
602
|
+
if (constraint)
|
|
603
|
+
return extractStringLiteralValue(constraint);
|
|
604
|
+
return this.expressionResolver.resolveTypeToStringValues(node)[0];
|
|
605
|
+
}
|
|
585
606
|
// Handle: typeof SomeConst → TsTypeQuery { exprName: { value: 'SomeConst' } }
|
|
586
607
|
if (node?.type === 'TsTypeQuery') {
|
|
587
608
|
const name = node.exprName?.value ?? node.exprName?.name;
|
|
@@ -609,6 +630,23 @@ class ASTVisitors {
|
|
|
609
630
|
return extractStringLiteralValue(node.typeParams[0]);
|
|
610
631
|
return undefined;
|
|
611
632
|
};
|
|
633
|
+
// `ReturnType<typeof useTranslation<'ns', 'kp'>>['t']` is `TFunction<'ns', 'kp'>` spelled indirectly.
|
|
634
|
+
if (typeAnn?.type === 'TsIndexedAccessType' && extractStringLiteralValue(typeAnn.indexType) === 't') {
|
|
635
|
+
const obj = typeAnn.objectType;
|
|
636
|
+
const query = obj?.type === 'TsTypeReference' && extractTypeName(obj) === 'ReturnType'
|
|
637
|
+
? (obj.typeParams?.params?.[0] ?? obj.typeArguments?.params?.[0])
|
|
638
|
+
: undefined;
|
|
639
|
+
const hookName = query?.type === 'TsTypeQuery' ? (query.exprName?.value ?? query.exprName?.name) : undefined;
|
|
640
|
+
const hookNames = this.config.extract.useTranslationNames || ['useTranslation', 'getT', 'useT'];
|
|
641
|
+
if (hookName && hookNames.some(h => (typeof h === 'string' ? h : h.name) === hookName)) {
|
|
642
|
+
const params = query.typeArguments?.params ?? query.typeArgs?.params ?? [];
|
|
643
|
+
const ns = extractStringLiteralValue(params[0]);
|
|
644
|
+
const kp = extractStringLiteralValue(params[1]);
|
|
645
|
+
if (ns || kp)
|
|
646
|
+
this.scopeManager.setVarInScope(paramKey, { defaultNs: ns, keyPrefix: kp });
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
612
650
|
// Detect TsTypeReference like: TFunction<"my-custom-namespace">
|
|
613
651
|
if (typeAnn && (typeAnn.type === 'TsTypeReference' || typeAnn.type === 'TsTypeRef' || typeAnn.type === 'TsTypeReference')) {
|
|
614
652
|
const finalTypeName = extractTypeName(typeAnn);
|
|
@@ -663,8 +701,12 @@ class ASTVisitors {
|
|
|
663
701
|
* alias name).
|
|
664
702
|
*/
|
|
665
703
|
getMemberTypeNode(tsType, memberName) {
|
|
704
|
+
return this.getObjectTypeMembers(tsType).find(m => m.name === memberName)?.typeNode;
|
|
705
|
+
}
|
|
706
|
+
/** `{ name, typeNode }` for every property of an object-shaped type annotation. */
|
|
707
|
+
getObjectTypeMembers(tsType) {
|
|
666
708
|
if (!tsType)
|
|
667
|
-
return
|
|
709
|
+
return [];
|
|
668
710
|
let members;
|
|
669
711
|
if (tsType.type === 'TsTypeLiteral')
|
|
670
712
|
members = tsType.members;
|
|
@@ -672,15 +714,16 @@ class ASTVisitors {
|
|
|
672
714
|
members = this.expressionResolver.getObjectTypeMembersRaw(tsType.typeName.value);
|
|
673
715
|
}
|
|
674
716
|
if (!Array.isArray(members))
|
|
675
|
-
return
|
|
717
|
+
return [];
|
|
718
|
+
const out = [];
|
|
676
719
|
for (const m of members) {
|
|
677
720
|
if (m?.type !== 'TsPropertySignature')
|
|
678
721
|
continue;
|
|
679
722
|
const name = m.key?.type === 'Identifier' || m.key?.type === 'StringLiteral' ? m.key.value : undefined;
|
|
680
|
-
if (name
|
|
681
|
-
|
|
723
|
+
if (name)
|
|
724
|
+
out.push({ name, typeNode: m.typeAnnotation?.typeAnnotation ?? m.typeAnnotation });
|
|
682
725
|
}
|
|
683
|
-
return
|
|
726
|
+
return out;
|
|
684
727
|
}
|
|
685
728
|
/**
|
|
686
729
|
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
@@ -310,7 +310,9 @@ function collectCommentTexts(src) {
|
|
|
310
310
|
const commentRegex = /\/\/(.*)|\/\*([\s\S]*?)\*\//g;
|
|
311
311
|
let cmatch;
|
|
312
312
|
while ((cmatch = commentRegex.exec(src)) !== null) {
|
|
313
|
-
|
|
313
|
+
// Strip fenced code blocks (```…```) from doc comments: they are usage
|
|
314
|
+
// examples, not real call sites, and would otherwise pollute the default namespace.
|
|
315
|
+
const content = (cmatch[1] ?? cmatch[2]).replace(/```[\s\S]*?```/g, '');
|
|
314
316
|
const s = content.trim();
|
|
315
317
|
if (s && !seen.has(s)) {
|
|
316
318
|
seen.add(s);
|
|
@@ -13,6 +13,8 @@ class ScopeManager {
|
|
|
13
13
|
simpleConstants = new Map();
|
|
14
14
|
// Track simple local constant objects with string literal property values
|
|
15
15
|
simpleConstantObjects = new Map();
|
|
16
|
+
// `const NS = ['a', 'b']` → used by resolveNsArg for `useTranslation(NS)`.
|
|
17
|
+
simpleConstantArrays = new Map();
|
|
16
18
|
// Shared (cross-file) tables so that exported constants from one file can be
|
|
17
19
|
// resolved when imported in another. These are NOT cleared by reset().
|
|
18
20
|
sharedConstants = new Map();
|
|
@@ -66,6 +68,7 @@ class ScopeManager {
|
|
|
66
68
|
this.scope = new Map();
|
|
67
69
|
this.simpleConstants.clear();
|
|
68
70
|
this.simpleConstantObjects.clear();
|
|
71
|
+
this.simpleConstantArrays.clear();
|
|
69
72
|
this.thisFieldStack = [];
|
|
70
73
|
}
|
|
71
74
|
/**
|
|
@@ -256,6 +259,15 @@ class ScopeManager {
|
|
|
256
259
|
this.sharedConstantObjects.set(node.id.value, map);
|
|
257
260
|
}
|
|
258
261
|
}
|
|
262
|
+
else if (unwrapped?.type === 'ArrayExpression') {
|
|
263
|
+
const arr = unwrapped.elements
|
|
264
|
+
.map((el) => ScopeManager.unwrapTsExpression(el?.expression))
|
|
265
|
+
.filter((e) => e?.type === 'StringLiteral')
|
|
266
|
+
.map((e) => e.value);
|
|
267
|
+
if (arr.length > 0 && arr.length === unwrapped.elements.length) {
|
|
268
|
+
this.simpleConstantArrays.set(node.id.value, arr);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
259
271
|
else {
|
|
260
272
|
const fromType = ScopeManager.extractStringFromTypeAnnotation(node);
|
|
261
273
|
if (fromType !== undefined) {
|
|
@@ -265,6 +277,17 @@ class ScopeManager {
|
|
|
265
277
|
}
|
|
266
278
|
// continue processing; still may be a useTranslation/getFixedT call below
|
|
267
279
|
}
|
|
280
|
+
// Handle: const t = useCallback((key, opts) => tLocal(key, opts), [...]) OR
|
|
281
|
+
// const t = (key, opts) => tLocal(key, opts)
|
|
282
|
+
// A wrapper that forwards its first parameter as the key to an already-scoped
|
|
283
|
+
// translation function inherits that function's scope.
|
|
284
|
+
if (node.id.type === 'Identifier') {
|
|
285
|
+
const wrapped = this.resolveDelegatingWrapperScope(init);
|
|
286
|
+
if (wrapped) {
|
|
287
|
+
this.setVarInScope(node.id.value, wrapped);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
268
291
|
// Handle: const { t } = this.#field OR const { t } = this.#field()
|
|
269
292
|
// Resolve the source `this.<field>` to its previously-registered ScopeInfo
|
|
270
293
|
// and propagate it onto the destructured variables.
|
|
@@ -542,10 +565,15 @@ class ScopeManager {
|
|
|
542
565
|
resolveNsArg(nsNode) {
|
|
543
566
|
if (!nsNode)
|
|
544
567
|
return {};
|
|
568
|
+
nsNode = ScopeManager.unwrapTsExpression(nsNode); // `[...] as const`, `satisfies`, `as X`
|
|
545
569
|
if (nsNode.type === 'StringLiteral')
|
|
546
570
|
return { defaultNs: nsNode.value };
|
|
547
|
-
if (nsNode.type === 'Identifier')
|
|
571
|
+
if (nsNode.type === 'Identifier') {
|
|
572
|
+
const arr = this.simpleConstantArrays.get(nsNode.value);
|
|
573
|
+
if (arr)
|
|
574
|
+
return { defaultNs: arr[0], namespaces: arr };
|
|
548
575
|
return { defaultNs: this.resolveSimpleStringIdentifier(nsNode.value) };
|
|
576
|
+
}
|
|
549
577
|
if (nsNode.type === 'MemberExpression')
|
|
550
578
|
return { defaultNs: this.resolveSimpleMemberExpression(nsNode) };
|
|
551
579
|
// `useTranslation(ns ?? 'fallback')` / `useTranslation(ns || 'fallback')`:
|
|
@@ -577,6 +605,51 @@ class ScopeManager {
|
|
|
577
605
|
}
|
|
578
606
|
return {};
|
|
579
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* If `init` is a function (optionally wrapped in `useCallback(fn, deps)`) whose
|
|
610
|
+
* first parameter is passed as the first argument to a call of an already-scoped
|
|
611
|
+
* variable (e.g. `tLocal(key, opts)`), return that variable's ScopeInfo.
|
|
612
|
+
*/
|
|
613
|
+
resolveDelegatingWrapperScope(init) {
|
|
614
|
+
let fn = ScopeManager.unwrapTsExpression(init);
|
|
615
|
+
if (fn?.type === 'CallExpression' && fn.callee?.type === 'Identifier' && fn.callee.value === 'useCallback') {
|
|
616
|
+
fn = fn.arguments?.[0]?.expression;
|
|
617
|
+
}
|
|
618
|
+
if (fn?.type !== 'ArrowFunctionExpression' && fn?.type !== 'FunctionExpression')
|
|
619
|
+
return undefined;
|
|
620
|
+
const first = fn.params?.[0];
|
|
621
|
+
const pat = first?.pat ?? first;
|
|
622
|
+
const keyParam = pat?.type === 'Identifier' ? pat.value : undefined;
|
|
623
|
+
if (!keyParam)
|
|
624
|
+
return undefined;
|
|
625
|
+
let found;
|
|
626
|
+
const visit = (n) => {
|
|
627
|
+
if (found || !n || typeof n !== 'object')
|
|
628
|
+
return;
|
|
629
|
+
if (Array.isArray(n)) {
|
|
630
|
+
for (const c of n)
|
|
631
|
+
visit(c);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (n.type === 'CallExpression' &&
|
|
635
|
+
n.callee?.type === 'Identifier' &&
|
|
636
|
+
n.arguments?.[0]?.expression?.type === 'Identifier' &&
|
|
637
|
+
n.arguments[0].expression.value === keyParam) {
|
|
638
|
+
const scope = this.getVarFromScope(n.callee.value);
|
|
639
|
+
if (scope) {
|
|
640
|
+
found = scope;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for (const k of Object.keys(n)) {
|
|
645
|
+
if (k === 'span')
|
|
646
|
+
continue;
|
|
647
|
+
visit(n[k]);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
visit(fn.body);
|
|
651
|
+
return found;
|
|
652
|
+
}
|
|
580
653
|
resolveStringArg(node) {
|
|
581
654
|
if (!node)
|
|
582
655
|
return undefined;
|
package/package.json
CHANGED
|
@@ -96,6 +96,8 @@ export declare class ASTVisitors {
|
|
|
96
96
|
* alias name).
|
|
97
97
|
*/
|
|
98
98
|
private getMemberTypeNode;
|
|
99
|
+
/** `{ name, typeNode }` for every property of an object-shaped type annotation. */
|
|
100
|
+
private getObjectTypeMembers;
|
|
99
101
|
/**
|
|
100
102
|
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
101
103
|
* string-array constant, returns the callback's first parameter name and the
|
|
@@ -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;;;;;;;;;;;;;OAaG;IACI,mBAAmB,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAK/C;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAqDzB;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;
|
|
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;IAqDzB;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IA+YZ;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAiH1B;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAIzB,mFAAmF;IACnF,OAAO,CAAC,oBAAoB;IAiB5B;;;;;;;;OAQG;IACH,OAAO,CAAC,gCAAgC;IAqDxC;;;;;;;;;OASG;IACH,OAAO,CAAC,0BAA0B;IA+ClC;;;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"}
|
|
@@ -7,6 +7,7 @@ export declare class ScopeManager {
|
|
|
7
7
|
private thisFieldStack;
|
|
8
8
|
private simpleConstants;
|
|
9
9
|
private simpleConstantObjects;
|
|
10
|
+
private simpleConstantArrays;
|
|
10
11
|
private sharedConstants;
|
|
11
12
|
private sharedConstantObjects;
|
|
12
13
|
constructor(config: Omit<I18nextToolkitConfig, 'plugins'>);
|
|
@@ -161,6 +162,12 @@ export declare class ScopeManager {
|
|
|
161
162
|
* either single form behaves identically downstream.
|
|
162
163
|
*/
|
|
163
164
|
private resolveNsArg;
|
|
165
|
+
/**
|
|
166
|
+
* If `init` is a function (optionally wrapped in `useCallback(fn, deps)`) whose
|
|
167
|
+
* first parameter is passed as the first argument to a call of an already-scoped
|
|
168
|
+
* variable (e.g. `tLocal(key, opts)`), return that variable's ScopeInfo.
|
|
169
|
+
*/
|
|
170
|
+
private resolveDelegatingWrapperScope;
|
|
164
171
|
private resolveStringArg;
|
|
165
172
|
/**
|
|
166
173
|
* Handles cases where a getFixedT-like function is a variable (from a custom hook)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/scope-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAEV,kBAAkB,EAMnB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,SAAS,EAA4B,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAG/F,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,KAAK,CAAqE;IAMlF,OAAO,CAAC,cAAc,CAAoC;IAG1D,OAAO,CAAC,eAAe,CAAiC;IAGxD,OAAO,CAAC,qBAAqB,CAAiD;
|
|
1
|
+
{"version":3,"file":"scope-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/scope-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAEV,kBAAkB,EAMnB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,SAAS,EAA4B,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAG/F,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,KAAK,CAAqE;IAMlF,OAAO,CAAC,cAAc,CAAoC;IAG1D,OAAO,CAAC,eAAe,CAAiC;IAGxD,OAAO,CAAC,qBAAqB,CAAiD;IAE9E,OAAO,CAAC,oBAAoB,CAAmC;IAI/D,OAAO,CAAC,eAAe,CAAiC;IACxD,OAAO,CAAC,qBAAqB,CAAiD;gBAEjE,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC;IAI1D;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAcjC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,+BAA+B;IAgB9C;;;;;;OAMG;IACI,KAAK,IAAK,IAAI;IASrB;;;;OAIG;IACH,eAAe,IAAK,IAAI;IAIxB;;;OAGG;IACH,cAAc,IAAK,IAAI;IAIvB;;;;;;;OAOG;IACH,YAAY,CAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAMvD;;;;;OAKG;IACH,YAAY,CAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IASvD;;;OAGG;IACH,UAAU,IAAK,IAAI;IAInB;;;OAGG;IACH,SAAS,IAAK,IAAI;IAIlB;;;;;;OAMG;IACH,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAUnD;;;;;;OAMG;IACH,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAkBrD,OAAO,CAAC,uBAAuB;IAoB/B;;OAEG;IACI,6BAA6B,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIvE;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IAqBrC;;;;;;;;;;OAUG;IACH,wBAAwB,CAAE,IAAI,EAAE,kBAAkB,GAAG,IAAI;IA0HzD;;;;;;;;OAQG;IACH,OAAO,CAAC,+BAA+B;IA8FvC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,8BAA8B;IA0EtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,yBAAyB;IAiBjC;;;;;;OAMG;IACH;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,YAAY;IAmCpB;;;;OAIG;IACH,OAAO,CAAC,6BAA6B;IAiCrC,OAAO,CAAC,gBAAgB;IAexB;;;;;;;;;OASG;IACH,OAAO,CAAC,qCAAqC;IAqB7C;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAgBnC;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IAyBrC;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI;IAwBlF;;;;;OAKG;IACH,OAAO,CAAC,4BAA4B;CA+DrC"}
|