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 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.0'); // This string is replaced with the actual version at build time by rollup
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 undefined;
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 undefined;
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 === memberName)
683
- return m.typeAnnotation?.typeAnnotation ?? m.typeAnnotation;
725
+ if (name)
726
+ out.push({ name, typeNode: m.typeAnnotation?.typeAnnotation ?? m.typeAnnotation });
684
727
  }
685
- return undefined;
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
- const content = cmatch[1] ?? cmatch[2];
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.0'); // This string is replaced with the actual version at build time by rollup
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 undefined;
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 undefined;
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 === memberName)
681
- return m.typeAnnotation?.typeAnnotation ?? m.typeAnnotation;
723
+ if (name)
724
+ out.push({ name, typeNode: m.typeAnnotation?.typeAnnotation ?? m.typeAnnotation });
682
725
  }
683
- return undefined;
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
- const content = cmatch[1] ?? cmatch[2];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.70.0",
3
+ "version": "1.70.2",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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;IAoYZ;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAwF1B;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;;;;;;;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"}
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;IAI9E,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;IAQrB;;;;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;IAsGzD;;;;;;;;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;IA8BpB,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"}
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"}