i18next-cli 1.34.0 → 1.35.0

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 (66) 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 +372 -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 +942 -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 +370 -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 +940 -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/extractor/core/ast-visitors.d.ts.map +1 -1
  64. package/types/extractor/parsers/call-expression-handler.d.ts +3 -2
  65. package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
  66. package/types/locize.d.ts.map +1 -1
@@ -1 +1,408 @@
1
- "use strict";var e=require("./ast-utils.js");exports.ScopeManager=class{scopeStack=[];config;scope=new Map;simpleConstants=new Map;constructor(e){this.config=e}reset(){this.scopeStack=[],this.scope=new Map,this.simpleConstants.clear()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0?this.scopeStack[this.scopeStack.length-1].set(e,t):this.scope.set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e)){return this.scopeStack[t].get(e)}const t=this.scope.get(e);if(t)return t}getUseTranslationConfig(e){const t=this.config.extract.useTranslationNames||["useTranslation"];for(const i of t){if("string"==typeof i&&i===e)return{name:e,nsArg:0,keyPrefixArg:1};if("object"==typeof i&&i.name===e)return{name:i.name,nsArg:i.nsArg??0,keyPrefixArg:i.keyPrefixArg??1}}}resolveSimpleStringIdentifier(e){return this.simpleConstants.get(e)}handleVariableDeclarator(e){const t=e.init;if(!t)return;"Identifier"===e.id.type&&"StringLiteral"===t.type&&this.simpleConstants.set(e.id.value,t.value);const i="AwaitExpression"===t.type&&"CallExpression"===t.argument.type?t.argument:"CallExpression"===t.type?t:null;if(!i)return;const r=i.callee;if("Identifier"===r.type){const t=this.getUseTranslationConfig(r.value);if(t)return this.handleUseTranslationDeclarator(e,i,t),void this.handleUseTranslationForComments(e,i,t)}if("Identifier"===r.type){if(this.getVarFromScope(r.value))return void this.handleGetFixedTFromVariableDeclarator(e,i,r.value)}"MemberExpression"===r.type&&"Identifier"===r.property.type&&"getFixedT"===r.property.value&&this.handleGetFixedTDeclarator(e,i)}handleUseTranslationForComments(t,i,r){let s;if("Identifier"===t.id.type&&(s=t.id.value),"ArrayPattern"===t.id.type){const e=t.id.elements[0];"Identifier"===e?.type&&(s=e.value)}if("ObjectPattern"===t.id.type)for(const e of t.id.properties){if("AssignmentPatternProperty"===e.type&&"Identifier"===e.key.type&&("t"===e.key.value||"getFixedT"===e.key.value)){s=e.key.value;break}if("KeyValuePatternProperty"===e.type&&"Identifier"===e.key.type&&("t"===e.key.value||"getFixedT"===e.key.value)&&"Identifier"===e.value.type){s=e.value.value;break}}if(!s)return;const n=r.nsArg??0,a=r.keyPrefixArg??1;let o,l;const p=i.arguments?.[0]?.expression,y=i.arguments?.[1]?.expression,u=i.arguments?.[2]?.expression;var f;let c;if("useTranslation"===r.name&&"StringLiteral"===p?.type&&"StringLiteral"===y?.type&&(f=p.value,/^[a-z]{2,3}([-_][A-Za-z0-9-]+)?$/i.test(f)))o=y.value,c=u;else{if(-1!==n){const e=i.arguments?.[n]?.expression;"StringLiteral"===e?.type?o=e.value:"ArrayExpression"===e?.type&&"StringLiteral"===e.elements[0]?.expression?.type&&(o=e.elements[0].expression.value)}c=-1===a?void 0:i.arguments?.[a]?.expression}if("ObjectExpression"===c?.type){const t=e.getObjectPropValue(c,"keyPrefix");l="string"==typeof t?t:void 0}else if("StringLiteral"===c?.type)l=c.value;else if("Identifier"===c?.type)l=this.resolveSimpleStringIdentifier(c.value);else if("TemplateLiteral"===c?.type){const e=c;0===(e.expressions||[]).length&&(l=e.quasis?.[0]?.cooked??void 0)}(o||l)&&this.scope.set(s,{defaultNs:o,keyPrefix:l})}handleUseTranslationDeclarator(t,i,r){let s;if("Identifier"===t.id.type&&(s=t.id.value),"ArrayPattern"===t.id.type){const e=t.id.elements[0];"Identifier"===e?.type&&(s=e.value)}if("ObjectPattern"===t.id.type)for(const e of t.id.properties){if("AssignmentPatternProperty"===e.type&&"Identifier"===e.key.type&&("t"===e.key.value||"getFixedT"===e.key.value)){s=e.key.value;break}if("KeyValuePatternProperty"===e.type&&"Identifier"===e.key.type&&("t"===e.key.value||"getFixedT"===e.key.value)&&"Identifier"===e.value.type){s=e.value.value;break}}if(!s)return;const n=r.nsArg??0,a=r.keyPrefixArg??1;let o,l;const p=i.arguments?.[0]?.expression,y=i.arguments?.[1]?.expression,u=i.arguments?.[2]?.expression;var f;let c;if("useTranslation"===r.name&&"StringLiteral"===p?.type&&"StringLiteral"===y?.type&&(f=p.value,/^[a-z]{2,3}([-_][A-Za-z0-9-]+)?$/i.test(f)))o=y.value,c=u;else{if(-1!==n){const e=i.arguments?.[n]?.expression;"StringLiteral"===e?.type?o=e.value:"ArrayExpression"===e?.type&&"StringLiteral"===e.elements[0]?.expression?.type&&(o=e.elements[0].expression.value)}c=-1===a?void 0:i.arguments?.[a]?.expression}if("ObjectExpression"===c?.type){const t=e.getObjectPropValue(c,"keyPrefix");l="string"==typeof t?t:void 0}else if("StringLiteral"===c?.type)l=c.value;else if("Identifier"===c?.type)l=this.resolveSimpleStringIdentifier(c.value);else if("TemplateLiteral"===c?.type){const e=c;0===(e.expressions||[]).length&&(l=e.quasis?.[0]?.cooked??void 0)}this.setVarInScope(s,{defaultNs:o,keyPrefix:l})}handleGetFixedTDeclarator(e,t){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const i=e.id.value,r=t.arguments,s=r[1]?.expression,n=r[2]?.expression,a="StringLiteral"===s?.type?s.value:void 0,o="StringLiteral"===n?.type?n.value:void 0;(a||o)&&this.setVarInScope(i,{defaultNs:a,keyPrefix:o})}handleGetFixedTFromVariableDeclarator(e,t,i){if("Identifier"!==e.id.type)return;const r=e.id.value,s=this.getVarFromScope(i);if(!s)return;const n=t.arguments,a=n[1]?.expression,o=n[2]?.expression,l="StringLiteral"===a?.type?a.value:void 0,p="StringLiteral"===o?.type?o.value:void 0,y=l??s.defaultNs,u=p??s.keyPrefix;(y||u)&&this.setVarInScope(r,{defaultNs:y,keyPrefix:u})}};
1
+ 'use strict';
2
+
3
+ var astUtils = require('./ast-utils.js');
4
+
5
+ class ScopeManager {
6
+ scopeStack = [];
7
+ config;
8
+ scope = new Map();
9
+ // Track simple local constants with string literal values to resolve identifier args
10
+ simpleConstants = new Map();
11
+ constructor(config) {
12
+ this.config = config;
13
+ }
14
+ /**
15
+ * Reset per-file scope state.
16
+ *
17
+ * This clears both the scope stack and the legacy scope map. It should be
18
+ * called at the start of processing each file so that scope info does not
19
+ * leak between files.
20
+ */
21
+ reset() {
22
+ this.scopeStack = [];
23
+ this.scope = new Map();
24
+ this.simpleConstants.clear();
25
+ }
26
+ /**
27
+ * Enters a new variable scope by pushing a new scope map onto the stack.
28
+ * Used when entering functions to isolate variable declarations.
29
+ */
30
+ enterScope() {
31
+ this.scopeStack.push(new Map());
32
+ }
33
+ /**
34
+ * Exits the current variable scope by popping the top scope map.
35
+ * Used when leaving functions to clean up variable tracking.
36
+ */
37
+ exitScope() {
38
+ this.scopeStack.pop();
39
+ }
40
+ /**
41
+ * Stores variable information in the current scope.
42
+ * Used to track translation functions and their configuration.
43
+ *
44
+ * @param name - Variable name to store
45
+ * @param info - Scope information about the variable
46
+ */
47
+ setVarInScope(name, info) {
48
+ if (this.scopeStack.length > 0) {
49
+ this.scopeStack[this.scopeStack.length - 1].set(name, info);
50
+ }
51
+ else {
52
+ // No active scope (top-level). Preserve in legacy scope map so lookups work
53
+ // for top-level variables (e.g., const { getFixedT } = useTranslate(...))
54
+ this.scope.set(name, info);
55
+ }
56
+ }
57
+ /**
58
+ * Retrieves variable information from the scope chain.
59
+ * Searches from innermost to outermost scope.
60
+ *
61
+ * @param name - Variable name to look up
62
+ * @returns Scope information if found, undefined otherwise
63
+ */
64
+ getVarFromScope(name) {
65
+ // First check the proper scope stack (this is the primary source of truth)
66
+ for (let i = this.scopeStack.length - 1; i >= 0; i--) {
67
+ if (this.scopeStack[i].has(name)) {
68
+ const scopeInfo = this.scopeStack[i].get(name);
69
+ return scopeInfo;
70
+ }
71
+ }
72
+ // Then check the legacy scope tracking for useTranslation calls (for comment parsing)
73
+ const legacyScope = this.scope.get(name);
74
+ if (legacyScope) {
75
+ return legacyScope;
76
+ }
77
+ return undefined;
78
+ }
79
+ getUseTranslationConfig(name) {
80
+ const useTranslationNames = this.config.extract.useTranslationNames || ['useTranslation'];
81
+ for (const item of useTranslationNames) {
82
+ if (typeof item === 'string' && item === name) {
83
+ // Default behavior for simple string entries
84
+ return { name, nsArg: 0, keyPrefixArg: 1 };
85
+ }
86
+ if (typeof item === 'object' && item.name === name) {
87
+ // Custom configuration with specified or default argument positions
88
+ return {
89
+ name: item.name,
90
+ nsArg: item.nsArg ?? 0,
91
+ keyPrefixArg: item.keyPrefixArg ?? 1,
92
+ };
93
+ }
94
+ }
95
+ return undefined;
96
+ }
97
+ /**
98
+ * Resolve simple identifier declared in-file to its string literal value, if known.
99
+ */
100
+ resolveSimpleStringIdentifier(name) {
101
+ return this.simpleConstants.get(name);
102
+ }
103
+ /**
104
+ * Handles variable declarations that might define translation functions.
105
+ *
106
+ * Processes two patterns:
107
+ * 1. `const { t } = useTranslation(...)` - React i18next pattern
108
+ * 2. `const t = i18next.getFixedT(...)` - Core i18next pattern
109
+ *
110
+ * Extracts namespace and key prefix information for later use.
111
+ *
112
+ * @param node - Variable declarator node to process
113
+ */
114
+ handleVariableDeclarator(node) {
115
+ const init = node.init;
116
+ if (!init)
117
+ return;
118
+ // Record simple const/let string initializers for later resolution
119
+ if (node.id.type === 'Identifier' && init.type === 'StringLiteral') {
120
+ this.simpleConstants.set(node.id.value, init.value);
121
+ // continue processing; still may be a useTranslation/getFixedT call below
122
+ }
123
+ // Determine the actual call expression, looking inside AwaitExpressions.
124
+ const callExpr = init.type === 'AwaitExpression' && init.argument.type === 'CallExpression'
125
+ ? init.argument
126
+ : init.type === 'CallExpression'
127
+ ? init
128
+ : null;
129
+ if (!callExpr)
130
+ return;
131
+ const callee = callExpr.callee;
132
+ // Handle: const { t } = useTranslation(...)
133
+ if (callee.type === 'Identifier') {
134
+ const hookConfig = this.getUseTranslationConfig(callee.value);
135
+ if (hookConfig) {
136
+ this.handleUseTranslationDeclarator(node, callExpr, hookConfig);
137
+ // ALSO store in the legacy scope for comment parsing compatibility
138
+ this.handleUseTranslationForComments(node, callExpr, hookConfig);
139
+ return;
140
+ }
141
+ }
142
+ // Handle: const t = getFixedT(...) where getFixedT is a previously declared variable
143
+ // (e.g., `const { getFixedT } = useTranslate('helloservice')`)
144
+ if (callee.type === 'Identifier') {
145
+ const sourceScope = this.getVarFromScope(callee.value);
146
+ if (sourceScope) {
147
+ // Propagate the source scope (keyPrefix/defaultNs) and augment it with
148
+ // arguments passed to this call (e.g., namespace argument).
149
+ this.handleGetFixedTFromVariableDeclarator(node, callExpr, callee.value);
150
+ return;
151
+ }
152
+ }
153
+ // Handle: const t = i18next.getFixedT(...)
154
+ if (callee.type === 'MemberExpression' &&
155
+ callee.property.type === 'Identifier' &&
156
+ callee.property.value === 'getFixedT') {
157
+ this.handleGetFixedTDeclarator(node, callExpr);
158
+ }
159
+ }
160
+ /**
161
+ * Handles useTranslation calls for comment scope resolution.
162
+ * This is a separate method to store scope info in the legacy scope map
163
+ * that the comment parser can access.
164
+ *
165
+ * @param node - Variable declarator with useTranslation call
166
+ * @param callExpr - The CallExpression node representing the useTranslation invocation
167
+ * @param hookConfig - Configuration describing argument positions for namespace and keyPrefix
168
+ */
169
+ handleUseTranslationForComments(node, callExpr, hookConfig) {
170
+ let variableName;
171
+ // Handle simple assignment: let t = useTranslation()
172
+ if (node.id.type === 'Identifier') {
173
+ variableName = node.id.value;
174
+ }
175
+ // Handle array destructuring: const [t, i18n] = useTranslation()
176
+ if (node.id.type === 'ArrayPattern') {
177
+ const firstElement = node.id.elements[0];
178
+ if (firstElement?.type === 'Identifier') {
179
+ variableName = firstElement.value;
180
+ }
181
+ }
182
+ // Handle object destructuring: const { t } or { t: t1 } = useTranslation()
183
+ if (node.id.type === 'ObjectPattern') {
184
+ for (const prop of node.id.properties) {
185
+ // Support both 't' and 'getFixedT' (and preserve existing behavior for 't').
186
+ if (prop.type === 'AssignmentPatternProperty' && prop.key.type === 'Identifier' && (prop.key.value === 't' || prop.key.value === 'getFixedT')) {
187
+ variableName = prop.key.value;
188
+ break;
189
+ }
190
+ if (prop.type === 'KeyValuePatternProperty' && prop.key.type === 'Identifier' && (prop.key.value === 't' || prop.key.value === 'getFixedT') && prop.value.type === 'Identifier') {
191
+ variableName = prop.value.value;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ // If we couldn't find a `t` function being declared, exit
197
+ if (!variableName)
198
+ return;
199
+ // Position-driven extraction: respect hookConfig positions (nsArg/keyPrefixArg).
200
+ // nsArg === -1 means "no namespace arg"; keyPrefixArg === -1 means "no keyPrefix arg".
201
+ const nsArgIndex = hookConfig.nsArg ?? 0;
202
+ const kpArgIndex = hookConfig.keyPrefixArg ?? 1;
203
+ let defaultNs;
204
+ let keyPrefix;
205
+ // Early detection of react-i18next common form: useTranslation(lng, ns)
206
+ // Only apply for the built-in hook name to avoid interfering with custom hooks.
207
+ const first = callExpr.arguments?.[0]?.expression;
208
+ const second = callExpr.arguments?.[1]?.expression;
209
+ const third = callExpr.arguments?.[2]?.expression;
210
+ const looksLikeLanguage = (s) => /^[a-z]{2,3}([-_][A-Za-z0-9-]+)?$/i.test(s);
211
+ const isBuiltInLngNsForm = hookConfig.name === 'useTranslation' &&
212
+ first?.type === 'StringLiteral' &&
213
+ second?.type === 'StringLiteral' &&
214
+ looksLikeLanguage(first.value);
215
+ let kpArg;
216
+ if (isBuiltInLngNsForm) {
217
+ // treat as useTranslation(lng, ns, [options])
218
+ defaultNs = second.value;
219
+ // prefer third arg as keyPrefix (may be undefined)
220
+ kpArg = third;
221
+ }
222
+ else {
223
+ // Position-driven extraction: respect hookConfig positions (nsArg/keyPrefixArg).
224
+ if (nsArgIndex !== -1) {
225
+ const nsNode = callExpr.arguments?.[nsArgIndex]?.expression;
226
+ if (nsNode?.type === 'StringLiteral') {
227
+ defaultNs = nsNode.value;
228
+ }
229
+ else if (nsNode?.type === 'ArrayExpression' && nsNode.elements[0]?.expression?.type === 'StringLiteral') {
230
+ defaultNs = nsNode.elements[0].expression.value;
231
+ }
232
+ }
233
+ kpArg = kpArgIndex === -1 ? undefined : callExpr.arguments?.[kpArgIndex]?.expression;
234
+ }
235
+ if (kpArg?.type === 'ObjectExpression') {
236
+ const kp = astUtils.getObjectPropValue(kpArg, 'keyPrefix');
237
+ keyPrefix = typeof kp === 'string' ? kp : undefined;
238
+ }
239
+ else if (kpArg?.type === 'StringLiteral') {
240
+ keyPrefix = kpArg.value;
241
+ }
242
+ else if (kpArg?.type === 'Identifier') {
243
+ keyPrefix = this.resolveSimpleStringIdentifier(kpArg.value);
244
+ }
245
+ else if (kpArg?.type === 'TemplateLiteral') {
246
+ const tpl = kpArg;
247
+ if ((tpl.expressions || []).length === 0) {
248
+ keyPrefix = tpl.quasis?.[0]?.cooked ?? undefined;
249
+ }
250
+ }
251
+ // Store in the legacy scope map for comment parsing
252
+ if (defaultNs || keyPrefix) {
253
+ this.scope.set(variableName, { defaultNs, keyPrefix });
254
+ }
255
+ }
256
+ /**
257
+ * Processes useTranslation hook declarations to extract scope information.
258
+ *
259
+ * Handles various destructuring patterns:
260
+ * - `const [t] = useTranslation('ns')` - Array destructuring
261
+ * - `const { t } = useTranslation('ns')` - Object destructuring
262
+ * - `const { t: myT } = useTranslation('ns')` - Aliased destructuring
263
+ *
264
+ * Extracts namespace from the first argument and keyPrefix from options.
265
+ *
266
+ * @param node - Variable declarator with useTranslation call
267
+ * @param callExpr - The CallExpression node representing the useTranslation invocation
268
+ * @param hookConfig - Configuration describing argument positions for namespace and keyPrefix
269
+ */
270
+ handleUseTranslationDeclarator(node, callExpr, hookConfig) {
271
+ let variableName;
272
+ // Handle simple assignment: let t = useTranslation()
273
+ if (node.id.type === 'Identifier') {
274
+ variableName = node.id.value;
275
+ }
276
+ // Handle array destructuring: const [t, i18n] = useTranslation()
277
+ if (node.id.type === 'ArrayPattern') {
278
+ const firstElement = node.id.elements[0];
279
+ if (firstElement?.type === 'Identifier') {
280
+ variableName = firstElement.value;
281
+ }
282
+ }
283
+ // Handle object destructuring: const { t } or { t: t1 } = useTranslation()
284
+ if (node.id.type === 'ObjectPattern') {
285
+ for (const prop of node.id.properties) {
286
+ // Also consider getFixedT so scope info is attached to that identifier
287
+ if (prop.type === 'AssignmentPatternProperty' && prop.key.type === 'Identifier' && (prop.key.value === 't' || prop.key.value === 'getFixedT')) {
288
+ variableName = prop.key.value;
289
+ break;
290
+ }
291
+ if (prop.type === 'KeyValuePatternProperty' && prop.key.type === 'Identifier' && (prop.key.value === 't' || prop.key.value === 'getFixedT') && prop.value.type === 'Identifier') {
292
+ variableName = prop.value.value;
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ // If we couldn't find a `t` function being declared, exit
298
+ if (!variableName)
299
+ return;
300
+ // Position-driven extraction: respect hookConfig positions (nsArg/keyPrefixArg).
301
+ const nsArgIndex = hookConfig.nsArg ?? 0;
302
+ const kpArgIndex = hookConfig.keyPrefixArg ?? 1;
303
+ let defaultNs;
304
+ let keyPrefix;
305
+ // Early detect useTranslation(lng, ns) for built-in hook name only
306
+ const first = callExpr.arguments?.[0]?.expression;
307
+ const second = callExpr.arguments?.[1]?.expression;
308
+ const third = callExpr.arguments?.[2]?.expression;
309
+ const looksLikeLanguage = (s) => /^[a-z]{2,3}([-_][A-Za-z0-9-]+)?$/i.test(s);
310
+ const isBuiltInLngNsForm = hookConfig.name === 'useTranslation' &&
311
+ first?.type === 'StringLiteral' &&
312
+ second?.type === 'StringLiteral' &&
313
+ looksLikeLanguage(first.value);
314
+ let kpArg;
315
+ if (isBuiltInLngNsForm) {
316
+ defaultNs = second.value;
317
+ kpArg = third;
318
+ }
319
+ else {
320
+ if (nsArgIndex !== -1) {
321
+ const nsNode = callExpr.arguments?.[nsArgIndex]?.expression;
322
+ if (nsNode?.type === 'StringLiteral')
323
+ defaultNs = nsNode.value;
324
+ else if (nsNode?.type === 'ArrayExpression' && nsNode.elements[0]?.expression?.type === 'StringLiteral') {
325
+ defaultNs = nsNode.elements[0].expression.value;
326
+ }
327
+ }
328
+ kpArg = kpArgIndex === -1 ? undefined : callExpr.arguments?.[kpArgIndex]?.expression;
329
+ }
330
+ if (kpArg?.type === 'ObjectExpression') {
331
+ const kp = astUtils.getObjectPropValue(kpArg, 'keyPrefix');
332
+ keyPrefix = typeof kp === 'string' ? kp : undefined;
333
+ }
334
+ else if (kpArg?.type === 'StringLiteral') {
335
+ keyPrefix = kpArg.value;
336
+ }
337
+ else if (kpArg?.type === 'Identifier') {
338
+ keyPrefix = this.resolveSimpleStringIdentifier(kpArg.value);
339
+ }
340
+ else if (kpArg?.type === 'TemplateLiteral') {
341
+ const tpl = kpArg;
342
+ if ((tpl.expressions || []).length === 0) {
343
+ keyPrefix = tpl.quasis?.[0]?.cooked ?? undefined;
344
+ }
345
+ }
346
+ // Store the scope info for the declared variable
347
+ this.setVarInScope(variableName, { defaultNs, keyPrefix });
348
+ }
349
+ /**
350
+ * Processes getFixedT function declarations to extract scope information.
351
+ *
352
+ * Handles the pattern: `const t = i18next.getFixedT(lng, ns, keyPrefix)`
353
+ * - Ignores the first argument (language)
354
+ * - Extracts namespace from the second argument
355
+ * - Extracts key prefix from the third argument
356
+ *
357
+ * @param node - Variable declarator with getFixedT call
358
+ * @param callExpr - The CallExpression node representing the getFixedT invocation
359
+ */
360
+ handleGetFixedTDeclarator(node, callExpr) {
361
+ // Ensure we are assigning to a simple variable, e.g., const t = ...
362
+ if (node.id.type !== 'Identifier' || !node.init || node.init.type !== 'CallExpression')
363
+ return;
364
+ const variableName = node.id.value;
365
+ const args = callExpr.arguments;
366
+ // getFixedT(lng, ns, keyPrefix)
367
+ // We ignore the first argument (lng) for key extraction.
368
+ const nsArg = args[1]?.expression;
369
+ const keyPrefixArg = args[2]?.expression;
370
+ const defaultNs = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined;
371
+ const keyPrefix = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined;
372
+ if (defaultNs || keyPrefix) {
373
+ this.setVarInScope(variableName, { defaultNs, keyPrefix });
374
+ }
375
+ }
376
+ /**
377
+ * Handles cases where a getFixedT-like function is a variable (from a custom hook)
378
+ * and is invoked to produce a bound `t` function, e.g.:
379
+ * const { getFixedT } = useTranslate('prefix')
380
+ * const t = getFixedT('en', 'ns')
381
+ *
382
+ * We combine the original source variable's scope (keyPrefix/defaultNs) with
383
+ * any namespace/keyPrefix arguments provided to this call and attach the
384
+ * resulting scope to the newly declared variable.
385
+ */
386
+ handleGetFixedTFromVariableDeclarator(node, callExpr, sourceVarName) {
387
+ if (node.id.type !== 'Identifier')
388
+ return;
389
+ const targetVarName = node.id.value;
390
+ const sourceScope = this.getVarFromScope(sourceVarName);
391
+ if (!sourceScope)
392
+ return;
393
+ const args = callExpr.arguments;
394
+ // getFixedT(lng, ns, keyPrefix)
395
+ const nsArg = args[1]?.expression;
396
+ const keyPrefixArg = args[2]?.expression;
397
+ const nsFromCall = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined;
398
+ const keyPrefixFromCall = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined;
399
+ // Merge: call args take precedence over source scope values
400
+ const finalNs = nsFromCall ?? sourceScope.defaultNs;
401
+ const finalKeyPrefix = keyPrefixFromCall ?? sourceScope.keyPrefix;
402
+ if (finalNs || finalKeyPrefix) {
403
+ this.setVarInScope(targetVarName, { defaultNs: finalNs, keyPrefix: finalKeyPrefix });
404
+ }
405
+ }
406
+ }
407
+
408
+ exports.ScopeManager = ScopeManager;
@@ -1 +1,106 @@
1
- "use strict";exports.createPluginContext=function(t,e,a,n){return{addKey:e=>{const n=!1===e.ns?void 0:e.ns,s=n??a.extract?.defaultNS??"translation",l=void 0===n,o=`${String(s)}:${e.key}`,i=e.defaultValue??e.key,u=t.get(o);if(u){const n=u.defaultValue===u.key||u.hasCount&&u.defaultValue&&u.key.includes("_")&&u.key.startsWith(u.defaultValue),c=i===e.key;e.locations&&(u.locations=[...u.locations||[],...e.locations]),n&&!c&&t.set(o,{...e,ns:s||a.extract?.defaultNS||"translation",nsIsImplicit:l,defaultValue:i,locations:u.locations})}else t.set(o,{...e,ns:s||a.extract?.defaultNS||"translation",nsIsImplicit:l,defaultValue:i})},config:Object.freeze({...a,plugins:[...e]}),logger:n,getVarFromScope:()=>{}}},exports.initializePlugins=async function(t){for(const e of t)await(e.setup?.())};
1
+ 'use strict';
2
+
3
+ /**
4
+ * Initializes an array of plugins by calling their setup hooks.
5
+ * This function should be called before starting the extraction process.
6
+ *
7
+ * @param plugins - Array of plugin objects to initialize
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const plugins = [customPlugin(), anotherPlugin()]
12
+ * await initializePlugins(plugins)
13
+ * // All plugin setup hooks have been called
14
+ * ```
15
+ */
16
+ async function initializePlugins(plugins) {
17
+ for (const plugin of plugins) {
18
+ await plugin.setup?.();
19
+ }
20
+ }
21
+ /**
22
+ * Creates a plugin context object that provides helper methods for plugins.
23
+ * The context allows plugins to add extracted keys to the main collection.
24
+ *
25
+ * @param allKeys - The main map where extracted keys are stored
26
+ * @returns A context object with helper methods for plugins
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const allKeys = new Map()
31
+ * const context = createPluginContext(allKeys)
32
+ *
33
+ * // Plugin can now add keys
34
+ * context.addKey({
35
+ * key: 'my.custom.key',
36
+ * defaultValue: 'Default Value',
37
+ * ns: 'common'
38
+ * })
39
+ * ```
40
+ */
41
+ function createPluginContext(allKeys, plugins, config, logger) {
42
+ const pluginContextConfig = Object.freeze({
43
+ ...config,
44
+ plugins: [...plugins],
45
+ });
46
+ return {
47
+ addKey: (keyInfo) => {
48
+ // Normalize boolean `false` namespace -> undefined (meaning "no explicit ns")
49
+ const explicitNs = keyInfo.ns === false ? undefined : keyInfo.ns;
50
+ // Internally prefer 'translation' as the logical namespace when none was specified.
51
+ // Record whether the namespace was implicit so the output generator can
52
+ // special-case config.extract.defaultNS === false.
53
+ const storedNs = explicitNs ?? (config.extract?.defaultNS ?? 'translation');
54
+ const nsIsImplicit = explicitNs === undefined;
55
+ const nsForKey = String(storedNs);
56
+ const uniqueKey = `${nsForKey}:${keyInfo.key}`;
57
+ const defaultValue = keyInfo.defaultValue ?? keyInfo.key;
58
+ // Check if key already exists
59
+ const existingKey = allKeys.get(uniqueKey);
60
+ if (existingKey) {
61
+ // Check if existing value is a generic fallback
62
+ // For plural keys, the fallback is often the base key (e.g., "item.count" for "item.count_other")
63
+ // For regular keys, the fallback is the key itself
64
+ const isExistingGenericFallback = existingKey.defaultValue === existingKey.key || // Regular key fallback
65
+ (existingKey.hasCount && existingKey.defaultValue &&
66
+ existingKey.key.includes('_') &&
67
+ existingKey.key.startsWith(existingKey.defaultValue)); // Plural key with base key fallback
68
+ const isNewGenericFallback = defaultValue === keyInfo.key;
69
+ // Merge locations
70
+ if (keyInfo.locations) {
71
+ existingKey.locations = [
72
+ ...(existingKey.locations || []),
73
+ ...keyInfo.locations
74
+ ];
75
+ }
76
+ // If existing value is a generic fallback and new value is specific, replace it
77
+ if (isExistingGenericFallback && !isNewGenericFallback) {
78
+ allKeys.set(uniqueKey, {
79
+ ...keyInfo,
80
+ ns: storedNs || config.extract?.defaultNS || 'translation',
81
+ nsIsImplicit,
82
+ defaultValue,
83
+ locations: existingKey.locations // Preserve merged locations
84
+ });
85
+ }
86
+ // Otherwise keep the existing one
87
+ }
88
+ else {
89
+ // New key, just add it
90
+ allKeys.set(uniqueKey, {
91
+ ...keyInfo,
92
+ ns: storedNs || config.extract?.defaultNS || 'translation',
93
+ nsIsImplicit,
94
+ defaultValue
95
+ });
96
+ }
97
+ },
98
+ config: pluginContextConfig,
99
+ logger,
100
+ // This will be attached later, so we provide a placeholder
101
+ getVarFromScope: () => undefined,
102
+ };
103
+ }
104
+
105
+ exports.createPluginContext = createPluginContext;
106
+ exports.initializePlugins = initializePlugins;
@@ -1 +1,99 @@
1
- "use strict";var s=require("glob"),e=require("node:fs/promises"),n=require("node:path");const o=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","app/i18n/locales/dev/*.json","src/i18n/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json","app/i18n/locales/en/*.json","src/i18n/locales/en/*.json","public/locales/en/*.json5","locales/en/*.json5","src/locales/en/*.json5","src/assets/locales/en/*.json5","app/i18n/locales/en/*.json5","src/i18n/locales/en/*.json5","public/locales/en-*/*.json","locales/en-*/*.json","src/locales/en-*/*.json","src/assets/locales/en-*/*.json","app/i18n/locales/en-*/*.json","src/i18n/locales/en-*/*.json"];exports.detectConfig=async function(){for(const l of o){const o=await s.glob(l,{ignore:"node_modules/**"});if(o.length>0){const s=o.find(s=>".json5"===n.extname(s))||o[0],l=n.dirname(n.dirname(s)),c=n.extname(s);let a="json";".ts"===c?a="ts":".js"===c?a="js":".json5"===c&&(a="json5");try{let s=(await e.readdir(l)).filter(s=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(s));if(s.length>0)return s.sort(),s.includes("dev")&&(s=["dev",...s.filter(s=>"dev"!==s)]),s.includes("en")&&(s=["en",...s.filter(s=>"en"!==s)]),{locales:s,extract:{input:["src/**/*.{js,jsx,ts,tsx}","app/**/*.{js,jsx,ts,tsx}","pages/**/*.{js,jsx,ts,tsx}","components/**/*.{js,jsx,ts,tsx}"],output:n.join(l,"{{language}}",`{{namespace}}${c}`),outputFormat:a,primaryLanguage:s.includes("en")?"en":s[0]}}}catch{continue}}}return null};
1
+ 'use strict';
2
+
3
+ var glob = require('glob');
4
+ var promises = require('node:fs/promises');
5
+ var node_path = require('node:path');
6
+
7
+ // A list of common glob patterns for the primary language ('en') or ('dev') translation files.
8
+ const HEURISTIC_PATTERNS = [
9
+ 'public/locales/dev/*.json',
10
+ 'locales/dev/*.json',
11
+ 'src/locales/dev/*.json',
12
+ 'src/assets/locales/dev/*.json',
13
+ 'app/i18n/locales/dev/*.json',
14
+ 'src/i18n/locales/dev/*.json',
15
+ 'public/locales/en/*.json',
16
+ 'locales/en/*.json',
17
+ 'src/locales/en/*.json',
18
+ 'src/assets/locales/en/*.json',
19
+ 'app/i18n/locales/en/*.json',
20
+ 'src/i18n/locales/en/*.json',
21
+ 'public/locales/en/*.json5',
22
+ 'locales/en/*.json5',
23
+ 'src/locales/en/*.json5',
24
+ 'src/assets/locales/en/*.json5',
25
+ 'app/i18n/locales/en/*.json5',
26
+ 'src/i18n/locales/en/*.json5',
27
+ 'public/locales/en-*/*.json',
28
+ 'locales/en-*/*.json',
29
+ 'src/locales/en-*/*.json',
30
+ 'src/assets/locales/en-*/*.json',
31
+ 'app/i18n/locales/en-*/*.json',
32
+ 'src/i18n/locales/en-*/*.json',
33
+ ];
34
+ /**
35
+ * Attempts to automatically detect the project's i18n structure by searching for
36
+ * common translation file locations.
37
+ *
38
+ * @returns A promise that resolves to a partial I18nextToolkitConfig if detection
39
+ * is successful, otherwise null.
40
+ */
41
+ async function detectConfig() {
42
+ for (const pattern of HEURISTIC_PATTERNS) {
43
+ const files = await glob.glob(pattern, { ignore: 'node_modules/**' });
44
+ if (files.length > 0) {
45
+ // Prefer .json5 if present
46
+ const json5File = files.find(f => node_path.extname(f) === '.json5');
47
+ const firstFile = json5File || files[0];
48
+ const basePath = node_path.dirname(node_path.dirname(firstFile));
49
+ const extension = node_path.extname(firstFile);
50
+ // Infer outputFormat from the file extension
51
+ let outputFormat = 'json';
52
+ if (extension === '.ts') {
53
+ outputFormat = 'ts';
54
+ }
55
+ else if (extension === '.js') {
56
+ // We can't know if it's ESM or CJS, so we default to a safe choice.
57
+ // The tool's file loaders can handle both.
58
+ outputFormat = 'js';
59
+ }
60
+ else if (extension === '.json5') {
61
+ outputFormat = 'json5';
62
+ }
63
+ try {
64
+ const allDirs = await promises.readdir(basePath);
65
+ let locales = allDirs.filter(dir => /^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(dir));
66
+ if (locales.length > 0) {
67
+ // Prioritization Logic
68
+ locales.sort();
69
+ if (locales.includes('dev')) {
70
+ locales = ['dev', ...locales.filter(l => l !== 'dev')];
71
+ }
72
+ if (locales.includes('en')) {
73
+ locales = ['en', ...locales.filter(l => l !== 'en')];
74
+ }
75
+ return {
76
+ locales,
77
+ extract: {
78
+ input: [
79
+ 'src/**/*.{js,jsx,ts,tsx}',
80
+ 'app/**/*.{js,jsx,ts,tsx}',
81
+ 'pages/**/*.{js,jsx,ts,tsx}',
82
+ 'components/**/*.{js,jsx,ts,tsx}'
83
+ ],
84
+ output: node_path.join(basePath, '{{language}}', `{{namespace}}${extension}`),
85
+ outputFormat,
86
+ primaryLanguage: locales.includes('en') ? 'en' : locales[0],
87
+ },
88
+ };
89
+ }
90
+ }
91
+ catch {
92
+ continue;
93
+ }
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+
99
+ exports.detectConfig = detectConfig;
package/dist/cjs/index.js CHANGED
@@ -1 +1,28 @@
1
- "use strict";var e=require("./config.js"),r=require("./extractor/core/extractor.js"),t=require("./extractor/core/key-finder.js"),n=require("./extractor/core/translation-manager.js");require("react"),require("react-i18next");var s=require("./linter.js"),o=require("./syncer.js"),a=require("./status.js"),c=require("./types-generator.js"),i=require("./rename-key.js");exports.defineConfig=e.defineConfig,exports.extract=r.extract,exports.runExtractor=r.runExtractor,exports.findKeys=t.findKeys,exports.getTranslations=n.getTranslations,exports.recommendedAcceptedAttributes=s.recommendedAcceptedAttributes,exports.recommendedAcceptedTags=s.recommendedAcceptedTags,exports.runLinter=s.runLinter,exports.runSyncer=o.runSyncer,exports.runStatus=a.runStatus,exports.runTypesGenerator=c.runTypesGenerator,exports.runRenameKey=i.runRenameKey;
1
+ 'use strict';
2
+
3
+ var config = require('./config.js');
4
+ var extractor = require('./extractor/core/extractor.js');
5
+ var keyFinder = require('./extractor/core/key-finder.js');
6
+ var translationManager = require('./extractor/core/translation-manager.js');
7
+ require('react');
8
+ require('react-i18next');
9
+ var linter = require('./linter.js');
10
+ var syncer = require('./syncer.js');
11
+ var status = require('./status.js');
12
+ var typesGenerator = require('./types-generator.js');
13
+ var renameKey = require('./rename-key.js');
14
+
15
+
16
+
17
+ exports.defineConfig = config.defineConfig;
18
+ exports.extract = extractor.extract;
19
+ exports.runExtractor = extractor.runExtractor;
20
+ exports.findKeys = keyFinder.findKeys;
21
+ exports.getTranslations = translationManager.getTranslations;
22
+ exports.recommendedAcceptedAttributes = linter.recommendedAcceptedAttributes;
23
+ exports.recommendedAcceptedTags = linter.recommendedAcceptedTags;
24
+ exports.runLinter = linter.runLinter;
25
+ exports.runSyncer = syncer.runSyncer;
26
+ exports.runStatus = status.runStatus;
27
+ exports.runTypesGenerator = typesGenerator.runTypesGenerator;
28
+ exports.runRenameKey = renameKey.runRenameKey;