i18next-cli 1.10.1 → 1.10.2
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 +8 -0
- package/README.md +3 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/ast-visitors.js +1 -0
- package/dist/cjs/extractor/core/key-finder.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +1 -1
- package/dist/cjs/extractor/parsers/call-expression-handler.js +1 -0
- package/dist/cjs/extractor/parsers/expression-resolver.js +1 -0
- package/dist/cjs/extractor/parsers/jsx-handler.js +1 -0
- package/dist/cjs/extractor/parsers/scope-manager.js +1 -0
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/ast-visitors.js +1 -0
- package/dist/esm/extractor/core/key-finder.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +1 -1
- package/dist/esm/extractor/parsers/call-expression-handler.js +1 -0
- package/dist/esm/extractor/parsers/expression-resolver.js +1 -0
- package/dist/esm/extractor/parsers/jsx-handler.js +1 -0
- package/dist/esm/extractor/parsers/scope-manager.js +1 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/extractor/core/ast-visitors.ts +170 -0
- package/src/extractor/core/extractor.ts +1 -1
- package/src/extractor/core/key-finder.ts +2 -2
- package/src/extractor/core/translation-manager.ts +88 -8
- package/src/extractor/index.ts +1 -1
- package/src/extractor/parsers/call-expression-handler.ts +506 -0
- package/src/extractor/parsers/expression-resolver.ts +178 -0
- package/src/extractor/parsers/jsx-handler.ts +358 -0
- package/src/extractor/parsers/scope-manager.ts +327 -0
- package/src/extractor.ts +1 -1
- package/src/types.ts +82 -0
- package/types/extractor/core/ast-visitors.d.ts +75 -0
- package/types/extractor/core/ast-visitors.d.ts.map +1 -0
- package/types/extractor/core/extractor.d.ts +1 -1
- package/types/extractor/core/extractor.d.ts.map +1 -1
- package/types/extractor/core/key-finder.d.ts.map +1 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/extractor/index.d.ts +1 -1
- package/types/extractor/index.d.ts.map +1 -1
- package/types/extractor/parsers/call-expression-handler.d.ts +74 -0
- package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -0
- package/types/extractor/parsers/expression-resolver.d.ts +62 -0
- package/types/extractor/parsers/expression-resolver.d.ts.map +1 -0
- package/types/extractor/parsers/jsx-handler.d.ts +44 -0
- package/types/extractor/parsers/jsx-handler.d.ts.map +1 -0
- package/types/extractor/parsers/scope-manager.d.ts +99 -0
- package/types/extractor/parsers/scope-manager.d.ts.map +1 -0
- package/types/extractor.d.ts +1 -1
- package/types/extractor.d.ts.map +1 -1
- package/types/types.d.ts +77 -0
- package/types/types.d.ts.map +1 -1
- package/dist/cjs/extractor/parsers/ast-visitors.js +0 -1
- package/dist/esm/extractor/parsers/ast-visitors.js +0 -1
- package/src/extractor/parsers/ast-visitors.ts +0 -1510
- package/types/extractor/parsers/ast-visitors.d.ts +0 -352
- package/types/extractor/parsers/ast-visitors.d.ts.map +0 -1
|
@@ -1,1510 +0,0 @@
|
|
|
1
|
-
import type { Module, Node, CallExpression, VariableDeclarator, JSXElement, ArrowFunctionExpression, ObjectExpression, Expression, TemplateLiteral, TsType, TsTemplateLiteralType } from '@swc/core'
|
|
2
|
-
import type { PluginContext, I18nextToolkitConfig, Logger, ExtractedKey, ScopeInfo } from '../../types'
|
|
3
|
-
import { extractFromTransComponent } from './jsx-parser'
|
|
4
|
-
import { getObjectProperty, getObjectPropValue } from './ast-utils'
|
|
5
|
-
|
|
6
|
-
interface UseTranslationHookConfig {
|
|
7
|
-
name: string;
|
|
8
|
-
nsArg: number;
|
|
9
|
-
keyPrefixArg: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ASTVisitorHooks {
|
|
13
|
-
onBeforeVisitNode?: (node: Node) => void
|
|
14
|
-
onAfterVisitNode?: (node: Node) => void
|
|
15
|
-
resolvePossibleContextStringValues?: (expression: Expression, returnEmptyStrings?: boolean) => string[]
|
|
16
|
-
resolvePossibleKeyStringValues?: (expression: Expression, returnEmptyStrings?: boolean) => string[]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* AST visitor class that traverses JavaScript/TypeScript syntax trees to extract translation keys.
|
|
21
|
-
*
|
|
22
|
-
* This class implements a manual recursive walker that:
|
|
23
|
-
* - Maintains scope information for tracking useTranslation and getFixedT calls
|
|
24
|
-
* - Extracts keys from t() function calls with various argument patterns
|
|
25
|
-
* - Handles JSX Trans components with complex children serialization
|
|
26
|
-
* - Supports both string literals and selector API for type-safe keys
|
|
27
|
-
* - Processes pluralization and context variants
|
|
28
|
-
* - Manages namespace resolution from multiple sources
|
|
29
|
-
*
|
|
30
|
-
* The visitor respects configuration options for separators, function names,
|
|
31
|
-
* component names, and other extraction settings.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* const visitors = new ASTVisitors(config, pluginContext, logger)
|
|
36
|
-
* visitors.visit(parsedAST)
|
|
37
|
-
*
|
|
38
|
-
* // The pluginContext will now contain all extracted keys
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
export class ASTVisitors {
|
|
42
|
-
private readonly pluginContext: PluginContext
|
|
43
|
-
private readonly config: Omit<I18nextToolkitConfig, 'plugins'>
|
|
44
|
-
private readonly logger: Logger
|
|
45
|
-
private scopeStack: Array<Map<string, ScopeInfo>> = []
|
|
46
|
-
private hooks: ASTVisitorHooks
|
|
47
|
-
|
|
48
|
-
public objectKeys = new Set<string>()
|
|
49
|
-
|
|
50
|
-
private scope: Map<string, { defaultNs?: string; keyPrefix?: string }> = new Map()
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Creates a new AST visitor instance.
|
|
54
|
-
*
|
|
55
|
-
* @param config - Toolkit configuration with extraction settings
|
|
56
|
-
* @param pluginContext - Context for adding discovered translation keys
|
|
57
|
-
* @param logger - Logger for warnings and debug information
|
|
58
|
-
*/
|
|
59
|
-
constructor (
|
|
60
|
-
config: Omit<I18nextToolkitConfig, 'plugins'>,
|
|
61
|
-
pluginContext: PluginContext,
|
|
62
|
-
logger: Logger,
|
|
63
|
-
hooks?: ASTVisitorHooks
|
|
64
|
-
) {
|
|
65
|
-
this.pluginContext = pluginContext
|
|
66
|
-
this.config = config
|
|
67
|
-
this.logger = logger
|
|
68
|
-
this.hooks = {
|
|
69
|
-
onBeforeVisitNode: hooks?.onBeforeVisitNode,
|
|
70
|
-
onAfterVisitNode: hooks?.onAfterVisitNode,
|
|
71
|
-
resolvePossibleKeyStringValues: hooks?.resolvePossibleKeyStringValues,
|
|
72
|
-
resolvePossibleContextStringValues: hooks?.resolvePossibleContextStringValues
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Main entry point for AST traversal.
|
|
78
|
-
* Creates a root scope and begins the recursive walk through the syntax tree.
|
|
79
|
-
*
|
|
80
|
-
* @param node - The root module node to traverse
|
|
81
|
-
*/
|
|
82
|
-
public visit (node: Module): void {
|
|
83
|
-
this.enterScope() // Create the root scope for the file
|
|
84
|
-
this.walk(node)
|
|
85
|
-
this.exitScope() // Clean up the root scope
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Recursively walks through AST nodes, handling scoping and visiting logic.
|
|
90
|
-
*
|
|
91
|
-
* This is the core traversal method that:
|
|
92
|
-
* 1. Manages function scopes (enter/exit)
|
|
93
|
-
* 2. Dispatches to specific handlers based on node type
|
|
94
|
-
* 3. Recursively processes child nodes
|
|
95
|
-
* 4. Maintains proper scope cleanup
|
|
96
|
-
*
|
|
97
|
-
* @param node - The current AST node to process
|
|
98
|
-
*
|
|
99
|
-
* @private
|
|
100
|
-
*/
|
|
101
|
-
private walk (node: Node | any): void {
|
|
102
|
-
if (!node) return
|
|
103
|
-
|
|
104
|
-
let isNewScope = false
|
|
105
|
-
// ENTER SCOPE for functions
|
|
106
|
-
if (node.type === 'Function' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
|
|
107
|
-
this.enterScope()
|
|
108
|
-
isNewScope = true
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.hooks.onBeforeVisitNode?.(node)
|
|
112
|
-
|
|
113
|
-
// --- VISIT LOGIC ---
|
|
114
|
-
// Handle specific node types
|
|
115
|
-
switch (node.type) {
|
|
116
|
-
case 'VariableDeclarator':
|
|
117
|
-
this.handleVariableDeclarator(node)
|
|
118
|
-
break
|
|
119
|
-
case 'CallExpression':
|
|
120
|
-
this.handleCallExpression(node)
|
|
121
|
-
break
|
|
122
|
-
case 'JSXElement':
|
|
123
|
-
this.handleJSXElement(node)
|
|
124
|
-
break
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
this.hooks.onAfterVisitNode?.(node)
|
|
128
|
-
|
|
129
|
-
// --- END VISIT LOGIC ---
|
|
130
|
-
|
|
131
|
-
// --- RECURSION ---
|
|
132
|
-
// Recurse into the children of the current node
|
|
133
|
-
for (const key in node) {
|
|
134
|
-
if (key === 'span') continue
|
|
135
|
-
|
|
136
|
-
const child = node[key]
|
|
137
|
-
if (Array.isArray(child)) {
|
|
138
|
-
for (const item of child) {
|
|
139
|
-
// Be less strict: if it's a non-null object, walk it.
|
|
140
|
-
// This allows traversal into nodes that might not have a `.type` property
|
|
141
|
-
// but still contain other valid AST nodes.
|
|
142
|
-
if (item && typeof item === 'object') {
|
|
143
|
-
this.walk(item)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} else if (child && typeof child === 'object') {
|
|
147
|
-
// The condition for single objects should be the same as for array items.
|
|
148
|
-
// Do not require `child.type`. This allows traversal into class method bodies.
|
|
149
|
-
this.walk(child)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// --- END RECURSION ---
|
|
153
|
-
|
|
154
|
-
// LEAVE SCOPE for functions
|
|
155
|
-
if (isNewScope) {
|
|
156
|
-
this.exitScope()
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Enters a new variable scope by pushing a new scope map onto the stack.
|
|
162
|
-
* Used when entering functions to isolate variable declarations.
|
|
163
|
-
*
|
|
164
|
-
* @private
|
|
165
|
-
*/
|
|
166
|
-
private enterScope (): void {
|
|
167
|
-
this.scopeStack.push(new Map())
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Exits the current variable scope by popping the top scope map.
|
|
172
|
-
* Used when leaving functions to clean up variable tracking.
|
|
173
|
-
*
|
|
174
|
-
* @private
|
|
175
|
-
*/
|
|
176
|
-
private exitScope (): void {
|
|
177
|
-
this.scopeStack.pop()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Stores variable information in the current scope.
|
|
182
|
-
* Used to track translation functions and their configuration.
|
|
183
|
-
*
|
|
184
|
-
* @param name - Variable name to store
|
|
185
|
-
* @param info - Scope information about the variable
|
|
186
|
-
*
|
|
187
|
-
* @private
|
|
188
|
-
*/
|
|
189
|
-
private setVarInScope (name: string, info: ScopeInfo): void {
|
|
190
|
-
if (this.scopeStack.length > 0) {
|
|
191
|
-
this.scopeStack[this.scopeStack.length - 1].set(name, info)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Retrieves variable information from the scope chain.
|
|
197
|
-
* Searches from innermost to outermost scope.
|
|
198
|
-
*
|
|
199
|
-
* @param name - Variable name to look up
|
|
200
|
-
* @returns Scope information if found, undefined otherwise
|
|
201
|
-
*
|
|
202
|
-
* @private
|
|
203
|
-
*/
|
|
204
|
-
public getVarFromScope (name: string): ScopeInfo | undefined {
|
|
205
|
-
// First check the proper scope stack (this is the primary source of truth)
|
|
206
|
-
for (let i = this.scopeStack.length - 1; i >= 0; i--) {
|
|
207
|
-
if (this.scopeStack[i].has(name)) {
|
|
208
|
-
const scopeInfo = this.scopeStack[i].get(name)
|
|
209
|
-
return scopeInfo
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Then check the legacy scope tracking for useTranslation calls (for comment parsing)
|
|
214
|
-
const legacyScope = this.scope.get(name)
|
|
215
|
-
if (legacyScope) {
|
|
216
|
-
return legacyScope
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return undefined
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Handles variable declarations that might define translation functions.
|
|
224
|
-
*
|
|
225
|
-
* Processes two patterns:
|
|
226
|
-
* 1. `const { t } = useTranslation(...)` - React i18next pattern
|
|
227
|
-
* 2. `const t = i18next.getFixedT(...)` - Core i18next pattern
|
|
228
|
-
*
|
|
229
|
-
* Extracts namespace and key prefix information for later use.
|
|
230
|
-
*
|
|
231
|
-
* @param node - Variable declarator node to process
|
|
232
|
-
*
|
|
233
|
-
* @private
|
|
234
|
-
*/
|
|
235
|
-
private handleVariableDeclarator (node: VariableDeclarator): void {
|
|
236
|
-
const init = node.init
|
|
237
|
-
if (!init) return
|
|
238
|
-
|
|
239
|
-
// Determine the actual call expression, looking inside AwaitExpressions.
|
|
240
|
-
const callExpr =
|
|
241
|
-
init.type === 'AwaitExpression' && init.argument.type === 'CallExpression'
|
|
242
|
-
? init.argument
|
|
243
|
-
: init.type === 'CallExpression'
|
|
244
|
-
? init
|
|
245
|
-
: null
|
|
246
|
-
|
|
247
|
-
if (!callExpr) return
|
|
248
|
-
|
|
249
|
-
const callee = callExpr.callee
|
|
250
|
-
|
|
251
|
-
// Handle: const { t } = useTranslation(...)
|
|
252
|
-
if (callee.type === 'Identifier') {
|
|
253
|
-
const hookConfig = this.getUseTranslationConfig(callee.value)
|
|
254
|
-
if (hookConfig) {
|
|
255
|
-
this.handleUseTranslationDeclarator(node, callExpr, hookConfig)
|
|
256
|
-
|
|
257
|
-
// ALSO store in the legacy scope for comment parsing compatibility
|
|
258
|
-
this.handleUseTranslationForComments(node, callExpr, hookConfig)
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Handle: const t = i18next.getFixedT(...)
|
|
264
|
-
if (
|
|
265
|
-
callee.type === 'MemberExpression' &&
|
|
266
|
-
callee.property.type === 'Identifier' &&
|
|
267
|
-
callee.property.value === 'getFixedT'
|
|
268
|
-
) {
|
|
269
|
-
this.handleGetFixedTDeclarator(node, callExpr)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Handles useTranslation calls for comment scope resolution.
|
|
275
|
-
* This is a separate method to store scope info in the legacy scope map
|
|
276
|
-
* that the comment parser can access.
|
|
277
|
-
*
|
|
278
|
-
* @param node - Variable declarator with useTranslation call
|
|
279
|
-
* @param callExpr - The CallExpression node representing the useTranslation invocation
|
|
280
|
-
* @param hookConfig - Configuration describing argument positions for namespace and keyPrefix
|
|
281
|
-
*
|
|
282
|
-
* @private
|
|
283
|
-
*/
|
|
284
|
-
private handleUseTranslationForComments (node: VariableDeclarator, callExpr: CallExpression, hookConfig: UseTranslationHookConfig): void {
|
|
285
|
-
let variableName: string | undefined
|
|
286
|
-
|
|
287
|
-
// Handle simple assignment: let t = useTranslation()
|
|
288
|
-
if (node.id.type === 'Identifier') {
|
|
289
|
-
variableName = node.id.value
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Handle array destructuring: const [t, i18n] = useTranslation()
|
|
293
|
-
if (node.id.type === 'ArrayPattern') {
|
|
294
|
-
const firstElement = node.id.elements[0]
|
|
295
|
-
if (firstElement?.type === 'Identifier') {
|
|
296
|
-
variableName = firstElement.value
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Handle object destructuring: const { t } or { t: t1 } = useTranslation()
|
|
301
|
-
if (node.id.type === 'ObjectPattern') {
|
|
302
|
-
for (const prop of node.id.properties) {
|
|
303
|
-
if (prop.type === 'AssignmentPatternProperty' && prop.key.type === 'Identifier' && prop.key.value === 't') {
|
|
304
|
-
// This handles { t = defaultT }
|
|
305
|
-
variableName = 't'
|
|
306
|
-
break
|
|
307
|
-
}
|
|
308
|
-
if (prop.type === 'KeyValuePatternProperty' && prop.key.type === 'Identifier' && prop.key.value === 't' && prop.value.type === 'Identifier') {
|
|
309
|
-
// This handles { t: myT }
|
|
310
|
-
variableName = prop.value.value
|
|
311
|
-
break
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// If we couldn't find a `t` function being declared, exit
|
|
317
|
-
if (!variableName) return
|
|
318
|
-
|
|
319
|
-
// Extract namespace from useTranslation arguments
|
|
320
|
-
const nsArg = callExpr.arguments?.[hookConfig.nsArg]?.expression
|
|
321
|
-
const optionsArg = callExpr.arguments?.[hookConfig.keyPrefixArg]?.expression
|
|
322
|
-
|
|
323
|
-
let defaultNs: string | undefined
|
|
324
|
-
let keyPrefix: string | undefined
|
|
325
|
-
|
|
326
|
-
// Parse namespace argument
|
|
327
|
-
if (nsArg?.type === 'StringLiteral') {
|
|
328
|
-
defaultNs = nsArg.value
|
|
329
|
-
} else if (nsArg?.type === 'ArrayExpression' && nsArg.elements[0]?.expression.type === 'StringLiteral') {
|
|
330
|
-
defaultNs = nsArg.elements[0].expression.value
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Parse keyPrefix from options object
|
|
334
|
-
if (optionsArg?.type === 'ObjectExpression') {
|
|
335
|
-
const keyPrefixProp = optionsArg.properties.find(
|
|
336
|
-
prop => prop.type === 'KeyValueProperty' &&
|
|
337
|
-
prop.key.type === 'Identifier' &&
|
|
338
|
-
prop.key.value === 'keyPrefix'
|
|
339
|
-
)
|
|
340
|
-
if (keyPrefixProp?.type === 'KeyValueProperty' && keyPrefixProp.value.type === 'StringLiteral') {
|
|
341
|
-
keyPrefix = keyPrefixProp.value.value
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Store in the legacy scope map for comment parsing
|
|
346
|
-
if (defaultNs || keyPrefix) {
|
|
347
|
-
this.scope.set(variableName, { defaultNs, keyPrefix })
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Processes useTranslation hook declarations to extract scope information.
|
|
353
|
-
*
|
|
354
|
-
* Handles various destructuring patterns:
|
|
355
|
-
* - `const [t] = useTranslation('ns')` - Array destructuring
|
|
356
|
-
* - `const { t } = useTranslation('ns')` - Object destructuring
|
|
357
|
-
* - `const { t: myT } = useTranslation('ns')` - Aliased destructuring
|
|
358
|
-
*
|
|
359
|
-
* Extracts namespace from the first argument and keyPrefix from options.
|
|
360
|
-
*
|
|
361
|
-
* @param node - Variable declarator with useTranslation call
|
|
362
|
-
* @param callExpr - The CallExpression node representing the useTranslation invocation
|
|
363
|
-
* @param hookConfig - Configuration describing argument positions for namespace and keyPrefix
|
|
364
|
-
*
|
|
365
|
-
* @private
|
|
366
|
-
*/
|
|
367
|
-
private handleUseTranslationDeclarator (node: VariableDeclarator, callExpr: CallExpression, hookConfig: UseTranslationHookConfig): void {
|
|
368
|
-
let variableName: string | undefined
|
|
369
|
-
|
|
370
|
-
// Handle simple assignment: let t = useTranslation()
|
|
371
|
-
if (node.id.type === 'Identifier') {
|
|
372
|
-
variableName = node.id.value
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Handle array destructuring: const [t, i18n] = useTranslation()
|
|
376
|
-
if (node.id.type === 'ArrayPattern') {
|
|
377
|
-
const firstElement = node.id.elements[0]
|
|
378
|
-
if (firstElement?.type === 'Identifier') {
|
|
379
|
-
variableName = firstElement.value
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Handle object destructuring: const { t } or { t: t1 } = useTranslation()
|
|
384
|
-
if (node.id.type === 'ObjectPattern') {
|
|
385
|
-
for (const prop of node.id.properties) {
|
|
386
|
-
if (prop.type === 'AssignmentPatternProperty' && prop.key.type === 'Identifier' && prop.key.value === 't') {
|
|
387
|
-
// This handles { t = defaultT }
|
|
388
|
-
variableName = 't'
|
|
389
|
-
break
|
|
390
|
-
}
|
|
391
|
-
if (prop.type === 'KeyValuePatternProperty' && prop.key.type === 'Identifier' && prop.key.value === 't' && prop.value.type === 'Identifier') {
|
|
392
|
-
// This handles { t: myT }
|
|
393
|
-
variableName = prop.value.value
|
|
394
|
-
break
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// If we couldn't find a `t` function being declared, exit
|
|
400
|
-
if (!variableName) return
|
|
401
|
-
|
|
402
|
-
// Use the configured argument indices from hookConfig
|
|
403
|
-
const nsArg = callExpr.arguments?.[hookConfig.nsArg]?.expression
|
|
404
|
-
|
|
405
|
-
let defaultNs: string | undefined
|
|
406
|
-
if (nsArg?.type === 'StringLiteral') {
|
|
407
|
-
defaultNs = nsArg.value
|
|
408
|
-
} else if (nsArg?.type === 'ArrayExpression' && nsArg.elements[0]?.expression.type === 'StringLiteral') {
|
|
409
|
-
defaultNs = nsArg.elements[0].expression.value
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const optionsArg = callExpr.arguments?.[hookConfig.keyPrefixArg]?.expression
|
|
413
|
-
let keyPrefix: string | undefined
|
|
414
|
-
if (optionsArg?.type === 'ObjectExpression') {
|
|
415
|
-
const kp = getObjectPropValue(optionsArg, 'keyPrefix')
|
|
416
|
-
keyPrefix = typeof kp === 'string' ? kp : undefined
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Store the scope info for the declared variable
|
|
420
|
-
this.setVarInScope(variableName, { defaultNs, keyPrefix })
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Processes getFixedT function declarations to extract scope information.
|
|
425
|
-
*
|
|
426
|
-
* Handles the pattern: `const t = i18next.getFixedT(lng, ns, keyPrefix)`
|
|
427
|
-
* - Ignores the first argument (language)
|
|
428
|
-
* - Extracts namespace from the second argument
|
|
429
|
-
* - Extracts key prefix from the third argument
|
|
430
|
-
*
|
|
431
|
-
* @param node - Variable declarator with getFixedT call
|
|
432
|
-
* @param callExpr - The CallExpression node representing the getFixedT invocation
|
|
433
|
-
*
|
|
434
|
-
* @private
|
|
435
|
-
*/
|
|
436
|
-
private handleGetFixedTDeclarator (node: VariableDeclarator, callExpr: CallExpression): void {
|
|
437
|
-
// Ensure we are assigning to a simple variable, e.g., const t = ...
|
|
438
|
-
if (node.id.type !== 'Identifier' || !node.init || node.init.type !== 'CallExpression') return
|
|
439
|
-
|
|
440
|
-
const variableName = node.id.value
|
|
441
|
-
const args = callExpr.arguments
|
|
442
|
-
|
|
443
|
-
// getFixedT(lng, ns, keyPrefix)
|
|
444
|
-
// We ignore the first argument (lng) for key extraction.
|
|
445
|
-
const nsArg = args[1]?.expression
|
|
446
|
-
const keyPrefixArg = args[2]?.expression
|
|
447
|
-
|
|
448
|
-
const defaultNs = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined
|
|
449
|
-
const keyPrefix = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined
|
|
450
|
-
|
|
451
|
-
if (defaultNs || keyPrefix) {
|
|
452
|
-
this.setVarInScope(variableName, { defaultNs, keyPrefix })
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Processes function call expressions to extract translation keys.
|
|
458
|
-
*
|
|
459
|
-
* This is the core extraction method that handles:
|
|
460
|
-
* - Standard t() calls with string literals
|
|
461
|
-
* - Selector API calls with arrow functions: `t($ => $.path.to.key)`
|
|
462
|
-
* - Namespace resolution from multiple sources
|
|
463
|
-
* - Default value extraction from various argument patterns
|
|
464
|
-
* - Pluralization and context handling
|
|
465
|
-
* - Key prefix application from scope
|
|
466
|
-
*
|
|
467
|
-
* @param node - Call expression node to process
|
|
468
|
-
*
|
|
469
|
-
* @private
|
|
470
|
-
*/
|
|
471
|
-
private handleCallExpression (node: CallExpression): void {
|
|
472
|
-
const functionName = this.getFunctionName(node.callee)
|
|
473
|
-
if (!functionName) return
|
|
474
|
-
|
|
475
|
-
// The scope lookup will only work for simple identifiers, which is okay for this fix.
|
|
476
|
-
const scopeInfo = this.getVarFromScope(functionName)
|
|
477
|
-
const configuredFunctions = this.config.extract.functions || ['t', '*.t']
|
|
478
|
-
let isFunctionToParse = scopeInfo !== undefined // A scoped variable (from useTranslation, etc.) is always parsed.
|
|
479
|
-
if (!isFunctionToParse) {
|
|
480
|
-
for (const pattern of configuredFunctions) {
|
|
481
|
-
if (pattern.startsWith('*.')) {
|
|
482
|
-
// Handle wildcard suffix (e.g., '*.t' matches 'i18n.t')
|
|
483
|
-
if (functionName.endsWith(pattern.substring(1))) {
|
|
484
|
-
isFunctionToParse = true
|
|
485
|
-
break
|
|
486
|
-
}
|
|
487
|
-
} else {
|
|
488
|
-
// Handle exact match
|
|
489
|
-
if (pattern === functionName) {
|
|
490
|
-
isFunctionToParse = true
|
|
491
|
-
break
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (!isFunctionToParse || node.arguments.length === 0) return
|
|
497
|
-
|
|
498
|
-
const { keysToProcess, isSelectorAPI } = this.handleCallExpressionArgument(node, 0)
|
|
499
|
-
|
|
500
|
-
if (keysToProcess.length === 0) return
|
|
501
|
-
|
|
502
|
-
let isOrdinalByKey = false
|
|
503
|
-
const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
|
|
504
|
-
|
|
505
|
-
for (let i = 0; i < keysToProcess.length; i++) {
|
|
506
|
-
if (keysToProcess[i].endsWith(`${pluralSeparator}ordinal`)) {
|
|
507
|
-
isOrdinalByKey = true
|
|
508
|
-
// Normalize the key by stripping the suffix
|
|
509
|
-
keysToProcess[i] = keysToProcess[i].slice(0, -8)
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
let defaultValue: string | undefined
|
|
514
|
-
let options: ObjectExpression | undefined
|
|
515
|
-
|
|
516
|
-
if (node.arguments.length > 1) {
|
|
517
|
-
const arg2 = node.arguments[1].expression
|
|
518
|
-
if (arg2.type === 'ObjectExpression') {
|
|
519
|
-
options = arg2
|
|
520
|
-
} else if (arg2.type === 'StringLiteral') {
|
|
521
|
-
defaultValue = arg2.value
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (node.arguments.length > 2) {
|
|
525
|
-
const arg3 = node.arguments[2].expression
|
|
526
|
-
if (arg3.type === 'ObjectExpression') {
|
|
527
|
-
options = arg3
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
const defaultValueFromOptions = options ? getObjectPropValue(options, 'defaultValue') : undefined
|
|
531
|
-
const finalDefaultValue = (typeof defaultValueFromOptions === 'string' ? defaultValueFromOptions : defaultValue)
|
|
532
|
-
|
|
533
|
-
// Loop through each key found (could be one or more) and process it
|
|
534
|
-
for (let i = 0; i < keysToProcess.length; i++) {
|
|
535
|
-
let key = keysToProcess[i]
|
|
536
|
-
let ns: string | undefined
|
|
537
|
-
|
|
538
|
-
// Determine namespace (explicit ns > ns:key > scope ns > default)
|
|
539
|
-
// See https://www.i18next.com/overview/api#getfixedt
|
|
540
|
-
if (options) {
|
|
541
|
-
const nsVal = getObjectPropValue(options, 'ns')
|
|
542
|
-
if (typeof nsVal === 'string') ns = nsVal
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const nsSeparator = this.config.extract.nsSeparator ?? ':'
|
|
546
|
-
if (!ns && nsSeparator && key.includes(nsSeparator)) {
|
|
547
|
-
const parts = key.split(nsSeparator)
|
|
548
|
-
ns = parts.shift()
|
|
549
|
-
key = parts.join(nsSeparator)
|
|
550
|
-
|
|
551
|
-
if (!key || key.trim() === '') {
|
|
552
|
-
this.logger.warn(`Skipping key that became empty after namespace removal: '${ns}${nsSeparator}'`)
|
|
553
|
-
continue
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (!ns && scopeInfo?.defaultNs) ns = scopeInfo.defaultNs
|
|
558
|
-
if (!ns) ns = this.config.extract.defaultNS
|
|
559
|
-
|
|
560
|
-
let finalKey = key
|
|
561
|
-
|
|
562
|
-
// Apply keyPrefix AFTER namespace extraction
|
|
563
|
-
if (scopeInfo?.keyPrefix) {
|
|
564
|
-
const keySeparator = this.config.extract.keySeparator ?? '.'
|
|
565
|
-
|
|
566
|
-
// Apply keyPrefix - handle case where keyPrefix already ends with separator
|
|
567
|
-
if (keySeparator !== false) {
|
|
568
|
-
if (scopeInfo.keyPrefix.endsWith(keySeparator)) {
|
|
569
|
-
finalKey = `${scopeInfo.keyPrefix}${key}`
|
|
570
|
-
} else {
|
|
571
|
-
finalKey = `${scopeInfo.keyPrefix}${keySeparator}${key}`
|
|
572
|
-
}
|
|
573
|
-
} else {
|
|
574
|
-
finalKey = `${scopeInfo.keyPrefix}${key}`
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Validate keyPrefix combinations that create problematic keys
|
|
578
|
-
if (keySeparator !== false) {
|
|
579
|
-
// Check for patterns that would create empty segments in the nested key structure
|
|
580
|
-
const segments = finalKey.split(keySeparator)
|
|
581
|
-
const hasEmptySegment = segments.some(segment => segment.trim() === '')
|
|
582
|
-
|
|
583
|
-
if (hasEmptySegment) {
|
|
584
|
-
this.logger.warn(`Skipping key with empty segments: '${finalKey}' (keyPrefix: '${scopeInfo.keyPrefix}', key: '${key}')`)
|
|
585
|
-
continue
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const isLastKey = i === keysToProcess.length - 1
|
|
591
|
-
const dv = isLastKey ? (finalDefaultValue || key) : key
|
|
592
|
-
|
|
593
|
-
// Handle plurals, context, and returnObjects
|
|
594
|
-
if (options) {
|
|
595
|
-
const contextProp = getObjectProperty(options, 'context')
|
|
596
|
-
|
|
597
|
-
const keysWithContext: ExtractedKey[] = []
|
|
598
|
-
|
|
599
|
-
// 1. Handle Context
|
|
600
|
-
if (contextProp?.value?.type === 'StringLiteral' || contextProp?.value.type === 'NumericLiteral' || contextProp?.value.type === 'BooleanLiteral') {
|
|
601
|
-
// If the context is static, we don't need to add the base key
|
|
602
|
-
const contextValue = `${contextProp.value.value}`
|
|
603
|
-
|
|
604
|
-
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
605
|
-
// Ignore context: ''
|
|
606
|
-
if (contextValue !== '') {
|
|
607
|
-
keysWithContext.push({ key: `${finalKey}${contextSeparator}${contextValue}`, ns, defaultValue: dv })
|
|
608
|
-
}
|
|
609
|
-
} else if (contextProp?.value) {
|
|
610
|
-
const contextValues = this.resolvePossibleContextStringValues(contextProp.value)
|
|
611
|
-
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
612
|
-
|
|
613
|
-
if (contextValues.length > 0) {
|
|
614
|
-
contextValues.forEach(context => {
|
|
615
|
-
keysWithContext.push({ key: `${finalKey}${contextSeparator}${context}`, ns, defaultValue: dv })
|
|
616
|
-
})
|
|
617
|
-
// For dynamic context, also add the base key as a fallback
|
|
618
|
-
keysWithContext.push({ key: finalKey, ns, defaultValue: dv })
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// 2. Handle Plurals
|
|
623
|
-
const hasCount = getObjectPropValue(options, 'count') !== undefined
|
|
624
|
-
const isOrdinalByOption = getObjectPropValue(options, 'ordinal') === true
|
|
625
|
-
if (hasCount || isOrdinalByKey) {
|
|
626
|
-
// Check if plurals are disabled
|
|
627
|
-
if (this.config.extract.disablePlurals) {
|
|
628
|
-
// When plurals are disabled, treat count as a regular option (for interpolation only)
|
|
629
|
-
// Still handle context normally
|
|
630
|
-
if (keysWithContext.length > 0) {
|
|
631
|
-
keysWithContext.forEach(this.pluginContext.addKey)
|
|
632
|
-
} else {
|
|
633
|
-
// No context, just add the base key (no plurals even if count is present)
|
|
634
|
-
this.pluginContext.addKey({ key: finalKey, ns, defaultValue: dv })
|
|
635
|
-
}
|
|
636
|
-
} else {
|
|
637
|
-
// Original plural handling logic when plurals are enabled
|
|
638
|
-
// Always pass the base key to handlePluralKeys - it will handle context internally
|
|
639
|
-
this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey, finalDefaultValue)
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
continue // This key is fully handled
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (keysWithContext.length > 0) {
|
|
646
|
-
keysWithContext.forEach(this.pluginContext.addKey)
|
|
647
|
-
|
|
648
|
-
continue // This key is now fully handled
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// 3. Handle returnObjects
|
|
652
|
-
if (getObjectPropValue(options, 'returnObjects') === true) {
|
|
653
|
-
this.objectKeys.add(finalKey)
|
|
654
|
-
// Fall through to add the base key itself
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// 4. Handle selector API as implicit returnObjects
|
|
659
|
-
if (isSelectorAPI) {
|
|
660
|
-
this.objectKeys.add(finalKey)
|
|
661
|
-
// Fall through to add the base key itself
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// 5. Default case: Add the simple key
|
|
665
|
-
this.pluginContext.addKey({ key: finalKey, ns, defaultValue: dv })
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Processed a call expression to extract keys from the specified argument.
|
|
671
|
-
*
|
|
672
|
-
* @param node - The call expression node
|
|
673
|
-
* @param argIndex - The index of the argument to process
|
|
674
|
-
* @returns An object containing the keys to process and a flag indicating if the selector API is used
|
|
675
|
-
*/
|
|
676
|
-
private handleCallExpressionArgument (
|
|
677
|
-
node: CallExpression,
|
|
678
|
-
argIndex: number
|
|
679
|
-
): { keysToProcess: string[]; isSelectorAPI: boolean } {
|
|
680
|
-
const firstArg = node.arguments[argIndex].expression
|
|
681
|
-
const keysToProcess: string[] = []
|
|
682
|
-
let isSelectorAPI = false
|
|
683
|
-
|
|
684
|
-
if (firstArg.type === 'ArrowFunctionExpression') {
|
|
685
|
-
const key = this.extractKeyFromSelector(firstArg)
|
|
686
|
-
if (key) {
|
|
687
|
-
keysToProcess.push(key)
|
|
688
|
-
isSelectorAPI = true
|
|
689
|
-
}
|
|
690
|
-
} else if (firstArg.type === 'ArrayExpression') {
|
|
691
|
-
for (const element of firstArg.elements) {
|
|
692
|
-
if (element?.expression) {
|
|
693
|
-
keysToProcess.push(...this.resolvePossibleKeyStringValues(element.expression))
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
} else {
|
|
697
|
-
keysToProcess.push(...this.resolvePossibleKeyStringValues(firstArg))
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
return {
|
|
701
|
-
keysToProcess: keysToProcess.filter((key) => !!key),
|
|
702
|
-
isSelectorAPI,
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Generates plural form keys based on the primary language's plural rules.
|
|
708
|
-
*
|
|
709
|
-
* Uses Intl.PluralRules to determine the correct plural categories
|
|
710
|
-
* for the configured primary language and generates suffixed keys
|
|
711
|
-
* for each category (e.g., 'item_one', 'item_other').
|
|
712
|
-
*
|
|
713
|
-
* @param key - Base key name for pluralization
|
|
714
|
-
* @param ns - Namespace for the keys
|
|
715
|
-
* @param options - object expression options
|
|
716
|
-
* @param isOrdinal - isOrdinal flag
|
|
717
|
-
*
|
|
718
|
-
* @private
|
|
719
|
-
*/
|
|
720
|
-
private handlePluralKeys (key: string, ns: string | undefined, options: ObjectExpression, isOrdinal: boolean, defaultValueFromCall?: string): void {
|
|
721
|
-
try {
|
|
722
|
-
const type = isOrdinal ? 'ordinal' : 'cardinal'
|
|
723
|
-
|
|
724
|
-
// Generate plural forms for ALL target languages to ensure we have all necessary keys
|
|
725
|
-
const allPluralCategories = new Set<string>()
|
|
726
|
-
|
|
727
|
-
for (const locale of this.config.locales) {
|
|
728
|
-
try {
|
|
729
|
-
const pluralRules = new Intl.PluralRules(locale, { type })
|
|
730
|
-
const categories = pluralRules.resolvedOptions().pluralCategories
|
|
731
|
-
categories.forEach(cat => allPluralCategories.add(cat))
|
|
732
|
-
} catch (e) {
|
|
733
|
-
// If a locale is invalid, fall back to English rules
|
|
734
|
-
const englishRules = new Intl.PluralRules('en', { type })
|
|
735
|
-
const categories = englishRules.resolvedOptions().pluralCategories
|
|
736
|
-
categories.forEach(cat => allPluralCategories.add(cat))
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const pluralCategories = Array.from(allPluralCategories).sort()
|
|
741
|
-
const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
|
|
742
|
-
|
|
743
|
-
// Get all possible default values once at the start
|
|
744
|
-
const defaultValue = getObjectPropValue(options, 'defaultValue')
|
|
745
|
-
const otherDefault = getObjectPropValue(options, `defaultValue${pluralSeparator}other`)
|
|
746
|
-
const ordinalOtherDefault = getObjectPropValue(options, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`)
|
|
747
|
-
|
|
748
|
-
// Get the count value and determine target category if available
|
|
749
|
-
const countValue = getObjectPropValue(options, 'count')
|
|
750
|
-
let targetCategory: string | undefined
|
|
751
|
-
|
|
752
|
-
if (typeof countValue === 'number') {
|
|
753
|
-
try {
|
|
754
|
-
const primaryLanguage = this.config.extract?.primaryLanguage || this.config.locales[0] || 'en'
|
|
755
|
-
const pluralRules = new Intl.PluralRules(primaryLanguage, { type })
|
|
756
|
-
targetCategory = pluralRules.select(countValue)
|
|
757
|
-
} catch (e) {
|
|
758
|
-
// If we can't determine the category, continue with normal logic
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Handle context - both static and dynamic
|
|
763
|
-
const contextProp = getObjectProperty(options, 'context')
|
|
764
|
-
const keysToGenerate: Array<{ key: string, context?: string }> = []
|
|
765
|
-
|
|
766
|
-
if (contextProp?.value) {
|
|
767
|
-
// Handle dynamic context by resolving all possible values
|
|
768
|
-
const contextValues = this.resolvePossibleContextStringValues(contextProp.value)
|
|
769
|
-
|
|
770
|
-
if (contextValues.length > 0) {
|
|
771
|
-
// Generate keys for each context value
|
|
772
|
-
for (const contextValue of contextValues) {
|
|
773
|
-
if (contextValue.length > 0) { // Skip empty contexts
|
|
774
|
-
keysToGenerate.push({ key, context: contextValue })
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// For dynamic context, also generate base plural forms if generateBasePluralForms is not disabled
|
|
779
|
-
const shouldGenerateBaseForms = this.config.extract?.generateBasePluralForms !== false
|
|
780
|
-
if (shouldGenerateBaseForms) {
|
|
781
|
-
keysToGenerate.push({ key })
|
|
782
|
-
}
|
|
783
|
-
} else {
|
|
784
|
-
// Couldn't resolve context, fall back to base key only
|
|
785
|
-
keysToGenerate.push({ key })
|
|
786
|
-
}
|
|
787
|
-
} else {
|
|
788
|
-
// No context, always generate base plural forms
|
|
789
|
-
keysToGenerate.push({ key })
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Generate plural forms for each key variant
|
|
793
|
-
for (const { key: baseKey, context } of keysToGenerate) {
|
|
794
|
-
for (const category of pluralCategories) {
|
|
795
|
-
// 1. Look for the most specific default value
|
|
796
|
-
const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`
|
|
797
|
-
const specificDefault = getObjectPropValue(options, specificDefaultKey)
|
|
798
|
-
|
|
799
|
-
// 2. Determine the final default value using a clear fallback chain
|
|
800
|
-
let finalDefaultValue: string | undefined
|
|
801
|
-
if (typeof specificDefault === 'string') {
|
|
802
|
-
finalDefaultValue = specificDefault
|
|
803
|
-
} else if (category === 'one' && typeof defaultValue === 'string') {
|
|
804
|
-
finalDefaultValue = defaultValue
|
|
805
|
-
} else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
|
|
806
|
-
finalDefaultValue = ordinalOtherDefault
|
|
807
|
-
} else if (!isOrdinal && typeof otherDefault === 'string') {
|
|
808
|
-
finalDefaultValue = otherDefault
|
|
809
|
-
} else if (typeof defaultValue === 'string') {
|
|
810
|
-
finalDefaultValue = defaultValue
|
|
811
|
-
} else if (defaultValueFromCall && targetCategory === category) {
|
|
812
|
-
finalDefaultValue = defaultValueFromCall
|
|
813
|
-
} else {
|
|
814
|
-
finalDefaultValue = baseKey
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// 3. Construct the final plural key
|
|
818
|
-
let finalKey: string
|
|
819
|
-
if (context) {
|
|
820
|
-
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
821
|
-
finalKey = isOrdinal
|
|
822
|
-
? `${baseKey}${contextSeparator}${context}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
823
|
-
: `${baseKey}${contextSeparator}${context}${pluralSeparator}${category}`
|
|
824
|
-
} else {
|
|
825
|
-
finalKey = isOrdinal
|
|
826
|
-
? `${baseKey}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
827
|
-
: `${baseKey}${pluralSeparator}${category}`
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
this.pluginContext.addKey({
|
|
831
|
-
key: finalKey,
|
|
832
|
-
ns,
|
|
833
|
-
defaultValue: finalDefaultValue,
|
|
834
|
-
hasCount: true,
|
|
835
|
-
isOrdinal
|
|
836
|
-
})
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
} catch (e) {
|
|
840
|
-
this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`)
|
|
841
|
-
// Fallback to a simple key if Intl API fails
|
|
842
|
-
const defaultValue = defaultValueFromCall || getObjectPropValue(options, 'defaultValue')
|
|
843
|
-
this.pluginContext.addKey({ key, ns, defaultValue: typeof defaultValue === 'string' ? defaultValue : key })
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Processes JSX elements to extract translation keys from Trans components.
|
|
849
|
-
*
|
|
850
|
-
* Identifies configured Trans components and delegates to the JSX parser
|
|
851
|
-
* for complex children serialization and attribute extraction.
|
|
852
|
-
*
|
|
853
|
-
* @param node - JSX element node to process
|
|
854
|
-
*
|
|
855
|
-
* @private
|
|
856
|
-
*/
|
|
857
|
-
private handleJSXElement (node: JSXElement): void {
|
|
858
|
-
const elementName = this.getElementName(node)
|
|
859
|
-
|
|
860
|
-
if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
|
|
861
|
-
const extractedAttributes = extractFromTransComponent(node, this.config)
|
|
862
|
-
|
|
863
|
-
const keysToProcess: string[] = []
|
|
864
|
-
|
|
865
|
-
if (extractedAttributes) {
|
|
866
|
-
if (extractedAttributes.keyExpression) {
|
|
867
|
-
const keyValues = this.resolvePossibleKeyStringValues(extractedAttributes.keyExpression)
|
|
868
|
-
keysToProcess.push(...keyValues)
|
|
869
|
-
} else {
|
|
870
|
-
keysToProcess.push(extractedAttributes.serializedChildren)
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
let extractedKeys: ExtractedKey[]
|
|
874
|
-
|
|
875
|
-
const { contextExpression, optionsNode, defaultValue, hasCount, isOrdinal, serializedChildren } = extractedAttributes
|
|
876
|
-
|
|
877
|
-
// If ns is not explicitly set on the component, try to find it from the key
|
|
878
|
-
// or the `t` prop
|
|
879
|
-
if (!extractedAttributes.ns) {
|
|
880
|
-
extractedKeys = keysToProcess.map(key => {
|
|
881
|
-
const nsSeparator = this.config.extract.nsSeparator ?? ':'
|
|
882
|
-
let ns: string | undefined
|
|
883
|
-
|
|
884
|
-
// If the key contains a namespace separator, it takes precedence
|
|
885
|
-
// over the default t ns value
|
|
886
|
-
if (nsSeparator && key.includes(nsSeparator)) {
|
|
887
|
-
let parts: string[]
|
|
888
|
-
([ns, ...parts] = key.split(nsSeparator))
|
|
889
|
-
|
|
890
|
-
key = parts.join(nsSeparator)
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
return {
|
|
894
|
-
key,
|
|
895
|
-
ns,
|
|
896
|
-
defaultValue: defaultValue || serializedChildren,
|
|
897
|
-
hasCount,
|
|
898
|
-
isOrdinal,
|
|
899
|
-
}
|
|
900
|
-
})
|
|
901
|
-
|
|
902
|
-
const tProp = node.opening.attributes?.find(
|
|
903
|
-
attr =>
|
|
904
|
-
attr.type === 'JSXAttribute' &&
|
|
905
|
-
attr.name.type === 'Identifier' &&
|
|
906
|
-
attr.name.value === 't'
|
|
907
|
-
)
|
|
908
|
-
|
|
909
|
-
// Check if the prop value is an identifier (e.g., t={t})
|
|
910
|
-
if (
|
|
911
|
-
tProp?.type === 'JSXAttribute' &&
|
|
912
|
-
tProp.value?.type === 'JSXExpressionContainer' &&
|
|
913
|
-
tProp.value.expression.type === 'Identifier'
|
|
914
|
-
) {
|
|
915
|
-
const tIdentifier = tProp.value.expression.value
|
|
916
|
-
const scopeInfo = this.getVarFromScope(tIdentifier)
|
|
917
|
-
if (scopeInfo?.defaultNs) {
|
|
918
|
-
extractedKeys.forEach(key => {
|
|
919
|
-
if (!key.ns) {
|
|
920
|
-
key.ns = scopeInfo.defaultNs
|
|
921
|
-
}
|
|
922
|
-
})
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
} else {
|
|
926
|
-
const { ns } = extractedAttributes
|
|
927
|
-
extractedKeys = keysToProcess.map(key => {
|
|
928
|
-
return {
|
|
929
|
-
key,
|
|
930
|
-
ns,
|
|
931
|
-
defaultValue: defaultValue || serializedChildren,
|
|
932
|
-
hasCount,
|
|
933
|
-
isOrdinal,
|
|
934
|
-
}
|
|
935
|
-
})
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
extractedKeys.forEach(key => {
|
|
939
|
-
// Apply defaultNS from config if no namespace was found on the component and
|
|
940
|
-
// the key does not contain a namespace prefix
|
|
941
|
-
if (!key.ns) {
|
|
942
|
-
key.ns = this.config.extract.defaultNS
|
|
943
|
-
}
|
|
944
|
-
})
|
|
945
|
-
|
|
946
|
-
// Handle the combination of context and count
|
|
947
|
-
if (contextExpression && hasCount) {
|
|
948
|
-
// Check if plurals are disabled
|
|
949
|
-
if (this.config.extract.disablePlurals) {
|
|
950
|
-
// When plurals are disabled, treat count as a regular option
|
|
951
|
-
// Still handle context normally
|
|
952
|
-
const contextValues = this.resolvePossibleContextStringValues(contextExpression)
|
|
953
|
-
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
954
|
-
|
|
955
|
-
if (contextValues.length > 0) {
|
|
956
|
-
// For static context (string literal), only add context variants
|
|
957
|
-
if (contextExpression.type === 'StringLiteral') {
|
|
958
|
-
for (const context of contextValues) {
|
|
959
|
-
for (const extractedKey of extractedKeys) {
|
|
960
|
-
const contextKey = `${extractedKey.key}${contextSeparator}${context}`
|
|
961
|
-
this.pluginContext.addKey({ key: contextKey, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
} else {
|
|
965
|
-
// For dynamic context, add both base and context variants
|
|
966
|
-
extractedKeys.forEach(extractedKey => {
|
|
967
|
-
this.pluginContext.addKey({
|
|
968
|
-
key: extractedKey.key,
|
|
969
|
-
ns: extractedKey.ns,
|
|
970
|
-
defaultValue: extractedKey.defaultValue
|
|
971
|
-
})
|
|
972
|
-
})
|
|
973
|
-
for (const context of contextValues) {
|
|
974
|
-
for (const extractedKey of extractedKeys) {
|
|
975
|
-
const contextKey = `${extractedKey.key}${contextSeparator}${context}`
|
|
976
|
-
this.pluginContext.addKey({ key: contextKey, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
} else {
|
|
981
|
-
// Fallback to just base keys if context resolution fails
|
|
982
|
-
extractedKeys.forEach(extractedKey => {
|
|
983
|
-
this.pluginContext.addKey({
|
|
984
|
-
key: extractedKey.key,
|
|
985
|
-
ns: extractedKey.ns,
|
|
986
|
-
defaultValue: extractedKey.defaultValue
|
|
987
|
-
})
|
|
988
|
-
})
|
|
989
|
-
}
|
|
990
|
-
} else {
|
|
991
|
-
// Original plural handling logic when plurals are enabled
|
|
992
|
-
// Find isOrdinal prop on the <Trans> component
|
|
993
|
-
const ordinalAttr = node.opening.attributes?.find(
|
|
994
|
-
(attr) =>
|
|
995
|
-
attr.type === 'JSXAttribute' &&
|
|
996
|
-
attr.name.type === 'Identifier' &&
|
|
997
|
-
attr.name.value === 'ordinal'
|
|
998
|
-
)
|
|
999
|
-
const isOrdinal = !!ordinalAttr
|
|
1000
|
-
|
|
1001
|
-
const contextValues = this.resolvePossibleContextStringValues(contextExpression)
|
|
1002
|
-
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
1003
|
-
|
|
1004
|
-
// Generate all combinations of context and plural forms
|
|
1005
|
-
if (contextValues.length > 0) {
|
|
1006
|
-
// Generate base plural forms (no context)
|
|
1007
|
-
extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
|
|
1008
|
-
|
|
1009
|
-
// Generate context + plural combinations
|
|
1010
|
-
for (const context of contextValues) {
|
|
1011
|
-
for (const extractedKey of extractedKeys) {
|
|
1012
|
-
const contextKey = `${extractedKey.key}${contextSeparator}${context}`
|
|
1013
|
-
this.generatePluralKeysForTrans(contextKey, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode)
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
} else {
|
|
1017
|
-
// Fallback to just plural forms if context resolution fails
|
|
1018
|
-
extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
} else if (contextExpression) {
|
|
1022
|
-
const contextValues = this.resolvePossibleContextStringValues(contextExpression)
|
|
1023
|
-
const contextSeparator = this.config.extract.contextSeparator ?? '_'
|
|
1024
|
-
|
|
1025
|
-
if (contextValues.length > 0) {
|
|
1026
|
-
// Add context variants
|
|
1027
|
-
for (const context of contextValues) {
|
|
1028
|
-
for (const { key, ns, defaultValue } of extractedKeys) {
|
|
1029
|
-
this.pluginContext.addKey({ key: `${key}${contextSeparator}${context}`, ns, defaultValue })
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
// Only add the base key as a fallback if the context is dynamic (i.e., not a simple string).
|
|
1033
|
-
if (contextExpression.type !== 'StringLiteral') {
|
|
1034
|
-
extractedKeys.forEach(extractedKey => {
|
|
1035
|
-
this.pluginContext.addKey({
|
|
1036
|
-
key: extractedKey.key,
|
|
1037
|
-
ns: extractedKey.ns,
|
|
1038
|
-
defaultValue: extractedKey.defaultValue
|
|
1039
|
-
})
|
|
1040
|
-
})
|
|
1041
|
-
}
|
|
1042
|
-
} else {
|
|
1043
|
-
// If no context values were resolved, just add base keys
|
|
1044
|
-
extractedKeys.forEach(extractedKey => {
|
|
1045
|
-
this.pluginContext.addKey({
|
|
1046
|
-
key: extractedKey.key,
|
|
1047
|
-
ns: extractedKey.ns,
|
|
1048
|
-
defaultValue: extractedKey.defaultValue
|
|
1049
|
-
})
|
|
1050
|
-
})
|
|
1051
|
-
}
|
|
1052
|
-
} else if (hasCount) {
|
|
1053
|
-
// Check if plurals are disabled
|
|
1054
|
-
if (this.config.extract.disablePlurals) {
|
|
1055
|
-
// When plurals are disabled, just add the base keys (no plural forms)
|
|
1056
|
-
extractedKeys.forEach(extractedKey => {
|
|
1057
|
-
this.pluginContext.addKey({
|
|
1058
|
-
key: extractedKey.key,
|
|
1059
|
-
ns: extractedKey.ns,
|
|
1060
|
-
defaultValue: extractedKey.defaultValue
|
|
1061
|
-
})
|
|
1062
|
-
})
|
|
1063
|
-
} else {
|
|
1064
|
-
// Original plural handling logic when plurals are enabled
|
|
1065
|
-
// Find isOrdinal prop on the <Trans> component
|
|
1066
|
-
const ordinalAttr = node.opening.attributes?.find(
|
|
1067
|
-
(attr) =>
|
|
1068
|
-
attr.type === 'JSXAttribute' &&
|
|
1069
|
-
attr.name.type === 'Identifier' &&
|
|
1070
|
-
attr.name.value === 'ordinal'
|
|
1071
|
-
)
|
|
1072
|
-
const isOrdinal = !!ordinalAttr
|
|
1073
|
-
|
|
1074
|
-
extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
|
|
1075
|
-
}
|
|
1076
|
-
} else {
|
|
1077
|
-
// No count or context - just add the base keys
|
|
1078
|
-
extractedKeys.forEach(extractedKey => {
|
|
1079
|
-
this.pluginContext.addKey({
|
|
1080
|
-
key: extractedKey.key,
|
|
1081
|
-
ns: extractedKey.ns,
|
|
1082
|
-
defaultValue: extractedKey.defaultValue
|
|
1083
|
-
})
|
|
1084
|
-
})
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
* Generates plural keys for Trans components, with support for tOptions plural defaults.
|
|
1092
|
-
*
|
|
1093
|
-
* @param key - Base key name for pluralization
|
|
1094
|
-
* @param defaultValue - Default value for the keys
|
|
1095
|
-
* @param ns - Namespace for the keys
|
|
1096
|
-
* @param isOrdinal - Whether to generate ordinal plural forms
|
|
1097
|
-
* @param optionsNode - Optional tOptions object expression for plural-specific defaults
|
|
1098
|
-
*
|
|
1099
|
-
* @private
|
|
1100
|
-
*/
|
|
1101
|
-
private generatePluralKeysForTrans (key: string, defaultValue: string | undefined, ns: string | undefined, isOrdinal: boolean, optionsNode?: ObjectExpression): void {
|
|
1102
|
-
try {
|
|
1103
|
-
const type = isOrdinal ? 'ordinal' : 'cardinal'
|
|
1104
|
-
const pluralCategories = new Intl.PluralRules(this.config.extract?.primaryLanguage, { type }).resolvedOptions().pluralCategories
|
|
1105
|
-
const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
|
|
1106
|
-
|
|
1107
|
-
// Get plural-specific default values from tOptions if available
|
|
1108
|
-
let otherDefault: string | undefined
|
|
1109
|
-
let ordinalOtherDefault: string | undefined
|
|
1110
|
-
|
|
1111
|
-
if (optionsNode) {
|
|
1112
|
-
otherDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}other`) as string | undefined
|
|
1113
|
-
ordinalOtherDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`) as string | undefined
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
for (const category of pluralCategories) {
|
|
1117
|
-
// Look for the most specific default value (e.g., defaultValue_ordinal_one)
|
|
1118
|
-
const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`
|
|
1119
|
-
const specificDefault = optionsNode ? getObjectPropValue(optionsNode, specificDefaultKey) as string | undefined : undefined
|
|
1120
|
-
|
|
1121
|
-
// Determine the final default value using a clear fallback chain
|
|
1122
|
-
let finalDefaultValue: string | undefined
|
|
1123
|
-
if (typeof specificDefault === 'string') {
|
|
1124
|
-
// 1. Use the most specific default if it exists (e.g., defaultValue_one)
|
|
1125
|
-
finalDefaultValue = specificDefault
|
|
1126
|
-
} else if (category === 'one' && typeof defaultValue === 'string') {
|
|
1127
|
-
// 2. SPECIAL CASE: The 'one' category falls back to the main default value (children content)
|
|
1128
|
-
finalDefaultValue = defaultValue
|
|
1129
|
-
} else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
|
|
1130
|
-
// 3a. Other ordinal categories fall back to 'defaultValue_ordinal_other'
|
|
1131
|
-
finalDefaultValue = ordinalOtherDefault
|
|
1132
|
-
} else if (!isOrdinal && typeof otherDefault === 'string') {
|
|
1133
|
-
// 3b. Other cardinal categories fall back to 'defaultValue_other'
|
|
1134
|
-
finalDefaultValue = otherDefault
|
|
1135
|
-
} else if (typeof defaultValue === 'string') {
|
|
1136
|
-
// 4. If no '_other' is found, all categories can fall back to the main default value
|
|
1137
|
-
finalDefaultValue = defaultValue
|
|
1138
|
-
} else {
|
|
1139
|
-
// 5. Final fallback to the base key itself
|
|
1140
|
-
finalDefaultValue = key
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
const finalKey = isOrdinal
|
|
1144
|
-
? `${key}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
1145
|
-
: `${key}${pluralSeparator}${category}`
|
|
1146
|
-
|
|
1147
|
-
this.pluginContext.addKey({
|
|
1148
|
-
key: finalKey,
|
|
1149
|
-
ns,
|
|
1150
|
-
defaultValue: finalDefaultValue,
|
|
1151
|
-
hasCount: true,
|
|
1152
|
-
isOrdinal
|
|
1153
|
-
})
|
|
1154
|
-
}
|
|
1155
|
-
} catch (e) {
|
|
1156
|
-
this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`)
|
|
1157
|
-
// Fallback to a simple key if Intl API fails
|
|
1158
|
-
this.pluginContext.addKey({ key, ns, defaultValue })
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
/**
|
|
1163
|
-
* Extracts element name from JSX opening tag.
|
|
1164
|
-
*
|
|
1165
|
-
* Handles both simple identifiers and member expressions:
|
|
1166
|
-
* - `<Trans>` → 'Trans'
|
|
1167
|
-
* - `<React.Trans>` → 'React.Trans'
|
|
1168
|
-
*
|
|
1169
|
-
* @param node - JSX element node
|
|
1170
|
-
* @returns Element name or undefined if not extractable
|
|
1171
|
-
*
|
|
1172
|
-
* @private
|
|
1173
|
-
*/
|
|
1174
|
-
private getElementName (node: JSXElement): string | undefined {
|
|
1175
|
-
if (node.opening.name.type === 'Identifier') {
|
|
1176
|
-
return node.opening.name.value
|
|
1177
|
-
} else if (node.opening.name.type === 'JSXMemberExpression') {
|
|
1178
|
-
let curr: any = node.opening.name
|
|
1179
|
-
const names: string[] = []
|
|
1180
|
-
while (curr.type === 'JSXMemberExpression') {
|
|
1181
|
-
if (curr.property.type === 'Identifier') names.unshift(curr.property.value)
|
|
1182
|
-
curr = curr.object
|
|
1183
|
-
}
|
|
1184
|
-
if (curr.type === 'Identifier') names.unshift(curr.value)
|
|
1185
|
-
return names.join('.')
|
|
1186
|
-
}
|
|
1187
|
-
return undefined
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* Extracts translation key from selector API arrow function.
|
|
1192
|
-
*
|
|
1193
|
-
* Processes selector expressions like:
|
|
1194
|
-
* - `$ => $.path.to.key` → 'path.to.key'
|
|
1195
|
-
* - `$ => $.app['title'].main` → 'app.title.main'
|
|
1196
|
-
* - `$ => { return $.nested.key; }` → 'nested.key'
|
|
1197
|
-
*
|
|
1198
|
-
* Handles both dot notation and bracket notation, respecting
|
|
1199
|
-
* the configured key separator or flat key structure.
|
|
1200
|
-
*
|
|
1201
|
-
* @param node - Arrow function expression from selector call
|
|
1202
|
-
* @returns Extracted key path or null if not statically analyzable
|
|
1203
|
-
*
|
|
1204
|
-
* @private
|
|
1205
|
-
*/
|
|
1206
|
-
private extractKeyFromSelector (node: ArrowFunctionExpression): string | null {
|
|
1207
|
-
let body = node.body
|
|
1208
|
-
|
|
1209
|
-
// Handle block bodies, e.g., $ => { return $.key; }
|
|
1210
|
-
if (body.type === 'BlockStatement') {
|
|
1211
|
-
const returnStmt = body.stmts.find(s => s.type === 'ReturnStatement')
|
|
1212
|
-
if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
|
|
1213
|
-
body = returnStmt.argument
|
|
1214
|
-
} else {
|
|
1215
|
-
return null
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
let current = body
|
|
1220
|
-
const parts: string[] = []
|
|
1221
|
-
|
|
1222
|
-
// Recursively walk down MemberExpressions
|
|
1223
|
-
while (current.type === 'MemberExpression') {
|
|
1224
|
-
const prop = current.property
|
|
1225
|
-
|
|
1226
|
-
if (prop.type === 'Identifier') {
|
|
1227
|
-
// This handles dot notation: .key
|
|
1228
|
-
parts.unshift(prop.value)
|
|
1229
|
-
} else if (prop.type === 'Computed' && prop.expression.type === 'StringLiteral') {
|
|
1230
|
-
// This handles bracket notation: ['key']
|
|
1231
|
-
parts.unshift(prop.expression.value)
|
|
1232
|
-
} else {
|
|
1233
|
-
// This is a dynamic property like [myVar] or a private name, which we cannot resolve.
|
|
1234
|
-
return null
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
current = current.object
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (parts.length > 0) {
|
|
1241
|
-
const keySeparator = this.config.extract.keySeparator
|
|
1242
|
-
const joiner = typeof keySeparator === 'string' ? keySeparator : '.'
|
|
1243
|
-
return parts.join(joiner)
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
return null
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
/**
|
|
1250
|
-
* Resolves an expression to one or more possible context string values that can be
|
|
1251
|
-
* determined statically from the AST. This is a wrapper around the plugin hook
|
|
1252
|
-
* `extractContextFromExpression` and {@link resolvePossibleStringValuesFromExpression}.
|
|
1253
|
-
*
|
|
1254
|
-
* @param expression - The SWC AST expression node to resolve
|
|
1255
|
-
* @returns An array of possible context string values that the expression may produce.
|
|
1256
|
-
*
|
|
1257
|
-
* @private
|
|
1258
|
-
*/
|
|
1259
|
-
private resolvePossibleContextStringValues (expression: Expression) {
|
|
1260
|
-
const strings = this.hooks.resolvePossibleContextStringValues?.(expression) ?? []
|
|
1261
|
-
|
|
1262
|
-
return [...strings, ...this.resolvePossibleStringValuesFromExpression(expression)]
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
/**
|
|
1266
|
-
* Resolves an expression to one or more possible key string values that can be
|
|
1267
|
-
* determined statically from the AST. This is a wrapper around the plugin hook
|
|
1268
|
-
* `extractKeysFromExpression` and {@link resolvePossibleStringValuesFromExpression}.
|
|
1269
|
-
*
|
|
1270
|
-
* @param expression - The SWC AST expression node to resolve
|
|
1271
|
-
* @returns An array of possible key string values that the expression may produce.
|
|
1272
|
-
*
|
|
1273
|
-
* @private
|
|
1274
|
-
*/
|
|
1275
|
-
private resolvePossibleKeyStringValues (expression: Expression) {
|
|
1276
|
-
const strings = this.hooks.resolvePossibleKeyStringValues?.(expression) ?? []
|
|
1277
|
-
|
|
1278
|
-
return [...strings, ...this.resolvePossibleStringValuesFromExpression(expression)]
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* Resolves an expression to one or more possible string values that can be
|
|
1283
|
-
* determined statically from the AST.
|
|
1284
|
-
*
|
|
1285
|
-
* Supports:
|
|
1286
|
-
* - StringLiteral -> single value (filtered to exclude empty strings for context)
|
|
1287
|
-
* - NumericLiteral -> single value
|
|
1288
|
-
* - BooleanLiteral -> single value
|
|
1289
|
-
* - ConditionalExpression (ternary) -> union of consequent and alternate resolved values
|
|
1290
|
-
* - TemplateLiteral -> union of all possible string values
|
|
1291
|
-
* - The identifier `undefined` -> empty array
|
|
1292
|
-
*
|
|
1293
|
-
* For any other expression types (identifiers, function calls, member expressions,
|
|
1294
|
-
* etc.) the value cannot be determined statically and an empty array is returned.
|
|
1295
|
-
*
|
|
1296
|
-
* @private
|
|
1297
|
-
* @param expression - The SWC AST expression node to resolve
|
|
1298
|
-
* @param returnEmptyStrings - Whether to include empty strings in the result
|
|
1299
|
-
* @returns An array of possible string values that the expression may produce.
|
|
1300
|
-
*/
|
|
1301
|
-
private resolvePossibleStringValuesFromExpression (expression: Expression, returnEmptyStrings = false): string[] {
|
|
1302
|
-
if (expression.type === 'StringLiteral') {
|
|
1303
|
-
// Filter out empty strings as they should be treated as "no context" like i18next does
|
|
1304
|
-
return expression.value || returnEmptyStrings ? [expression.value] : []
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
if (expression.type === 'ConditionalExpression') { // This is a ternary operator
|
|
1308
|
-
const consequentValues = this.resolvePossibleStringValuesFromExpression(expression.consequent, returnEmptyStrings)
|
|
1309
|
-
const alternateValues = this.resolvePossibleStringValuesFromExpression(expression.alternate, returnEmptyStrings)
|
|
1310
|
-
return [...consequentValues, ...alternateValues]
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
if (expression.type === 'Identifier' && expression.value === 'undefined') {
|
|
1314
|
-
return [] // Handle the `undefined` case
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (expression.type === 'TemplateLiteral') {
|
|
1318
|
-
return this.resolvePossibleStringValuesFromTemplateString(expression)
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
if (expression.type === 'NumericLiteral' || expression.type === 'BooleanLiteral') {
|
|
1322
|
-
return [`${expression.value}`] // Handle literals like 5 or true
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
// Support building translation keys for
|
|
1326
|
-
// `variable satisfies 'coaching' | 'therapy'`
|
|
1327
|
-
if (expression.type === 'TsSatisfiesExpression' || expression.type === 'TsAsExpression') {
|
|
1328
|
-
const annotation = expression.typeAnnotation
|
|
1329
|
-
|
|
1330
|
-
return this.resolvePossibleStringValuesFromType(annotation, returnEmptyStrings)
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
1334
|
-
return []
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
private resolvePossibleStringValuesFromType (type: TsType, returnEmptyStrings = false): string[] {
|
|
1338
|
-
if (type.type === 'TsUnionType') {
|
|
1339
|
-
return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings))
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
if (type.type === 'TsLiteralType') {
|
|
1343
|
-
if (type.literal.type === 'StringLiteral') {
|
|
1344
|
-
// Filter out empty strings as they should be treated as "no context" like i18next does
|
|
1345
|
-
return type.literal.value || returnEmptyStrings ? [type.literal.value] : []
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
if (type.literal.type === 'TemplateLiteral') {
|
|
1349
|
-
return this.resolvePossibleStringValuesFromTemplateLiteralType(type.literal)
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
if (type.literal.type === 'NumericLiteral' || type.literal.type === 'BooleanLiteral') {
|
|
1353
|
-
return [`${type.literal.value}`] // Handle literals like 5 or true
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
1358
|
-
return []
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
/**
|
|
1362
|
-
* Resolves a template literal string to one or more possible strings that can be
|
|
1363
|
-
* determined statically from the AST.
|
|
1364
|
-
*
|
|
1365
|
-
* @private
|
|
1366
|
-
* @param templateString - The SWC AST template literal string to resolve
|
|
1367
|
-
* @returns An array of possible string values that the template may produce.
|
|
1368
|
-
*/
|
|
1369
|
-
private resolvePossibleStringValuesFromTemplateString (templateString: TemplateLiteral): string[] {
|
|
1370
|
-
// If there are no expressions, we can just return the cooked value
|
|
1371
|
-
if (templateString.quasis.length === 1 && templateString.expressions.length === 0) {
|
|
1372
|
-
// Ex. `translation.key.no.substitution`
|
|
1373
|
-
return [templateString.quasis[0].cooked || '']
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// Ex. `translation.key.with.expression.${x ? 'title' : 'description'}`
|
|
1377
|
-
const [firstQuasis, ...tails] = templateString.quasis
|
|
1378
|
-
|
|
1379
|
-
const stringValues = templateString.expressions.reduce(
|
|
1380
|
-
(heads, expression, i) => {
|
|
1381
|
-
return heads.flatMap((head) => {
|
|
1382
|
-
const tail = tails[i]?.cooked ?? ''
|
|
1383
|
-
return this.resolvePossibleStringValuesFromExpression(expression, true).map(
|
|
1384
|
-
(expressionValue) => `${head}${expressionValue}${tail}`
|
|
1385
|
-
)
|
|
1386
|
-
})
|
|
1387
|
-
},
|
|
1388
|
-
[firstQuasis.cooked ?? '']
|
|
1389
|
-
)
|
|
1390
|
-
|
|
1391
|
-
return stringValues
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
/**
|
|
1395
|
-
* Resolves a template literal type to one or more possible strings that can be
|
|
1396
|
-
* determined statically from the AST.
|
|
1397
|
-
*
|
|
1398
|
-
* @private
|
|
1399
|
-
* @param templateLiteralType - The SWC AST template literal type to resolve
|
|
1400
|
-
* @returns An array of possible string values that the template may produce.
|
|
1401
|
-
*/
|
|
1402
|
-
private resolvePossibleStringValuesFromTemplateLiteralType (templateLiteralType: TsTemplateLiteralType): string[] {
|
|
1403
|
-
// If there are no types, we can just return the cooked value
|
|
1404
|
-
if (templateLiteralType.quasis.length === 1 && templateLiteralType.types.length === 0) {
|
|
1405
|
-
// Ex. `translation.key.no.substitution`
|
|
1406
|
-
return [templateLiteralType.quasis[0].cooked || '']
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// Ex. `translation.key.with.expression.${'title' | 'description'}`
|
|
1410
|
-
const [firstQuasis, ...tails] = templateLiteralType.quasis
|
|
1411
|
-
|
|
1412
|
-
const stringValues = templateLiteralType.types.reduce(
|
|
1413
|
-
(heads, type, i) => {
|
|
1414
|
-
return heads.flatMap((head) => {
|
|
1415
|
-
const tail = tails[i]?.cooked ?? ''
|
|
1416
|
-
return this.resolvePossibleStringValuesFromType(type, true).map(
|
|
1417
|
-
(expressionValue) => `${head}${expressionValue}${tail}`
|
|
1418
|
-
)
|
|
1419
|
-
})
|
|
1420
|
-
},
|
|
1421
|
-
[firstQuasis.cooked ?? '']
|
|
1422
|
-
)
|
|
1423
|
-
|
|
1424
|
-
return stringValues
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
/**
|
|
1428
|
-
* Finds the configuration for a given useTranslation function name.
|
|
1429
|
-
* Applies default argument positions if none are specified.
|
|
1430
|
-
*
|
|
1431
|
-
* @param name - The identifier name to look up in the configured useTranslationNames
|
|
1432
|
-
* @returns The resolved UseTranslationHookConfig when a match is found, otherwise undefined
|
|
1433
|
-
*/
|
|
1434
|
-
private getUseTranslationConfig (name: string): UseTranslationHookConfig | undefined {
|
|
1435
|
-
const useTranslationNames = this.config.extract.useTranslationNames || ['useTranslation']
|
|
1436
|
-
|
|
1437
|
-
for (const item of useTranslationNames) {
|
|
1438
|
-
if (typeof item === 'string' && item === name) {
|
|
1439
|
-
// Default behavior for simple string entries
|
|
1440
|
-
return { name, nsArg: 0, keyPrefixArg: 1 }
|
|
1441
|
-
}
|
|
1442
|
-
if (typeof item === 'object' && item.name === name) {
|
|
1443
|
-
// Custom configuration with specified or default argument positions
|
|
1444
|
-
return {
|
|
1445
|
-
name: item.name,
|
|
1446
|
-
nsArg: item.nsArg ?? 0,
|
|
1447
|
-
keyPrefixArg: item.keyPrefixArg ?? 1,
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
return undefined
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
/**
|
|
1455
|
-
* Serializes a callee node (Identifier or MemberExpression) into a string.
|
|
1456
|
-
*
|
|
1457
|
-
* Produces a dotted name for simple callees that can be used for scope lookups
|
|
1458
|
-
* or configuration matching.
|
|
1459
|
-
*
|
|
1460
|
-
* Supported inputs:
|
|
1461
|
-
* - Identifier: returns the identifier name (e.g., `t` -> "t")
|
|
1462
|
-
* - MemberExpression with Identifier parts: returns a dotted path of identifiers
|
|
1463
|
-
* (e.g., `i18n.t` -> "i18n.t", `i18n.getFixedT` -> "i18n.getFixedT")
|
|
1464
|
-
*
|
|
1465
|
-
* Behavior notes:
|
|
1466
|
-
* - Computed properties are not supported and cause this function to return null
|
|
1467
|
-
* (e.g., `i18n['t']` -> null).
|
|
1468
|
-
* - The base of a MemberExpression must be a simple Identifier. More complex bases
|
|
1469
|
-
* (other expressions, `this`, etc.) will result in null.
|
|
1470
|
-
* - This function does not attempt to resolve or evaluate expressions — it only
|
|
1471
|
-
* serializes static identifier/member chains.
|
|
1472
|
-
*
|
|
1473
|
-
* Examples:
|
|
1474
|
-
* - Identifier callee: { type: 'Identifier', value: 't' } -> "t"
|
|
1475
|
-
* - Member callee: { type: 'MemberExpression', object: { type: 'Identifier', value: 'i18n' }, property: { type: 'Identifier', value: 't' } } -> "i18n.t"
|
|
1476
|
-
*
|
|
1477
|
-
* @param callee - The CallExpression callee node to serialize
|
|
1478
|
-
* @returns A dotted string name for supported callees, or null when the callee
|
|
1479
|
-
* is a computed/unsupported expression.
|
|
1480
|
-
*
|
|
1481
|
-
* @private
|
|
1482
|
-
*/
|
|
1483
|
-
private getFunctionName (callee: CallExpression['callee']): string | null {
|
|
1484
|
-
if (callee.type === 'Identifier') {
|
|
1485
|
-
return callee.value
|
|
1486
|
-
}
|
|
1487
|
-
if (callee.type === 'MemberExpression') {
|
|
1488
|
-
const parts: string[] = []
|
|
1489
|
-
let current: any = callee
|
|
1490
|
-
while (current.type === 'MemberExpression') {
|
|
1491
|
-
if (current.property.type === 'Identifier') {
|
|
1492
|
-
parts.unshift(current.property.value)
|
|
1493
|
-
} else {
|
|
1494
|
-
return null // Cannot handle computed properties like i18n['t']
|
|
1495
|
-
}
|
|
1496
|
-
current = current.object
|
|
1497
|
-
}
|
|
1498
|
-
// Handle `this` as the base of the expression (e.g., this._i18n.t)
|
|
1499
|
-
if (current.type === 'ThisExpression') {
|
|
1500
|
-
parts.unshift('this')
|
|
1501
|
-
} else if (current.type === 'Identifier') {
|
|
1502
|
-
parts.unshift(current.value)
|
|
1503
|
-
} else {
|
|
1504
|
-
return null // Base of the expression is not a simple identifier
|
|
1505
|
-
}
|
|
1506
|
-
return parts.join('.')
|
|
1507
|
-
}
|
|
1508
|
-
return null
|
|
1509
|
-
}
|
|
1510
|
-
}
|