i18next-cli 1.24.13 → 1.24.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/cjs/cli.js +1 -1
  2. package/dist/cjs/rename-key.js +1 -1
  3. package/dist/esm/cli.js +1 -1
  4. package/dist/esm/rename-key.js +1 -1
  5. package/package.json +6 -6
  6. package/types/cli.d.ts +3 -1
  7. package/types/cli.d.ts.map +1 -1
  8. package/CHANGELOG.md +0 -599
  9. package/src/cli.ts +0 -283
  10. package/src/config.ts +0 -215
  11. package/src/extractor/core/ast-visitors.ts +0 -259
  12. package/src/extractor/core/extractor.ts +0 -250
  13. package/src/extractor/core/key-finder.ts +0 -142
  14. package/src/extractor/core/translation-manager.ts +0 -750
  15. package/src/extractor/index.ts +0 -7
  16. package/src/extractor/parsers/ast-utils.ts +0 -87
  17. package/src/extractor/parsers/call-expression-handler.ts +0 -793
  18. package/src/extractor/parsers/comment-parser.ts +0 -424
  19. package/src/extractor/parsers/expression-resolver.ts +0 -391
  20. package/src/extractor/parsers/jsx-handler.ts +0 -488
  21. package/src/extractor/parsers/jsx-parser.ts +0 -1463
  22. package/src/extractor/parsers/scope-manager.ts +0 -445
  23. package/src/extractor/plugin-manager.ts +0 -116
  24. package/src/extractor.ts +0 -15
  25. package/src/heuristic-config.ts +0 -92
  26. package/src/index.ts +0 -22
  27. package/src/init.ts +0 -175
  28. package/src/linter.ts +0 -345
  29. package/src/locize.ts +0 -263
  30. package/src/migrator.ts +0 -208
  31. package/src/rename-key.ts +0 -398
  32. package/src/status.ts +0 -380
  33. package/src/syncer.ts +0 -133
  34. package/src/types-generator.ts +0 -139
  35. package/src/types.ts +0 -577
  36. package/src/utils/default-value.ts +0 -45
  37. package/src/utils/file-utils.ts +0 -167
  38. package/src/utils/funnel-msg-tracker.ts +0 -84
  39. package/src/utils/logger.ts +0 -36
  40. package/src/utils/nested-object.ts +0 -135
  41. 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
- }
@@ -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'