i18next-cli 0.9.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 (127) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +489 -0
  4. package/dist/cjs/cli.js +2 -0
  5. package/dist/cjs/config.js +1 -0
  6. package/dist/cjs/extractor/core/extractor.js +1 -0
  7. package/dist/cjs/extractor/core/key-finder.js +1 -0
  8. package/dist/cjs/extractor/core/translation-manager.js +1 -0
  9. package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
  10. package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
  11. package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
  12. package/dist/cjs/extractor/plugin-manager.js +1 -0
  13. package/dist/cjs/heuristic-config.js +1 -0
  14. package/dist/cjs/index.js +1 -0
  15. package/dist/cjs/init.js +1 -0
  16. package/dist/cjs/linter.js +1 -0
  17. package/dist/cjs/locize.js +1 -0
  18. package/dist/cjs/migrator.js +1 -0
  19. package/dist/cjs/package.json +1 -0
  20. package/dist/cjs/status.js +1 -0
  21. package/dist/cjs/syncer.js +1 -0
  22. package/dist/cjs/types-generator.js +1 -0
  23. package/dist/cjs/utils/file-utils.js +1 -0
  24. package/dist/cjs/utils/logger.js +1 -0
  25. package/dist/cjs/utils/nested-object.js +1 -0
  26. package/dist/cjs/utils/validation.js +1 -0
  27. package/dist/esm/cli.js +2 -0
  28. package/dist/esm/config.js +1 -0
  29. package/dist/esm/extractor/core/extractor.js +1 -0
  30. package/dist/esm/extractor/core/key-finder.js +1 -0
  31. package/dist/esm/extractor/core/translation-manager.js +1 -0
  32. package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
  33. package/dist/esm/extractor/parsers/comment-parser.js +1 -0
  34. package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
  35. package/dist/esm/extractor/plugin-manager.js +1 -0
  36. package/dist/esm/heuristic-config.js +1 -0
  37. package/dist/esm/index.js +1 -0
  38. package/dist/esm/init.js +1 -0
  39. package/dist/esm/linter.js +1 -0
  40. package/dist/esm/locize.js +1 -0
  41. package/dist/esm/migrator.js +1 -0
  42. package/dist/esm/status.js +1 -0
  43. package/dist/esm/syncer.js +1 -0
  44. package/dist/esm/types-generator.js +1 -0
  45. package/dist/esm/utils/file-utils.js +1 -0
  46. package/dist/esm/utils/logger.js +1 -0
  47. package/dist/esm/utils/nested-object.js +1 -0
  48. package/dist/esm/utils/validation.js +1 -0
  49. package/package.json +81 -0
  50. package/src/cli.ts +166 -0
  51. package/src/config.ts +158 -0
  52. package/src/extractor/core/extractor.ts +195 -0
  53. package/src/extractor/core/key-finder.ts +70 -0
  54. package/src/extractor/core/translation-manager.ts +115 -0
  55. package/src/extractor/index.ts +7 -0
  56. package/src/extractor/parsers/ast-visitors.ts +637 -0
  57. package/src/extractor/parsers/comment-parser.ts +125 -0
  58. package/src/extractor/parsers/jsx-parser.ts +166 -0
  59. package/src/extractor/plugin-manager.ts +54 -0
  60. package/src/extractor.ts +15 -0
  61. package/src/heuristic-config.ts +64 -0
  62. package/src/index.ts +12 -0
  63. package/src/init.ts +156 -0
  64. package/src/linter.ts +191 -0
  65. package/src/locize.ts +251 -0
  66. package/src/migrator.ts +139 -0
  67. package/src/status.ts +192 -0
  68. package/src/syncer.ts +114 -0
  69. package/src/types-generator.ts +116 -0
  70. package/src/types.ts +312 -0
  71. package/src/utils/file-utils.ts +81 -0
  72. package/src/utils/logger.ts +36 -0
  73. package/src/utils/nested-object.ts +113 -0
  74. package/src/utils/validation.ts +69 -0
  75. package/tryme.js +8 -0
  76. package/tsconfig.json +71 -0
  77. package/types/cli.d.ts +3 -0
  78. package/types/cli.d.ts.map +1 -0
  79. package/types/config.d.ts +50 -0
  80. package/types/config.d.ts.map +1 -0
  81. package/types/extractor/core/extractor.d.ts +66 -0
  82. package/types/extractor/core/extractor.d.ts.map +1 -0
  83. package/types/extractor/core/key-finder.d.ts +31 -0
  84. package/types/extractor/core/key-finder.d.ts.map +1 -0
  85. package/types/extractor/core/translation-manager.d.ts +31 -0
  86. package/types/extractor/core/translation-manager.d.ts.map +1 -0
  87. package/types/extractor/index.d.ts +8 -0
  88. package/types/extractor/index.d.ts.map +1 -0
  89. package/types/extractor/parsers/ast-visitors.d.ts +235 -0
  90. package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
  91. package/types/extractor/parsers/comment-parser.d.ts +24 -0
  92. package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
  93. package/types/extractor/parsers/jsx-parser.d.ts +35 -0
  94. package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
  95. package/types/extractor/plugin-manager.d.ts +37 -0
  96. package/types/extractor/plugin-manager.d.ts.map +1 -0
  97. package/types/extractor.d.ts +7 -0
  98. package/types/extractor.d.ts.map +1 -0
  99. package/types/heuristic-config.d.ts +10 -0
  100. package/types/heuristic-config.d.ts.map +1 -0
  101. package/types/index.d.ts +4 -0
  102. package/types/index.d.ts.map +1 -0
  103. package/types/init.d.ts +29 -0
  104. package/types/init.d.ts.map +1 -0
  105. package/types/linter.d.ts +33 -0
  106. package/types/linter.d.ts.map +1 -0
  107. package/types/locize.d.ts +5 -0
  108. package/types/locize.d.ts.map +1 -0
  109. package/types/migrator.d.ts +37 -0
  110. package/types/migrator.d.ts.map +1 -0
  111. package/types/status.d.ts +20 -0
  112. package/types/status.d.ts.map +1 -0
  113. package/types/syncer.d.ts +33 -0
  114. package/types/syncer.d.ts.map +1 -0
  115. package/types/types-generator.d.ts +29 -0
  116. package/types/types-generator.d.ts.map +1 -0
  117. package/types/types.d.ts +268 -0
  118. package/types/types.d.ts.map +1 -0
  119. package/types/utils/file-utils.d.ts +61 -0
  120. package/types/utils/file-utils.d.ts.map +1 -0
  121. package/types/utils/logger.d.ts +34 -0
  122. package/types/utils/logger.d.ts.map +1 -0
  123. package/types/utils/nested-object.d.ts +71 -0
  124. package/types/utils/nested-object.d.ts.map +1 -0
  125. package/types/utils/validation.d.ts +47 -0
  126. package/types/utils/validation.d.ts.map +1 -0
  127. package/vitest.config.ts +13 -0
