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