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.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +489 -0
- package/dist/cjs/cli.js +2 -0
- package/dist/cjs/config.js +1 -0
- package/dist/cjs/extractor/core/extractor.js +1 -0
- package/dist/cjs/extractor/core/key-finder.js +1 -0
- package/dist/cjs/extractor/core/translation-manager.js +1 -0
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
- package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
- package/dist/cjs/extractor/plugin-manager.js +1 -0
- package/dist/cjs/heuristic-config.js +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/init.js +1 -0
- package/dist/cjs/linter.js +1 -0
- package/dist/cjs/locize.js +1 -0
- package/dist/cjs/migrator.js +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/status.js +1 -0
- package/dist/cjs/syncer.js +1 -0
- package/dist/cjs/types-generator.js +1 -0
- package/dist/cjs/utils/file-utils.js +1 -0
- package/dist/cjs/utils/logger.js +1 -0
- package/dist/cjs/utils/nested-object.js +1 -0
- package/dist/cjs/utils/validation.js +1 -0
- package/dist/esm/cli.js +2 -0
- package/dist/esm/config.js +1 -0
- package/dist/esm/extractor/core/extractor.js +1 -0
- package/dist/esm/extractor/core/key-finder.js +1 -0
- package/dist/esm/extractor/core/translation-manager.js +1 -0
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
- package/dist/esm/extractor/parsers/comment-parser.js +1 -0
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
- package/dist/esm/extractor/plugin-manager.js +1 -0
- package/dist/esm/heuristic-config.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/init.js +1 -0
- package/dist/esm/linter.js +1 -0
- package/dist/esm/locize.js +1 -0
- package/dist/esm/migrator.js +1 -0
- package/dist/esm/status.js +1 -0
- package/dist/esm/syncer.js +1 -0
- package/dist/esm/types-generator.js +1 -0
- package/dist/esm/utils/file-utils.js +1 -0
- package/dist/esm/utils/logger.js +1 -0
- package/dist/esm/utils/nested-object.js +1 -0
- package/dist/esm/utils/validation.js +1 -0
- package/package.json +81 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +158 -0
- package/src/extractor/core/extractor.ts +195 -0
- package/src/extractor/core/key-finder.ts +70 -0
- package/src/extractor/core/translation-manager.ts +115 -0
- package/src/extractor/index.ts +7 -0
- package/src/extractor/parsers/ast-visitors.ts +637 -0
- package/src/extractor/parsers/comment-parser.ts +125 -0
- package/src/extractor/parsers/jsx-parser.ts +166 -0
- package/src/extractor/plugin-manager.ts +54 -0
- package/src/extractor.ts +15 -0
- package/src/heuristic-config.ts +64 -0
- package/src/index.ts +12 -0
- package/src/init.ts +156 -0
- package/src/linter.ts +191 -0
- package/src/locize.ts +251 -0
- package/src/migrator.ts +139 -0
- package/src/status.ts +192 -0
- package/src/syncer.ts +114 -0
- package/src/types-generator.ts +116 -0
- package/src/types.ts +312 -0
- package/src/utils/file-utils.ts +81 -0
- package/src/utils/logger.ts +36 -0
- package/src/utils/nested-object.ts +113 -0
- package/src/utils/validation.ts +69 -0
- package/tryme.js +8 -0
- package/tsconfig.json +71 -0
- package/types/cli.d.ts +3 -0
- package/types/cli.d.ts.map +1 -0
- package/types/config.d.ts +50 -0
- package/types/config.d.ts.map +1 -0
- package/types/extractor/core/extractor.d.ts +66 -0
- package/types/extractor/core/extractor.d.ts.map +1 -0
- package/types/extractor/core/key-finder.d.ts +31 -0
- package/types/extractor/core/key-finder.d.ts.map +1 -0
- package/types/extractor/core/translation-manager.d.ts +31 -0
- package/types/extractor/core/translation-manager.d.ts.map +1 -0
- package/types/extractor/index.d.ts +8 -0
- package/types/extractor/index.d.ts.map +1 -0
- package/types/extractor/parsers/ast-visitors.d.ts +235 -0
- package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
- package/types/extractor/parsers/comment-parser.d.ts +24 -0
- package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
- package/types/extractor/parsers/jsx-parser.d.ts +35 -0
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
- package/types/extractor/plugin-manager.d.ts +37 -0
- package/types/extractor/plugin-manager.d.ts.map +1 -0
- package/types/extractor.d.ts +7 -0
- package/types/extractor.d.ts.map +1 -0
- package/types/heuristic-config.d.ts +10 -0
- package/types/heuristic-config.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/init.d.ts +29 -0
- package/types/init.d.ts.map +1 -0
- package/types/linter.d.ts +33 -0
- package/types/linter.d.ts.map +1 -0
- package/types/locize.d.ts +5 -0
- package/types/locize.d.ts.map +1 -0
- package/types/migrator.d.ts +37 -0
- package/types/migrator.d.ts.map +1 -0
- package/types/status.d.ts +20 -0
- package/types/status.d.ts.map +1 -0
- package/types/syncer.d.ts +33 -0
- package/types/syncer.d.ts.map +1 -0
- package/types/types-generator.d.ts +29 -0
- package/types/types-generator.d.ts.map +1 -0
- package/types/types.d.ts +268 -0
- package/types/types.d.ts.map +1 -0
- package/types/utils/file-utils.d.ts +61 -0
- package/types/utils/file-utils.d.ts.map +1 -0
- package/types/utils/logger.d.ts +34 -0
- package/types/utils/logger.d.ts.map +1 -0
- package/types/utils/nested-object.d.ts +71 -0
- package/types/utils/nested-object.d.ts.map +1 -0
- package/types/utils/validation.d.ts +47 -0
- package/types/utils/validation.d.ts.map +1 -0
- 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
|
+
}
|