@@ -0,0 +1,637 @@
1
+ import type { Module, Node, CallExpression, VariableDeclarator, JSXElement, ArrowFunctionExpression, ObjectExpression } from '@swc/core'
2
+ import type { PluginContext, I18nextToolkitConfig, Logger } from '../../types'
3
+ import { extractFromTransComponent } from './jsx-parser'
4
+
5
+ /**
6
+ * Represents variable scope information tracked during AST traversal.
7
+ * Used to maintain context about translation functions and their configuration.
8
+ */
9
+ interface ScopeInfo {
10
+ /** Default namespace for translation calls in this scope */
11
+ defaultNs?: string;
12
+ /** Key prefix to prepend to all translation keys in this scope */
13
+ keyPrefix?: string;
14
+ }
15
+
16
+ /**
17
+ * AST visitor class that traverses JavaScript/TypeScript syntax trees to extract translation keys.
18
+ *
19
+ * This class implements a manual recursive walker that:
20
+ * - Maintains scope information for tracking useTranslation and getFixedT calls
21
+ * - Extracts keys from t() function calls with various argument patterns
22
+ * - Handles JSX Trans components with complex children serialization
23
+ * - Supports both string literals and selector API for type-safe keys
24
+ * - Processes pluralization and context variants
25
+ * - Manages namespace resolution from multiple sources
26
+ *
27
+ * The visitor respects configuration options for separators, function names,
28
+ * component names, and other extraction settings.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const visitors = new ASTVisitors(config, pluginContext, logger)
33
+ * visitors.visit(parsedAST)
34
+ *
35
+ * // The pluginContext will now contain all extracted keys
36
+ * ```
37
+ */
38
+ export class ASTVisitors {
39
+ private readonly pluginContext: PluginContext
40
+ private readonly config: I18nextToolkitConfig
41
+ private readonly logger: Logger
42
+ private scopeStack: Array<Map<string, ScopeInfo>> = []
43
+
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 (
52
+ config: I18nextToolkitConfig,
53
+ pluginContext: PluginContext,
54
+ logger: Logger
55
+ ) {
56
+ this.pluginContext = pluginContext
57
+ this.config = config
58
+ this.logger = logger
59
+ }
60
+
61
+ /**
62
+ * Main entry point for AST traversal.
63
+ * Creates a root scope and begins the recursive walk through the syntax tree.
64
+ *
65
+ * @param node - The root module node to traverse
66
+ */
67
+ public visit (node: Module): void {
68
+ this.enterScope() // Create the root scope for the file
69
+ this.walk(node)
70
+ this.exitScope() // Clean up the root scope
71
+ }
72
+
73
+ /**
74
+ * Recursively walks through AST nodes, handling scoping and visiting logic.
75
+ *
76
+ * This is the core traversal method that:
77
+ * 1. Manages function scopes (enter/exit)
78
+ * 2. Dispatches to specific handlers based on node type
79
+ * 3. Recursively processes child nodes
80
+ * 4. Maintains proper scope cleanup
81
+ *
82
+ * @param node - The current AST node to process
83
+ *
84
+ * @private
85
+ */
86
+ private walk (node: Node | any): void {
87
+ if (!node) return
88
+
89
+ let isNewScope = false
90
+ // ENTER SCOPE for functions
91
+ if (node.type === 'Function' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
92
+ this.enterScope()
93
+ isNewScope = true
94
+ }
95
+
96
+ // --- VISIT LOGIC ---
97
+ // Handle specific node types
98
+ switch (node.type) {
99
+ case 'VariableDeclarator':
100
+ this.handleVariableDeclarator(node)
101
+ break
102
+ case 'CallExpression':
103
+ this.handleCallExpression(node)
104
+ break
105
+ case 'JSXElement':
106
+ this.handleJSXElement(node)
107
+ break
108
+ }
109
+ // --- END VISIT LOGIC ---
110
+
111
+ // --- RECURSION ---
112
+ // Recurse into the children of the current node
113
+ for (const key in node) {
114
+ if (key === 'span') continue
115
+
116
+ const child = node[key]
117
+ if (Array.isArray(child)) {
118
+ for (const item of child) {
119
+ if (item && typeof item === 'object' && item.type) {
120
+ this.walk(item)
121
+ }
122
+ }
123
+ } else if (child && child.type) {
124
+ this.walk(child)
125
+ }
126
+ }
127
+ // --- END RECURSION ---
128
+
129
+ // LEAVE SCOPE for functions
130
+ if (isNewScope) {
131
+ this.exitScope()
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Enters a new variable scope by pushing a new scope map onto the stack.
137
+ * Used when entering functions to isolate variable declarations.
138
+ *
139
+ * @private
140
+ */
141
+ private enterScope (): void {
142
+ this.scopeStack.push(new Map())
143
+ }
144
+
145
+ /**
146
+ * Exits the current variable scope by popping the top scope map.
147
+ * Used when leaving functions to clean up variable tracking.
148
+ *
149
+ * @private
150
+ */
151
+ private exitScope (): void {
152
+ this.scopeStack.pop()
153
+ }
154
+
155
+ /**
156
+ * Stores variable information in the current scope.
157
+ * Used to track translation functions and their configuration.
158
+ *
159
+ * @param name - Variable name to store
160
+ * @param info - Scope information about the variable
161
+ *
162
+ * @private
163
+ */
164
+ private setVarInScope (name: string, info: ScopeInfo): void {
165
+ if (this.scopeStack.length > 0) {
166
+ this.scopeStack[this.scopeStack.length - 1].set(name, info)
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Retrieves variable information from the scope chain.
172
+ * Searches from innermost to outermost scope.
173
+ *
174
+ * @param name - Variable name to look up
175
+ * @returns Scope information if found, undefined otherwise
176
+ *
177
+ * @private
178
+ */
179
+ private getVarFromScope (name: string): ScopeInfo | undefined {
180
+ for (let i = this.scopeStack.length - 1; i >= 0; i--) {
181
+ if (this.scopeStack[i].has(name)) {
182
+ return this.scopeStack[i].get(name)
183
+ }
184
+ }
185
+ return undefined
186
+ }
187
+
188
+ /**
189
+ * Handles variable declarations that might define translation functions.
190
+ *
191
+ * Processes two patterns:
192
+ * 1. `const { t } = useTranslation(...)` - React i18next pattern
193
+ * 2. `const t = i18next.getFixedT(...)` - Core i18next pattern
194
+ *
195
+ * Extracts namespace and key prefix information for later use.
196
+ *
197
+ * @param node - Variable declarator node to process
198
+ *
199
+ * @private
200
+ */
201
+ private handleVariableDeclarator (node: VariableDeclarator): void {
202
+ if (node.init?.type !== 'CallExpression') return
203
+
204
+ const callee = node.init.callee
205
+
206
+ // Handle: const { t } = useTranslation(...)
207
+ if (callee.type === 'Identifier' && (this.config.extract.useTranslationNames || ['useTranslation']).indexOf(callee.value) > -1) {
208
+ this.handleUseTranslationDeclarator(node)
209
+ return
210
+ }
211
+
212
+ // Handle: const t = i18next.getFixedT(...)
213
+ if (
214
+ callee.type === 'MemberExpression' &&
215
+ callee.property.type === 'Identifier' &&
216
+ callee.property.value === 'getFixedT'
217
+ ) {
218
+ this.handleGetFixedTDeclarator(node)
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Processes useTranslation hook declarations to extract scope information.
224
+ *
225
+ * Handles various destructuring patterns:
226
+ * - `const [t] = useTranslation('ns')` - Array destructuring
227
+ * - `const { t } = useTranslation('ns')` - Object destructuring
228
+ * - `const { t: myT } = useTranslation('ns')` - Aliased destructuring
229
+ *
230
+ * Extracts namespace from the first argument and keyPrefix from options.
231
+ *
232
+ * @param node - Variable declarator with useTranslation call
233
+ *
234
+ * @private
235
+ */
236
+ private handleUseTranslationDeclarator (node: VariableDeclarator): void {
237
+ if (!node.init || node.init.type !== 'CallExpression') return
238
+
239
+ let variableName: string | undefined
240
+
241
+ // Handle array destructuring: const [t, i18n] = useTranslation()
242
+ if (node.id.type === 'ArrayPattern') {
243
+ const firstElement = node.id.elements[0]
244
+ if (firstElement?.type === 'Identifier') {
245
+ variableName = firstElement.value
246
+ }
247
+ }
248
+
249
+ // Handle object destructuring: const { t } or { t: t1 } = useTranslation()
250
+ if (node.id.type === 'ObjectPattern') {
251
+ for (const prop of node.id.properties) {
252
+ if (prop.type === 'AssignmentPatternProperty' && prop.key.type === 'Identifier' && prop.key.value === 't') {
253
+ variableName = 't'
254
+ break
255
+ }
256
+ if (prop.type === 'KeyValuePatternProperty' && prop.key.type === 'Identifier' && prop.key.value === 't' && prop.value.type === 'Identifier') {
257
+ variableName = prop.value.value
258
+ break
259
+ }
260
+ }
261
+ }
262
+
263
+ // If we couldn't find a `t` function being declared, exit
264
+ if (!variableName) return
265
+
266
+ // Find the namespace and keyPrefix from the useTranslation call arguments
267
+ const nsArg = node.init.arguments?.[0]?.expression
268
+ let defaultNs: string | undefined
269
+ if (nsArg?.type === 'StringLiteral') {
270
+ defaultNs = nsArg.value
271
+ } else if (nsArg?.type === 'ArrayExpression' && nsArg.elements[0]?.expression.type === 'StringLiteral') {
272
+ defaultNs = nsArg.elements[0].expression.value
273
+ }
274
+
275
+ const optionsArg = node.init.arguments?.[1]?.expression
276
+ let keyPrefix: string | undefined
277
+ if (optionsArg?.type === 'ObjectExpression') {
278
+ keyPrefix = this.getObjectPropValue(optionsArg, 'keyPrefix')
279
+ }
280
+
281
+ // Store the scope info for the declared variable
282
+ this.setVarInScope(variableName, { defaultNs, keyPrefix })
283
+ }
284
+
285
+ /**
286
+ * Processes getFixedT function declarations to extract scope information.
287
+ *
288
+ * Handles the pattern: `const t = i18next.getFixedT(lng, ns, keyPrefix)`
289
+ * - Ignores the first argument (language)
290
+ * - Extracts namespace from the second argument
291
+ * - Extracts key prefix from the third argument
292
+ *
293
+ * @param node - Variable declarator with getFixedT call
294
+ *
295
+ * @private
296
+ */
297
+ private handleGetFixedTDeclarator (node: VariableDeclarator): void {
298
+ // Ensure we are assigning to a simple variable, e.g., const t = ...
299
+ if (node.id.type !== 'Identifier' || !node.init || node.init.type !== 'CallExpression') return
300
+
301
+ const variableName = node.id.value
302
+ const args = node.init.arguments
303
+
304
+ // getFixedT(lng, ns, keyPrefix)
305
+ // We ignore the first argument (lng) for key extraction.
306
+ const nsArg = args[1]?.expression
307
+ const keyPrefixArg = args[2]?.expression
308
+
309
+ const defaultNs = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined
310
+ const keyPrefix = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined
311
+
312
+ if (defaultNs || keyPrefix) {
313
+ this.setVarInScope(variableName, { defaultNs, keyPrefix })
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Processes function call expressions to extract translation keys.
319
+ *
320
+ * This is the core extraction method that handles:
321
+ * - Standard t() calls with string literals
322
+ * - Selector API calls with arrow functions: `t($ => $.path.to.key)`
323
+ * - Namespace resolution from multiple sources
324
+ * - Default value extraction from various argument patterns
325
+ * - Pluralization and context handling
326
+ * - Key prefix application from scope
327
+ *
328
+ * @param node - Call expression node to process
329
+ *
330
+ * @private
331
+ */
332
+ private handleCallExpression (node: CallExpression): void {
333
+ const callee = node.callee
334
+ if (callee.type !== 'Identifier') return
335
+
336
+ const isConfiguredFunction = (this.config.extract.functions || []).includes(callee.value)
337
+ const scopeInfo = this.getVarFromScope(callee.value)
338
+ const isScopedFunction = scopeInfo !== undefined
339
+
340
+ if (!isConfiguredFunction && !isScopedFunction) return
341
+ if (node.arguments.length === 0) return
342
+
343
+ const firstArg = node.arguments[0].expression
344
+ let key: string | null = null
345
+
346
+ if (firstArg.type === 'StringLiteral') {
347
+ key = firstArg.value
348
+ } else if (firstArg.type === 'ArrowFunctionExpression') {
349
+ key = this.extractKeyFromSelector(firstArg)
350
+ }
351
+
352
+ if (!key) return // Could not statically extract a key
353
+
354
+ let ns: string | undefined
355
+ let finalKey = key
356
+ const options = node.arguments.length > 1 ? node.arguments[1].expression : undefined
357
+
358
+ // Determine namespace (explicit ns > scope ns > ns:key > default)
359
+ if (options?.type === 'ObjectExpression') ns = this.getObjectPropValue(options, 'ns')
360
+ if (!ns && scopeInfo?.defaultNs) ns = scopeInfo.defaultNs
361
+
362
+ const nsSeparator = this.config.extract.nsSeparator ?? ':'
363
+ const contextSeparator = this.config.extract.contextSeparator ?? '_'
364
+ if (!ns && nsSeparator && key.includes(nsSeparator)) {
365
+ const parts = key.split(nsSeparator)
366
+ ns = parts.shift()
367
+ key = parts.join(nsSeparator)
368
+ finalKey = key
369
+ }
370
+ if (!ns) ns = this.config.extract.defaultNS
371
+
372
+ // Prepend keyPrefix from scope if it exists
373
+ if (scopeInfo?.keyPrefix) {
374
+ const keySeparator = this.config.extract.keySeparator ?? '.'
375
+ finalKey = `${scopeInfo.keyPrefix}${keySeparator}${key}`
376
+ }
377
+
378
+ // For selectors, defaultValue is the key. For strings, parse it.
379
+ const defaultValue = (firstArg.type === 'StringLiteral') ? this.getDefaultValue(node, key) : key
380
+
381
+ // Plural/Context logic
382
+ if (options?.type === 'ObjectExpression') {
383
+ const contextValue = this.getObjectPropValue(options, 'context')
384
+ if (contextValue) {
385
+ this.pluginContext.addKey({ key: `${finalKey}${contextSeparator}${contextValue}`, ns, defaultValue })
386
+ return
387
+ }
388
+ if (this.getObjectPropValue(options, 'count') !== undefined) {
389
+ this.handlePluralKeys(finalKey, defaultValue, ns)
390
+ return
391
+ }
392
+ }
393
+
394
+ // Standard key
395
+ this.pluginContext.addKey({ key: finalKey, ns, defaultValue })
396
+ }
397
+
398
+ /**
399
+ * Generates plural form keys based on the primary language's plural rules.
400
+ *
401
+ * Uses Intl.PluralRules to determine the correct plural categories
402
+ * for the configured primary language and generates suffixed keys
403
+ * for each category (e.g., 'item_one', 'item_other').
404
+ *
405
+ * @param key - Base key name for pluralization
406
+ * @param defaultValue - Default value to use for all plural forms
407
+ * @param ns - Namespace for the keys
408
+ *
409
+ * @private
410
+ */
411
+ private handlePluralKeys (key: string, defaultValue: string | undefined, ns: string | undefined): void {
412
+ try {
413
+ const pluralCategories = new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories
414
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
415
+
416
+ for (const category of pluralCategories) {
417
+ this.pluginContext.addKey({
418
+ key: `${key}${pluralSeparator}${category}`,
419
+ ns,
420
+ defaultValue,
421
+ hasCount: true
422
+ })
423
+ }
424
+ } catch (e) {
425
+ this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`)
426
+ this.pluginContext.addKey({ key, defaultValue, ns })
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Extracts default value from translation function call arguments.
432
+ *
433
+ * Supports multiple patterns:
434
+ * - String as second argument: `t('key', 'Default')`
435
+ * - Object with defaultValue: `t('key', { defaultValue: 'Default' })`
436
+ * - Falls back to the key itself if no default found
437
+ *
438
+ * @param node - Call expression node
439
+ * @param fallback - Fallback value if no default found
440
+ * @returns Extracted default value
441
+ *
442
+ * @private
443
+ */
444
+ private getDefaultValue (node: CallExpression, fallback: string): string {
445
+ if (node.arguments.length <= 1) return fallback
446
+
447
+ const secondArg = node.arguments[1].expression
448
+
449
+ if (secondArg.type === 'StringLiteral') {
450
+ return secondArg.value || fallback
451
+ }
452
+
453
+ if (secondArg.type === 'ObjectExpression') {
454
+ return this.getObjectPropValue(secondArg, 'defaultValue') || fallback
455
+ }
456
+
457
+ return fallback
458
+ }
459
+
460
+ /**
461
+ * Processes JSX elements to extract translation keys from Trans components.
462
+ *
463
+ * Identifies configured Trans components and delegates to the JSX parser
464
+ * for complex children serialization and attribute extraction.
465
+ *
466
+ * @param node - JSX element node to process
467
+ *
468
+ * @private
469
+ */
470
+ private handleJSXElement (node: JSXElement): void {
471
+ const elementName = this.getElementName(node)
472
+
473
+ if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
474
+ const extractedKey = extractFromTransComponent(node, this.config)
475
+ if (extractedKey) {
476
+ // If ns is not explicitly set on the component, try to find it from the `t` prop
477
+ if (!extractedKey.ns) {
478
+ const tProp = node.opening.attributes?.find(
479
+ attr =>
480
+ attr.type === 'JSXAttribute' &&
481
+ attr.name.type === 'Identifier' &&
482
+ attr.name.value === 't'
483
+ )
484
+
485
+ // Check if the prop value is an identifier (e.g., t={t})
486
+ if (
487
+ tProp?.type === 'JSXAttribute' &&
488
+ tProp.value?.type === 'JSXExpressionContainer' &&
489
+ tProp.value.expression.type === 'Identifier'
490
+ ) {
491
+ const tIdentifier = tProp.value.expression.value
492
+ const scopeInfo = this.getVarFromScope(tIdentifier)
493
+ if (scopeInfo?.defaultNs) {
494
+ extractedKey.ns = scopeInfo.defaultNs
495
+ }
496
+ }
497
+ }
498
+
499
+ // Apply defaultNS from config if no namespace was found on the component
500
+ if (!extractedKey.ns) {
501
+ extractedKey.ns = this.config.extract.defaultNS
502
+ }
503
+
504
+ // If the component has a `count` prop, use the plural handler
505
+ if (extractedKey.hasCount) {
506
+ this.handlePluralKeys(extractedKey.key, extractedKey.defaultValue, extractedKey.ns)
507
+ } else {
508
+ // Otherwise, add the key as-is
509
+ this.pluginContext.addKey(extractedKey)
510
+ }
511
+ // The duplicated addKey call has been removed.
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Extracts element name from JSX opening tag.
518
+ *
519
+ * Handles both simple identifiers and member expressions:
520
+ * - `<Trans>` → 'Trans'
521
+ * - `<React.Trans>` → 'React.Trans'
522
+ *
523
+ * @param node - JSX element node
524
+ * @returns Element name or undefined if not extractable
525
+ *
526
+ * @private
527
+ */
528
+ private getElementName (node: JSXElement): string | undefined {
529
+ if (node.opening.name.type === 'Identifier') {
530
+ return node.opening.name.value
531
+ } else if (node.opening.name.type === 'JSXMemberExpression') {
532
+ let curr: any = node.opening.name
533
+ const names: string[] = []
534
+ while (curr.type === 'JSXMemberExpression') {
535
+ if (curr.property.type === 'Identifier') names.unshift(curr.property.value)
536
+ curr = curr.object
537
+ }
538
+ if (curr.type === 'Identifier') names.unshift(curr.value)
539
+ return names.join('.')
540
+ }
541
+ return undefined
542
+ }
543
+
544
+ /**
545
+ * Extracts string value from object property.
546
+ *
547
+ * Looks for properties by name and returns their string values.
548
+ * Used for extracting options like 'ns', 'defaultValue', 'context', etc.
549
+ *
550
+ * @param object - Object expression to search
551
+ * @param propName - Property name to find
552
+ * @returns String value if found, empty string if property exists but isn't a string, undefined if not found
553
+ *
554
+ * @private
555
+ */
556
+ private getObjectPropValue (object: ObjectExpression, propName: string): string | undefined {
557
+ const prop = (object.properties).find(
558
+ (p) =>
559
+ p.type === 'KeyValueProperty' &&
560
+ (
561
+ (p.key?.type === 'Identifier' && p.key.value === propName) ||
562
+ (p.key?.type === 'StringLiteral' && p.key.value === propName)
563
+ )
564
+ )
565
+
566
+ if (prop?.type === 'KeyValueProperty') {
567
+ const val = prop.value
568
+ // Only return the value if it's a string, otherwise we just care that it exists (for `count`)
569
+ if (val.type === 'StringLiteral') {
570
+ return val.value
571
+ }
572
+ // For properties like `count`, the value could be a number, but we just need to know it's there.
573
+ // So we return a non-undefined value. An empty string is fine.
574
+ return ''
575
+ }
576
+ return undefined
577
+ }
578
+
579
+ /**
580
+ * Extracts translation key from selector API arrow function.
581
+ *
582
+ * Processes selector expressions like:
583
+ * - `$ => $.path.to.key` → 'path.to.key'
584
+ * - `$ => $.app['title'].main` → 'app.title.main'
585
+ * - `$ => { return $.nested.key; }` → 'nested.key'
586
+ *
587
+ * Handles both dot notation and bracket notation, respecting
588
+ * the configured key separator or flat key structure.
589
+ *
590
+ * @param node - Arrow function expression from selector call
591
+ * @returns Extracted key path or null if not statically analyzable
592
+ *
593
+ * @private
594
+ */
595
+ private extractKeyFromSelector (node: ArrowFunctionExpression): string | null {
596
+ let body = node.body
597
+
598
+ // Handle block bodies, e.g., $ => { return $.key; }
599
+ if (body.type === 'BlockStatement') {
600
+ const returnStmt = body.stmts.find(s => s.type === 'ReturnStatement')
601
+ if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
602
+ body = returnStmt.argument
603
+ } else {
604
+ return null
605
+ }
606
+ }
607
+
608
+ let current = body
609
+ const parts: string[] = []
610
+
611
+ // Recursively walk down MemberExpressions
612
+ while (current.type === 'MemberExpression') {
613
+ const prop = current.property
614
+
615
+ if (prop.type === 'Identifier') {
616
+ // This handles dot notation: .key
617
+ parts.unshift(prop.value)
618
+ } else if (prop.type === 'Computed' && prop.expression.type === 'StringLiteral') {
619
+ // This handles bracket notation: ['key']
620
+ parts.unshift(prop.expression.value)
621
+ } else {
622
+ // This is a dynamic property like [myVar] or a private name, which we cannot resolve.
623
+ return null
624
+ }
625
+
626
+ current = current.object
627
+ }
628
+
629
+ if (parts.length > 0) {
630
+ const keySeparator = this.config.extract.keySeparator
631
+ const joiner = typeof keySeparator === 'string' ? keySeparator : '.'
632
+ return parts.join(joiner)
633
+ }
634
+
635
+ return null
636
+ }
637
+ }