i18next-cli 1.34.0 → 1.34.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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/cli.js +271 -1
  3. package/dist/cjs/config.js +211 -1
  4. package/dist/cjs/extractor/core/ast-visitors.js +364 -1
  5. package/dist/cjs/extractor/core/extractor.js +245 -1
  6. package/dist/cjs/extractor/core/key-finder.js +132 -1
  7. package/dist/cjs/extractor/core/translation-manager.js +745 -1
  8. package/dist/cjs/extractor/parsers/ast-utils.js +85 -1
  9. package/dist/cjs/extractor/parsers/call-expression-handler.js +941 -1
  10. package/dist/cjs/extractor/parsers/comment-parser.js +375 -1
  11. package/dist/cjs/extractor/parsers/expression-resolver.js +362 -1
  12. package/dist/cjs/extractor/parsers/jsx-handler.js +492 -1
  13. package/dist/cjs/extractor/parsers/jsx-parser.js +355 -1
  14. package/dist/cjs/extractor/parsers/scope-manager.js +408 -1
  15. package/dist/cjs/extractor/plugin-manager.js +106 -1
  16. package/dist/cjs/heuristic-config.js +99 -1
  17. package/dist/cjs/index.js +28 -1
  18. package/dist/cjs/init.js +174 -1
  19. package/dist/cjs/linter.js +431 -1
  20. package/dist/cjs/locize.js +269 -1
  21. package/dist/cjs/migrator.js +196 -1
  22. package/dist/cjs/rename-key.js +354 -1
  23. package/dist/cjs/status.js +336 -1
  24. package/dist/cjs/syncer.js +120 -1
  25. package/dist/cjs/types-generator.js +165 -1
  26. package/dist/cjs/utils/default-value.js +43 -1
  27. package/dist/cjs/utils/file-utils.js +136 -1
  28. package/dist/cjs/utils/funnel-msg-tracker.js +75 -1
  29. package/dist/cjs/utils/logger.js +36 -1
  30. package/dist/cjs/utils/nested-object.js +124 -1
  31. package/dist/cjs/utils/validation.js +71 -1
  32. package/dist/esm/cli.js +269 -1
  33. package/dist/esm/config.js +206 -1
  34. package/dist/esm/extractor/core/ast-visitors.js +362 -1
  35. package/dist/esm/extractor/core/extractor.js +241 -1
  36. package/dist/esm/extractor/core/key-finder.js +130 -1
  37. package/dist/esm/extractor/core/translation-manager.js +743 -1
  38. package/dist/esm/extractor/parsers/ast-utils.js +80 -1
  39. package/dist/esm/extractor/parsers/call-expression-handler.js +939 -1
  40. package/dist/esm/extractor/parsers/comment-parser.js +373 -1
  41. package/dist/esm/extractor/parsers/expression-resolver.js +360 -1
  42. package/dist/esm/extractor/parsers/jsx-handler.js +490 -1
  43. package/dist/esm/extractor/parsers/jsx-parser.js +334 -1
  44. package/dist/esm/extractor/parsers/scope-manager.js +406 -1
  45. package/dist/esm/extractor/plugin-manager.js +103 -1
  46. package/dist/esm/heuristic-config.js +97 -1
  47. package/dist/esm/index.js +11 -1
  48. package/dist/esm/init.js +172 -1
  49. package/dist/esm/linter.js +425 -1
  50. package/dist/esm/locize.js +265 -1
  51. package/dist/esm/migrator.js +194 -1
  52. package/dist/esm/rename-key.js +352 -1
  53. package/dist/esm/status.js +334 -1
  54. package/dist/esm/syncer.js +118 -1
  55. package/dist/esm/types-generator.js +163 -1
  56. package/dist/esm/utils/default-value.js +41 -1
  57. package/dist/esm/utils/file-utils.js +131 -1
  58. package/dist/esm/utils/funnel-msg-tracker.js +72 -1
  59. package/dist/esm/utils/logger.js +34 -1
  60. package/dist/esm/utils/nested-object.js +120 -1
  61. package/dist/esm/utils/validation.js +68 -1
  62. package/package.json +2 -2
  63. package/types/locize.d.ts.map +1 -1
