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