@@ -1 +1,364 @@
1
- "use strict";var e=require("../parsers/scope-manager.js"),t=require("../parsers/expression-resolver.js"),r=require("../parsers/call-expression-handler.js"),a=require("../parsers/jsx-handler.js");exports.ASTVisitors=class{pluginContext;config;logger;hooks;get objectKeys(){return this.callExpressionHandler.objectKeys}scopeManager;expressionResolver;callExpressionHandler;jsxHandler;currentFile="";currentCode="";constructor(s,i,n,o,p){this.pluginContext=i,this.config=s,this.logger=n,this.hooks={onBeforeVisitNode:o?.onBeforeVisitNode,onAfterVisitNode:o?.onAfterVisitNode,resolvePossibleKeyStringValues:o?.resolvePossibleKeyStringValues,resolvePossibleContextStringValues:o?.resolvePossibleContextStringValues},this.scopeManager=new e.ScopeManager(s),this.expressionResolver=p??new t.ExpressionResolver(this.hooks),this.callExpressionHandler=new r.CallExpressionHandler(s,i,n,this.expressionResolver,()=>this.getCurrentFile(),()=>this.getCurrentCode()),this.jsxHandler=new a.JSXHandler(s,i,this.expressionResolver,()=>this.getCurrentFile(),()=>this.getCurrentCode())}visit(e){this.scopeManager.reset(),this.expressionResolver.resetFileSymbols(),this.scopeManager.enterScope(),this.walk(e),this.scopeManager.exitScope()}walk(e){if(!e)return;let t=!1;if("Function"===e.type||"FunctionDeclaration"===e.type||"FunctionDecl"===e.type||"FnDecl"===e.type||"ArrowFunctionExpression"===e.type||"FunctionExpression"===e.type||"MethodDefinition"===e.type||"ClassMethod"===e.type||"ObjectMethod"===e.type){this.scopeManager.enterScope(),t=!0;const r=e.params&&Array.isArray(e.params)?e.params:e.params||[];for(const e of r){let t;if(!e)continue;if("Identifier"===e.type?t=e:"AssignmentPattern"===e.type&&e.left&&"Identifier"===e.left.type?t=e.left:"RestElement"===e.type&&e.argument&&"Identifier"===e.argument.type?t=e.argument:"Param"!==e.type&&"FnParam"!==e.type&&"Arg"!==e.type||!e.pat||"Identifier"!==e.pat.type?"Param"!==e.type&&"FnParam"!==e.type&&"Arg"!==e.type||!e.pattern||"Identifier"!==e.pattern.type?e.pat&&"Identifier"===e.pat.type?t=e.pat:e.pattern&&"Identifier"===e.pattern.type?t=e.pattern:e.left&&e.left.param&&"Identifier"===e.left.param.type?t=e.left.param:e.param&&"Identifier"===e.param.type&&(t=e.param):t=e.pattern:t=e.pat,!t)continue;const r=t.value??t.name;if(!r)continue;const a=t.typeAnnotation??e.typeAnnotation??(e.left&&e.left.typeAnnotation);let s;s=a?"TsTypeAnn"===a.type||"TsTypeAnnotation"===a.type?a.typeAnnotation??a:a:void 0;const i=e=>{if(e){if(e.typeName&&"Identifier"===e.typeName.type)return e.typeName.value??e.typeName.name;if(e.typeName&&"TsQualifiedName"===e.typeName.type){const t=e.typeName.right??e.typeName;return t?.value??t?.name}return e.typeName&&"string"==typeof e.typeName?e.typeName:"Identifier"===e.type?e.value??e.name:e.id?e.id?.value??e.id?.name??e.id:void 0}},n=e=>{if(e)return"TsLiteralType"===e.type&&e.literal?e.literal.value??e.literal.raw:"StringLiteral"===e.type||"Str"===e.type||"Literal"===e.type?e.value??e.raw??e.value:!e.literal||"StringLiteral"!==e.literal.type&&"Str"!==e.literal.type?"string"==typeof e.value?e.value:e.params&&Array.isArray(e.params)&&e.params[0]?n(e.params[0]):e.typeArguments&&Array.isArray(e.typeArguments)&&e.typeArguments[0]?n(e.typeArguments[0]):e.typeParameters&&Array.isArray(e.typeParameters)&&e.typeParameters[0]?n(e.typeParameters[0]):e.typeParams&&Array.isArray(e.typeParams)&&e.typeParams[0]?n(e.typeParams[0]):void 0:e.literal.value};if(s&&("TsTypeReference"===s.type||"TsTypeRef"===s.type||"TsTypeReference"===s.type)){if("TFunction"===i(s)){const e=[s.typeParameters?.params?.[0],s.typeParameters?.[0],s.typeArguments?.params?.[0],s.typeArguments?.[0],s.typeParams?.params?.[0],s.typeParams?.[0],s.params?.[0],s.args?.[0],s.typeParameters,s.typeArguments,s.typeParams];let t;for(const r of e)if(r){t=r;break}const a=n(t);a&&this.scopeManager.setVarInScope(r,{defaultNs:a})}}}}switch(this.hooks.onBeforeVisitNode?.(e),e.type){case"VariableDeclarator":this.scopeManager.handleVariableDeclarator(e),this.expressionResolver.captureVariableDeclarator(e);break;case"TSEnumDeclaration":case"TsEnumDeclaration":case"TsEnumDecl":this.expressionResolver.captureEnumDeclaration(e);break;case"CallExpression":this.callExpressionHandler.handleCallExpression(e,this.scopeManager.getVarFromScope.bind(this.scopeManager));break;case"JSXElement":this.jsxHandler.handleJSXElement(e,this.scopeManager.getVarFromScope.bind(this.scopeManager))}this.hooks.onAfterVisitNode?.(e);for(const t in e){if("span"===t)continue;const r=e[t];if(Array.isArray(r)){for(const e of r)if(e&&"object"==typeof e)if("VariableDeclarator"!==e.type){if(e&&e.id&&Array.isArray(e.members)&&this.expressionResolver.captureEnumDeclaration(e),"VariableDeclaration"===e.type&&Array.isArray(e.declarations))for(const t of e.declarations)t&&"object"==typeof t&&"VariableDeclarator"===t.type&&(this.scopeManager.handleVariableDeclarator(t),this.expressionResolver.captureVariableDeclarator(t))}else this.scopeManager.handleVariableDeclarator(e),this.expressionResolver.captureVariableDeclarator(e);for(const e of r)e&&"object"==typeof e&&this.walk(e)}else r&&"object"==typeof r&&this.walk(r)}t&&this.scopeManager.exitScope()}getVarFromScope(e){return this.scopeManager.getVarFromScope(e)}setCurrentFile(e,t){this.currentFile=e,this.currentCode=t,this.callExpressionHandler.resetSearchIndex(),this.jsxHandler.resetSearchIndex()}getCurrentFile(){return this.currentFile}getCurrentCode(){return this.currentCode}};
1
+ 'use strict';
2
+
3
+ var scopeManager = require('../parsers/scope-manager.js');
4
+ var expressionResolver = require('../parsers/expression-resolver.js');
5
+ var callExpressionHandler = require('../parsers/call-expression-handler.js');
6
+ var jsxHandler = require('../parsers/jsx-handler.js');
7
+
8
+ /**
9
+ * AST visitor class that traverses JavaScript/TypeScript syntax trees to extract translation keys.
10
+ *
11
+ * This class implements a manual recursive walker that:
12
+ * - Maintains scope information for tracking useTranslation and getFixedT calls
13
+ * - Extracts keys from t() function calls with various argument patterns
14
+ * - Handles JSX Trans components with complex children serialization
15
+ * - Supports both string literals and selector API for type-safe keys
16
+ * - Processes pluralization and context variants
17
+ * - Manages namespace resolution from multiple sources
18
+ *
19
+ * The visitor respects configuration options for separators, function names,
20
+ * component names, and other extraction settings.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const visitors = new ASTVisitors(config, pluginContext, logger)
25
+ * visitors.visit(parsedAST)
26
+ *
27
+ * // The pluginContext will now contain all extracted keys
28
+ * ```
29
+ */
30
+ class ASTVisitors {
31
+ pluginContext;
32
+ config;
33
+ logger;
34
+ hooks;
35
+ get objectKeys() {
36
+ return this.callExpressionHandler.objectKeys;
37
+ }
38
+ scopeManager;
39
+ expressionResolver;
40
+ callExpressionHandler;
41
+ jsxHandler;
42
+ currentFile = '';
43
+ currentCode = '';
44
+ /**
45
+ * Creates a new AST visitor instance.
46
+ *
47
+ * @param config - Toolkit configuration with extraction settings
48
+ * @param pluginContext - Context for adding discovered translation keys
49
+ * @param logger - Logger for warnings and debug information
50
+ */
51
+ constructor(config, pluginContext, logger, hooks, expressionResolver$1) {
52
+ this.pluginContext = pluginContext;
53
+ this.config = config;
54
+ this.logger = logger;
55
+ this.hooks = {
56
+ onBeforeVisitNode: hooks?.onBeforeVisitNode,
57
+ onAfterVisitNode: hooks?.onAfterVisitNode,
58
+ resolvePossibleKeyStringValues: hooks?.resolvePossibleKeyStringValues,
59
+ resolvePossibleContextStringValues: hooks?.resolvePossibleContextStringValues
60
+ };
61
+ this.scopeManager = new scopeManager.ScopeManager(config);
62
+ // use shared resolver when provided so captured enums/objects are visible across files
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());
65
+ this.jsxHandler = new jsxHandler.JSXHandler(config, pluginContext, this.expressionResolver, () => this.getCurrentFile(), () => this.getCurrentCode());
66
+ }
67
+ /**
68
+ * Main entry point for AST traversal.
69
+ * Creates a root scope and begins the recursive walk through the syntax tree.
70
+ *
71
+ * @param node - The root module node to traverse
72
+ */
73
+ visit(node) {
74
+ // Reset any per-file scope state to avoid leaking scopes between files.
75
+ this.scopeManager.reset();
76
+ // Reset per-file captured variables in the expression resolver so variables from other files don't leak.
77
+ this.expressionResolver.resetFileSymbols();
78
+ this.scopeManager.enterScope(); // Create the root scope for the file
79
+ this.walk(node);
80
+ this.scopeManager.exitScope(); // Clean up the root scope
81
+ }
82
+ /**
83
+ * Recursively walks through AST nodes, handling scoping and visiting logic.
84
+ *
85
+ * This is the core traversal method that:
86
+ * 1. Manages function scopes (enter/exit)
87
+ * 2. Dispatches to specific handlers based on node type
88
+ * 3. Recursively processes child nodes
89
+ * 4. Maintains proper scope cleanup
90
+ *
91
+ * @param node - The current AST node to process
92
+ *
93
+ * @private
94
+ */
95
+ walk(node) {
96
+ if (!node)
97
+ return;
98
+ let isNewScope = false;
99
+ // ENTER SCOPE for functions
100
+ // Accept many SWC/TS AST variants for function-like nodes (declarations, expressions, arrow functions)
101
+ if (node.type === 'Function' ||
102
+ node.type === 'FunctionDeclaration' ||
103
+ node.type === 'FunctionDecl' ||
104
+ node.type === 'FnDecl' ||
105
+ node.type === 'ArrowFunctionExpression' ||
106
+ node.type === 'FunctionExpression' ||
107
+ node.type === 'MethodDefinition' ||
108
+ node.type === 'ClassMethod' ||
109
+ node.type === 'ObjectMethod') {
110
+ this.scopeManager.enterScope();
111
+ isNewScope = true;
112
+ const params = (node.params && Array.isArray(node.params)) ? node.params : (node.params || []);
113
+ for (const p of params) {
114
+ // handle common param shapes: Identifier, AssignmentPattern (default), RestElement ignored
115
+ let ident;
116
+ if (!p)
117
+ continue;
118
+ // direct identifier (arrow fn params etc)
119
+ if (p.type === 'Identifier')
120
+ ident = p;
121
+ // default params: (x = ...) -> AssignmentPattern.left
122
+ else if (p.type === 'AssignmentPattern' && p.left && p.left.type === 'Identifier')
123
+ ident = p.left;
124
+ // rest: (...args)
125
+ else if (p.type === 'RestElement' && p.argument && p.argument.type === 'Identifier')
126
+ ident = p.argument;
127
+ // SWC/TS often wrap params: { pat: Identifier } or { pattern: Identifier } or FnParam/Param
128
+ else if ((p.type === 'Param' || p.type === 'FnParam' || p.type === 'Arg') && p.pat && p.pat.type === 'Identifier')
129
+ ident = p.pat;
130
+ else if ((p.type === 'Param' || p.type === 'FnParam' || p.type === 'Arg') && p.pattern && p.pattern.type === 'Identifier')
131
+ ident = p.pattern;
132
+ else if (p.pat && p.pat.type === 'Identifier')
133
+ ident = p.pat;
134
+ else if (p.pattern && p.pattern.type === 'Identifier')
135
+ ident = p.pattern;
136
+ // some parsers expose .param or .left.param shapes
137
+ else if ((p.left && p.left.param && p.left.param.type === 'Identifier'))
138
+ ident = p.left.param;
139
+ else if ((p.param && p.param.type === 'Identifier'))
140
+ ident = p.param;
141
+ if (!ident)
142
+ continue;
143
+ const paramKey = (ident.value ?? ident.name);
144
+ if (!paramKey)
145
+ continue;
146
+ // Try to locate TypeScript type node carried on the identifier.
147
+ const rawTypeAnn = (ident.typeAnnotation ?? p.typeAnnotation ?? (p.left && p.left.typeAnnotation));
148
+ let typeAnn;
149
+ if (rawTypeAnn) {
150
+ // SWC may wrap the actual TS type in a wrapper like TsTypeAnn / TsTypeAnnotation
151
+ if (rawTypeAnn.type === 'TsTypeAnn' || rawTypeAnn.type === 'TsTypeAnnotation') {
152
+ typeAnn = rawTypeAnn.typeAnnotation ?? rawTypeAnn;
153
+ }
154
+ else {
155
+ typeAnn = rawTypeAnn;
156
+ }
157
+ }
158
+ else {
159
+ typeAnn = undefined;
160
+ }
161
+ // Small helpers to robustly extract the referenced type name and literal string
162
+ const extractTypeName = (ta) => {
163
+ if (!ta)
164
+ return undefined;
165
+ // Identifier style: { type: 'Identifier', value: 'TFunction' } OR { name: 'TFunction' }
166
+ if (ta.typeName && (ta.typeName.type === 'Identifier'))
167
+ return ta.typeName.value ?? ta.typeName.name;
168
+ if (ta.typeName && ta.typeName.type === 'TsQualifiedName') {
169
+ // Qualified like Foo.TFunction -> try right side
170
+ const right = (ta.typeName.right ?? ta.typeName);
171
+ return right?.value ?? right?.name;
172
+ }
173
+ if (ta.typeName && typeof ta.typeName === 'string')
174
+ return ta.typeName;
175
+ if (ta.type === 'Identifier')
176
+ return ta.value ?? ta.name;
177
+ if (ta.id)
178
+ return ta.id?.value ?? ta.id?.name ?? ta.id;
179
+ return undefined;
180
+ };
181
+ const extractStringLiteralValue = (node) => {
182
+ if (!node)
183
+ return undefined;
184
+ // shapes: TsLiteralType -> { literal: { type: 'StringLiteral', value: 'x' } }
185
+ if (node.type === 'TsLiteralType' && node.literal)
186
+ return node.literal.value ?? node.literal.raw;
187
+ if (node.type === 'StringLiteral' || node.type === 'Str' || node.type === 'Literal')
188
+ return node.value ?? node.raw ?? node.value;
189
+ if (node.literal && (node.literal.type === 'StringLiteral' || node.literal.type === 'Str'))
190
+ return node.literal.value;
191
+ // some SWC builds put the string directly on .value
192
+ if (typeof node.value === 'string')
193
+ return node.value;
194
+ // handle wrapped parameter like { params: [ ... ] } where a literal might be one level deeper
195
+ if (node.params && Array.isArray(node.params) && node.params[0])
196
+ return extractStringLiteralValue(node.params[0]);
197
+ if (node.typeArguments && Array.isArray(node.typeArguments) && node.typeArguments[0])
198
+ return extractStringLiteralValue(node.typeArguments[0]);
199
+ if (node.typeParameters && Array.isArray(node.typeParameters) && node.typeParameters[0])
200
+ return extractStringLiteralValue(node.typeParameters[0]);
201
+ if (node.typeParams && Array.isArray(node.typeParams) && node.typeParams[0])
202
+ return extractStringLiteralValue(node.typeParams[0]);
203
+ return undefined;
204
+ };
205
+ // Detect TsTypeReference like: TFunction<"my-custom-namespace">
206
+ if (typeAnn && (typeAnn.type === 'TsTypeReference' || typeAnn.type === 'TsTypeRef' || typeAnn.type === 'TsTypeReference')) {
207
+ const finalTypeName = extractTypeName(typeAnn);
208
+ if (finalTypeName === 'TFunction') {
209
+ // support multiple AST shapes for type parameters:
210
+ // - typeAnn.typeParameters?.params?.[0]
211
+ // - typeAnn.typeArguments?.params?.[0]
212
+ // - typeAnn.typeParams?.[0] / typeAnn.params?.[0]
213
+ const candidates = [
214
+ typeAnn.typeParameters?.params?.[0],
215
+ typeAnn.typeParameters?.[0],
216
+ typeAnn.typeArguments?.params?.[0],
217
+ typeAnn.typeArguments?.[0],
218
+ typeAnn.typeParams?.params?.[0],
219
+ typeAnn.typeParams?.[0],
220
+ typeAnn.params?.[0],
221
+ typeAnn.args?.[0],
222
+ typeAnn.typeParameters, // fallback if it's directly the literal
223
+ typeAnn.typeArguments,
224
+ typeAnn.typeParams,
225
+ ];
226
+ let tp;
227
+ for (const c of candidates) {
228
+ if (c) {
229
+ tp = c;
230
+ break;
231
+ }
232
+ }
233
+ const ns = extractStringLiteralValue(tp);
234
+ if (ns) {
235
+ this.scopeManager.setVarInScope(paramKey, { defaultNs: ns });
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ this.hooks.onBeforeVisitNode?.(node);
242
+ // --- VISIT LOGIC ---
243
+ // Handle specific node types
244
+ switch (node.type) {
245
+ case 'VariableDeclarator':
246
+ this.scopeManager.handleVariableDeclarator(node);
247
+ // Capture simple variable initializers so the expressionResolver can
248
+ // resolve identifiers / member expressions that reference them.
249
+ this.expressionResolver.captureVariableDeclarator(node);
250
+ break;
251
+ case 'TSEnumDeclaration':
252
+ case 'TsEnumDeclaration':
253
+ case 'TsEnumDecl':
254
+ // capture enums into resolver symbol table
255
+ this.expressionResolver.captureEnumDeclaration(node);
256
+ break;
257
+ case 'CallExpression':
258
+ this.callExpressionHandler.handleCallExpression(node, this.scopeManager.getVarFromScope.bind(this.scopeManager));
259
+ break;
260
+ case 'JSXElement':
261
+ this.jsxHandler.handleJSXElement(node, this.scopeManager.getVarFromScope.bind(this.scopeManager));
262
+ break;
263
+ }
264
+ this.hooks.onAfterVisitNode?.(node);
265
+ // --- END VISIT LOGIC ---
266
+ // --- RECURSION ---
267
+ // Recurse into the children of the current node
268
+ for (const key in node) {
269
+ if (key === 'span')
270
+ continue;
271
+ const child = node[key];
272
+ if (Array.isArray(child)) {
273
+ // Pre-scan array children to register VariableDeclarator-based scopes
274
+ // (e.g., `const { t } = useTranslation(...)`) before walking the rest
275
+ // of the items. This ensures that functions/arrow-functions defined
276
+ // earlier in the same block that reference t will resolve to the
277
+ // correct scope even if the `useTranslation` declarator appears later.
278
+ for (const item of child) {
279
+ if (!item || typeof item !== 'object')
280
+ continue;
281
+ // Direct declarator present in arrays (rare)
282
+ if (item.type === 'VariableDeclarator') {
283
+ this.scopeManager.handleVariableDeclarator(item);
284
+ this.expressionResolver.captureVariableDeclarator(item);
285
+ continue;
286
+ }
287
+ // enum declarations can appear as ExportDeclaration.declaration earlier; be permissive
288
+ if (item && item.id && Array.isArray(item.members)) {
289
+ this.expressionResolver.captureEnumDeclaration(item);
290
+ // continue to allow further traversal
291
+ }
292
+ // Common case: VariableDeclaration which contains .declarations (VariableDeclarator[])
293
+ if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
294
+ for (const decl of item.declarations) {
295
+ if (decl && typeof decl === 'object' && decl.type === 'VariableDeclarator') {
296
+ this.scopeManager.handleVariableDeclarator(decl);
297
+ this.expressionResolver.captureVariableDeclarator(decl);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ for (const item of child) {
303
+ // Be less strict: if it's a non-null object, walk it.
304
+ // This allows traversal into nodes that might not have a `.type` property
305
+ // but still contain other valid AST nodes.
306
+ if (item && typeof item === 'object') {
307
+ this.walk(item);
308
+ }
309
+ }
310
+ }
311
+ else if (child && typeof child === 'object') {
312
+ // The condition for single objects should be the same as for array items.
313
+ // Do not require `child.type`. This allows traversal into class method bodies.
314
+ this.walk(child);
315
+ }
316
+ }
317
+ // --- END RECURSION ---
318
+ // LEAVE SCOPE for functions
319
+ if (isNewScope) {
320
+ this.scopeManager.exitScope();
321
+ }
322
+ }
323
+ /**
324
+ * Retrieves variable information from the scope chain.
325
+ * Searches from innermost to outermost scope.
326
+ *
327
+ * @param name - Variable name to look up
328
+ * @returns Scope information if found, undefined otherwise
329
+ *
330
+ * @private
331
+ */
332
+ getVarFromScope(name) {
333
+ return this.scopeManager.getVarFromScope(name);
334
+ }
335
+ /**
336
+ * Sets the current file path and code used by the extractor.
337
+ * Also resets the search index for location tracking.
338
+ */
339
+ setCurrentFile(file, code) {
340
+ this.currentFile = file;
341
+ this.currentCode = code;
342
+ // Reset search indexes when processing a new file
343
+ this.callExpressionHandler.resetSearchIndex();
344
+ this.jsxHandler.resetSearchIndex();
345
+ }
346
+ /**
347
+ * Returns the currently set file path.
348
+ *
349
+ * @returns The current file path as a string, or `undefined` if no file has been set.
350
+ * @remarks
351
+ * Use this to retrieve the file context that was previously set via `setCurrentFile`.
352
+ */
353
+ getCurrentFile() {
354
+ return this.currentFile;
355
+ }
356
+ /**
357
+ * @returns The full source code string for the file currently under processing.
358
+ */
359
+ getCurrentCode() {
360
+ return this.currentCode;
361
+ }
362
+ }
363
+
364
+ exports.ASTVisitors = ASTVisitors;
@@ -1 +1,245 @@
1
- "use strict";var t=require("ora"),e=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),s=require("./key-finder.js"),o=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../parsers/comment-parser.js"),l=require("../../utils/logger.js"),u=require("../../utils/file-utils.js"),m=require("../../utils/funnel-msg-tracker.js");exports.extract=async function(t,{syncPrimaryWithDefaults:e=!1}={}){t.extract.primaryLanguage||=t.locales[0]||"en",t.extract.secondaryLanguages||=t.locales.filter(e=>e!==t?.extract?.primaryLanguage),t.extract.functions||=["t","*.t"],t.extract.transComponents||=["Trans"];const{allKeys:r,objectKeys:a}=await s.findKeys(t);return o.getTranslations(r,a,t,{syncPrimaryWithDefaults:e})},exports.processFile=async function(t,s,o,u,m,g=new l.ConsoleLogger){try{let e=await a.readFile(t,"utf-8");for(const r of s)try{const a=await(r.onLoad?.(e,t));void 0!==a&&(e=a)}catch(t){g.warn(`Plugin ${r.name} onLoad failed:`,t)}const l=n.extname(t).toLowerCase(),y=".ts"===l||".tsx"===l||".mts"===l||".cts"===l,d=".tsx"===l,p=".jsx"===l;let f;try{f=await r.parse(e,{syntax:y?"typescript":"ecmascript",tsx:d,jsx:p,decorators:!0,dynamicImport:!0,comments:!0})}catch(a){if(".ts"!==l||d){if(".js"!==l||p)throw new i.ExtractorError("Failed to process file",t,a);try{f=await r.parse(e,{syntax:"ecmascript",jsx:!0,decorators:!0,dynamicImport:!0,comments:!0}),g.info?.(`Parsed ${t} using JSX fallback`)}catch(e){throw new i.ExtractorError("Failed to process file",t,e)}}else try{f=await r.parse(e,{syntax:"typescript",tsx:!0,decorators:!0,dynamicImport:!0,comments:!0}),g.info?.(`Parsed ${t} using TSX fallback`)}catch(e){throw new i.ExtractorError("Failed to process file",t,e)}}u.getVarFromScope=o.getVarFromScope.bind(o),o.setCurrentFile(t,e),o.visit(f),!1!==m.extract.extractFromComments&&c.extractKeysFromComments(e,u,m,o.getVarFromScope.bind(o))}catch(r){g.warn(`${e.yellow("Skipping file due to error:")} ${t}`);const a=r,n="string"==typeof a?.message&&a.message.trim().length>0?a.message:("string"==typeof a?a:"")||a?.toString?.()||"Unknown error";g.warn(` ${n}`),a?.message&&""!==String(a.message).trim()||!a?.stack||g.warn(` ${String(a.stack)}`)}},exports.runExtractor=async function(r,{isWatchMode:c=!1,isDryRun:g=!1,syncPrimaryWithDefaults:y=!1,syncAll:d=!1}={},p=new l.ConsoleLogger){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(t=>t!==r?.extract?.primaryLanguage),r.extract.functions||=["t","*.t"],r.extract.transComponents||=["Trans"],i.validateExtractorConfig(r);const f=r.plugins||[],x=t("Running i18next key extractor...\n").start();try{const{allKeys:t,objectKeys:i}=await s.findKeys(r,p);x.text=`Found ${t.size} unique keys. Updating translation files...`;const c=await o.getTranslations(t,i,r,{syncPrimaryWithDefaults:y,syncAll:d});let l=!1;for(const t of c)if(t.updated&&(l=!0,!g)){const s=r.extract.outputFormat??(t.path.endsWith(".json5")?"json5":"json"),o="json5"===s?await u.loadRawJson5Content(t.path)??void 0:void 0,i=u.serializeTranslationFile(t.newTranslations,s,r.extract.indentation,o);await a.mkdir(n.dirname(t.path),{recursive:!0}),await a.writeFile(t.path,i),p.info(e.green(`Updated: ${t.path}`))}if(f.length>0){x.text="Running post-extraction plugins...";for(const t of f)await(t.afterSync?.(c,r))}return x.succeed(e.bold("Extraction complete!")),l&&await async function(){if(!await m.shouldShowFunnel("extract"))return;return console.log(e.yellow.bold("\nšŸ’” Tip: Tired of running the extractor manually?")),console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,'),console.log(" where keys are created and translated automatically as you code."),console.log(` Learn more: ${e.cyan("https://www.locize.com/blog/i18next-savemissing-ai-automation")}`),console.log(` Watch the video: ${e.cyan("https://youtu.be/joPsZghT3wM")}`),m.recordFunnelShown("extract")}(),l}catch(t){throw x.fail(e.red("Extraction failed.")),t}};
1
+ 'use strict';
2
+
3
+ var ora = require('ora');
4
+ var chalk = require('chalk');
5
+ var core = require('@swc/core');
6
+ var promises = require('node:fs/promises');
7
+ var node_path = require('node:path');
8
+ var keyFinder = require('./key-finder.js');
9
+ var translationManager = require('./translation-manager.js');
10
+ var validation = require('../../utils/validation.js');
11
+ var commentParser = require('../parsers/comment-parser.js');
12
+ var logger = require('../../utils/logger.js');
13
+ var fileUtils = require('../../utils/file-utils.js');
14
+ var funnelMsgTracker = require('../../utils/funnel-msg-tracker.js');
15
+
16
+ /**
17
+ * Main extractor function that runs the complete key extraction and file generation process.
18
+ *
19
+ * This is the primary entry point that:
20
+ * 1. Validates configuration
21
+ * 2. Sets up default sync options
22
+ * 3. Finds all translation keys across source files
23
+ * 4. Generates/updates translation files for all locales
24
+ * 5. Provides progress feedback via spinner
25
+ * 6. Returns whether any files were updated
26
+ *
27
+ * @param config - The i18next toolkit configuration object
28
+ * @param logger - Logger instance for output (defaults to ConsoleLogger)
29
+ * @returns Promise resolving to boolean indicating if any files were updated
30
+ *
31
+ * @throws {ExtractorError} When configuration validation fails or extraction process encounters errors
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const config = await loadConfig()
36
+ * const updated = await runExtractor(config)
37
+ * if (updated) {
38
+ * console.log('Translation files were updated')
39
+ * }
40
+ * ```
41
+ */
42
+ async function runExtractor(config, { isWatchMode = false, isDryRun = false, syncPrimaryWithDefaults = false, syncAll = false } = {}, logger$1 = new logger.ConsoleLogger()) {
43
+ config.extract.primaryLanguage ||= config.locales[0] || 'en';
44
+ config.extract.secondaryLanguages ||= config.locales.filter((l) => l !== config?.extract?.primaryLanguage);
45
+ // Ensure default function and component names are set if not provided.
46
+ config.extract.functions ||= ['t', '*.t'];
47
+ config.extract.transComponents ||= ['Trans'];
48
+ validation.validateExtractorConfig(config);
49
+ const plugins = config.plugins || [];
50
+ const spinner = ora('Running i18next key extractor...\n').start();
51
+ try {
52
+ const { allKeys, objectKeys } = await keyFinder.findKeys(config, logger$1);
53
+ spinner.text = `Found ${allKeys.size} unique keys. Updating translation files...`;
54
+ const results = await translationManager.getTranslations(allKeys, objectKeys, config, { syncPrimaryWithDefaults, syncAll });
55
+ let anyFileUpdated = false;
56
+ for (const result of results) {
57
+ if (result.updated) {
58
+ anyFileUpdated = true;
59
+ if (!isDryRun) {
60
+ // prefer explicit outputFormat; otherwise infer from file extension per-file
61
+ const effectiveFormat = config.extract.outputFormat ?? (result.path.endsWith('.json5') ? 'json5' : 'json');
62
+ const rawContent = effectiveFormat === 'json5'
63
+ ? (await fileUtils.loadRawJson5Content(result.path)) ?? undefined
64
+ : undefined;
65
+ const fileContent = fileUtils.serializeTranslationFile(result.newTranslations, effectiveFormat, config.extract.indentation, rawContent);
66
+ await promises.mkdir(node_path.dirname(result.path), { recursive: true });
67
+ await promises.writeFile(result.path, fileContent);
68
+ logger$1.info(chalk.green(`Updated: ${result.path}`));
69
+ }
70
+ }
71
+ }
72
+ // Run afterSync hooks from plugins
73
+ if (plugins.length > 0) {
74
+ spinner.text = 'Running post-extraction plugins...';
75
+ for (const plugin of plugins) {
76
+ await plugin.afterSync?.(results, config);
77
+ }
78
+ }
79
+ spinner.succeed(chalk.bold('Extraction complete!'));
80
+ // Show the funnel message only if files were actually changed.
81
+ if (anyFileUpdated)
82
+ await printLocizeFunnel();
83
+ return anyFileUpdated;
84
+ }
85
+ catch (error) {
86
+ spinner.fail(chalk.red('Extraction failed.'));
87
+ // Re-throw or handle error
88
+ throw error;
89
+ }
90
+ }
91
+ /**
92
+ * Processes an individual source file for translation key extraction.
93
+ *
94
+ * This function:
95
+ * 1. Reads the source file
96
+ * 2. Runs plugin onLoad hooks for code transformation
97
+ * 3. Parses the code into an Abstract Syntax Tree (AST) using SWC
98
+ * 4. Extracts keys from comments using regex patterns
99
+ * 5. Traverses the AST using visitors to find translation calls
100
+ * 6. Runs plugin onVisitNode hooks for custom extraction logic
101
+ *
102
+ * @param file - Path to the source file to process
103
+ * @param config - The i18next toolkit configuration object
104
+ * @param logger - Logger instance for output
105
+ * @param allKeys - Map to accumulate found translation keys
106
+ *
107
+ * @throws {ExtractorError} When file processing fails
108
+ *
109
+ * @internal
110
+ */
111
+ async function processFile(file, plugins, astVisitors, pluginContext, config, logger$1 = new logger.ConsoleLogger()) {
112
+ try {
113
+ let code = await promises.readFile(file, 'utf-8');
114
+ // Run onLoad hooks from plugins with error handling
115
+ for (const plugin of plugins) {
116
+ try {
117
+ const result = await plugin.onLoad?.(code, file);
118
+ if (result !== undefined) {
119
+ code = result;
120
+ }
121
+ }
122
+ catch (err) {
123
+ logger$1.warn(`Plugin ${plugin.name} onLoad failed:`, err);
124
+ // Continue with the original code if the plugin fails
125
+ }
126
+ }
127
+ // Determine parser options from file extension so .ts is not parsed as TSX
128
+ const fileExt = node_path.extname(file).toLowerCase();
129
+ const isTypeScriptFile = fileExt === '.ts' || fileExt === '.tsx' || fileExt === '.mts' || fileExt === '.cts';
130
+ const isTSX = fileExt === '.tsx';
131
+ const isJSX = fileExt === '.jsx';
132
+ let ast;
133
+ try {
134
+ ast = await core.parse(code, {
135
+ syntax: isTypeScriptFile ? 'typescript' : 'ecmascript',
136
+ tsx: isTSX,
137
+ jsx: isJSX,
138
+ decorators: true,
139
+ dynamicImport: true,
140
+ comments: true,
141
+ });
142
+ }
143
+ catch (err) {
144
+ // Fallback for .ts files with JSX (already present)
145
+ if (fileExt === '.ts' && !isTSX) {
146
+ try {
147
+ ast = await core.parse(code, {
148
+ syntax: 'typescript',
149
+ tsx: true,
150
+ decorators: true,
151
+ dynamicImport: true,
152
+ comments: true,
153
+ });
154
+ logger$1.info?.(`Parsed ${file} using TSX fallback`);
155
+ }
156
+ catch (err2) {
157
+ throw new validation.ExtractorError('Failed to process file', file, err2);
158
+ }
159
+ // Fallback for .js files with JSX
160
+ }
161
+ else if (fileExt === '.js' && !isJSX) {
162
+ try {
163
+ ast = await core.parse(code, {
164
+ syntax: 'ecmascript',
165
+ jsx: true,
166
+ decorators: true,
167
+ dynamicImport: true,
168
+ comments: true,
169
+ });
170
+ logger$1.info?.(`Parsed ${file} using JSX fallback`);
171
+ }
172
+ catch (err2) {
173
+ throw new validation.ExtractorError('Failed to process file', file, err2);
174
+ }
175
+ }
176
+ else {
177
+ throw new validation.ExtractorError('Failed to process file', file, err);
178
+ }
179
+ }
180
+ // "Wire up" the visitor's scope method to the context.
181
+ // This avoids a circular dependency while giving plugins access to the scope.
182
+ pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors);
183
+ // Pass BOTH file and code
184
+ astVisitors.setCurrentFile(file, code);
185
+ // 3. FIRST: Visit the AST to build scope information
186
+ astVisitors.visit(ast);
187
+ // 4. THEN: Extract keys from comments with scope resolution (now scope info is available)
188
+ if (config.extract.extractFromComments !== false) {
189
+ commentParser.extractKeysFromComments(code, pluginContext, config, astVisitors.getVarFromScope.bind(astVisitors));
190
+ }
191
+ }
192
+ catch (error) {
193
+ logger$1.warn(`${chalk.yellow('Skipping file due to error:')} ${file}`);
194
+ const err = error;
195
+ const msg = typeof err?.message === 'string' && err.message.trim().length > 0
196
+ ? err.message
197
+ : (typeof err === 'string' ? err : '') || err?.toString?.() || 'Unknown error';
198
+ logger$1.warn(` ${msg}`);
199
+ // If message is missing, stack is often the only useful clue
200
+ if ((!err?.message || String(err.message).trim() === '') && err?.stack) {
201
+ logger$1.warn(` ${String(err.stack)}`);
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Simplified extraction function that returns translation results without file writing.
207
+ * Used primarily for testing and programmatic access.
208
+ *
209
+ * @param config - The i18next toolkit configuration object
210
+ * @returns Promise resolving to array of translation results
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * const results = await extract(config)
215
+ * for (const result of results) {
216
+ * console.log(`${result.path}: ${result.updated ? 'Updated' : 'No changes'}`)
217
+ * }
218
+ * ```
219
+ */
220
+ async function extract(config, { syncPrimaryWithDefaults = false } = {}) {
221
+ config.extract.primaryLanguage ||= config.locales[0] || 'en';
222
+ config.extract.secondaryLanguages ||= config.locales.filter((l) => l !== config?.extract?.primaryLanguage);
223
+ config.extract.functions ||= ['t', '*.t'];
224
+ config.extract.transComponents ||= ['Trans'];
225
+ const { allKeys, objectKeys } = await keyFinder.findKeys(config);
226
+ return translationManager.getTranslations(allKeys, objectKeys, config, { syncPrimaryWithDefaults });
227
+ }
228
+ /**
229
+ * Prints a promotional message for the locize saveMissing workflow.
230
+ * This message is shown after a successful extraction that resulted in changes.
231
+ */
232
+ async function printLocizeFunnel() {
233
+ if (!(await funnelMsgTracker.shouldShowFunnel('extract')))
234
+ return;
235
+ console.log(chalk.yellow.bold('\nšŸ’” Tip: Tired of running the extractor manually?'));
236
+ console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,');
237
+ console.log(' where keys are created and translated automatically as you code.');
238
+ console.log(` Learn more: ${chalk.cyan('https://www.locize.com/blog/i18next-savemissing-ai-automation')}`);
239
+ console.log(` Watch the video: ${chalk.cyan('https://youtu.be/joPsZghT3wM')}`);
240
+ return funnelMsgTracker.recordFunnelShown('extract');
241
+ }
242
+
243
+ exports.extract = extract;
244
+ exports.processFile = processFile;
245
+ exports.runExtractor = runExtractor;