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.
Files changed (39) hide show
  1. package/dist/cjs/cli.js +1 -1
  2. package/dist/esm/cli.js +1 -1
  3. package/package.json +6 -6
  4. package/types/cli.d.ts +3 -1
  5. package/types/cli.d.ts.map +1 -1
  6. package/CHANGELOG.md +0 -599
  7. package/src/cli.ts +0 -283
  8. package/src/config.ts +0 -215
  9. package/src/extractor/core/ast-visitors.ts +0 -259
  10. package/src/extractor/core/extractor.ts +0 -250
  11. package/src/extractor/core/key-finder.ts +0 -142
  12. package/src/extractor/core/translation-manager.ts +0 -750
  13. package/src/extractor/index.ts +0 -7
  14. package/src/extractor/parsers/ast-utils.ts +0 -87
  15. package/src/extractor/parsers/call-expression-handler.ts +0 -793
  16. package/src/extractor/parsers/comment-parser.ts +0 -424
  17. package/src/extractor/parsers/expression-resolver.ts +0 -391
  18. package/src/extractor/parsers/jsx-handler.ts +0 -488
  19. package/src/extractor/parsers/jsx-parser.ts +0 -1463
  20. package/src/extractor/parsers/scope-manager.ts +0 -445
  21. package/src/extractor/plugin-manager.ts +0 -116
  22. package/src/extractor.ts +0 -15
  23. package/src/heuristic-config.ts +0 -92
  24. package/src/index.ts +0 -22
  25. package/src/init.ts +0 -175
  26. package/src/linter.ts +0 -345
  27. package/src/locize.ts +0 -263
  28. package/src/migrator.ts +0 -208
  29. package/src/rename-key.ts +0 -398
  30. package/src/status.ts +0 -380
  31. package/src/syncer.ts +0 -133
  32. package/src/types-generator.ts +0 -139
  33. package/src/types.ts +0 -577
  34. package/src/utils/default-value.ts +0 -45
  35. package/src/utils/file-utils.ts +0 -167
  36. package/src/utils/funnel-msg-tracker.ts +0 -84
  37. package/src/utils/logger.ts +0 -36
  38. package/src/utils/nested-object.ts +0 -135
  39. package/src/utils/validation.ts +0 -72
@@ -1,259 +0,0 @@
1
- import type { Module, Node } from '@swc/core'
2
- import type { PluginContext, I18nextToolkitConfig, Logger, ASTVisitorHooks, ScopeInfo } from '../../types'
3
- import { ScopeManager } from '../parsers/scope-manager'
4
- import { ExpressionResolver } from '../parsers/expression-resolver'
5
- import { CallExpressionHandler } from '../parsers/call-expression-handler'
6
- import { JSXHandler } from '../parsers/jsx-handler'
7
-
8
- /**
9
- * AST visitor class that traverses JavaScript/TypeScript syntax trees to extract translation keys.
10
- *
11
- * This class implements a manual recursive walker that:
12
- * - Maintains scope information for tracking useTranslation and getFixedT calls
13
- * - Extracts keys from t() function calls with various argument patterns
14
- * - Handles JSX Trans components with complex children serialization
15
- * - Supports both string literals and selector API for type-safe keys
16
- * - Processes pluralization and context variants
17
- * - Manages namespace resolution from multiple sources
18
- *
19
- * The visitor respects configuration options for separators, function names,
20
- * component names, and other extraction settings.
21
- *
22
- * @example
23
- * ```typescript
24
- * const visitors = new ASTVisitors(config, pluginContext, logger)
25
- * visitors.visit(parsedAST)
26
- *
27
- * // The pluginContext will now contain all extracted keys
28
- * ```
29
- */
30
- export class ASTVisitors {
31
- private readonly pluginContext: PluginContext
32
- private readonly config: Omit<I18nextToolkitConfig, 'plugins'>
33
- private readonly logger: Logger
34
- private hooks: ASTVisitorHooks
35
-
36
- public get objectKeys () {
37
- return this.callExpressionHandler.objectKeys
38
- }
39
-
40
- private readonly scopeManager: ScopeManager
41
- private readonly expressionResolver: ExpressionResolver
42
- private readonly callExpressionHandler: CallExpressionHandler
43
- private readonly jsxHandler: JSXHandler
44
- private currentFile: string = ''
45
- private currentCode: string = ''
46
-
47
- /**
48
- * Creates a new AST visitor instance.
49
- *
50
- * @param config - Toolkit configuration with extraction settings
51
- * @param pluginContext - Context for adding discovered translation keys
52
- * @param logger - Logger for warnings and debug information
53
- */
54
- constructor (
55
- config: Omit<I18nextToolkitConfig, 'plugins'>,
56
- pluginContext: PluginContext,
57
- logger: Logger,
58
- hooks?: ASTVisitorHooks,
59
- expressionResolver?: ExpressionResolver
60
- ) {
61
- this.pluginContext = pluginContext
62
- this.config = config
63
- this.logger = logger
64
- this.hooks = {
65
- onBeforeVisitNode: hooks?.onBeforeVisitNode,
66
- onAfterVisitNode: hooks?.onAfterVisitNode,
67
- resolvePossibleKeyStringValues: hooks?.resolvePossibleKeyStringValues,
68
- resolvePossibleContextStringValues: hooks?.resolvePossibleContextStringValues
69
- }
70
-
71
- this.scopeManager = new ScopeManager(config)
72
- // use shared resolver when provided so captured enums/objects are visible across files
73
- this.expressionResolver = expressionResolver ?? new ExpressionResolver(this.hooks)
74
- this.callExpressionHandler = new CallExpressionHandler(
75
- config,
76
- pluginContext,
77
- logger,
78
- this.expressionResolver,
79
- () => this.getCurrentFile(),
80
- () => this.getCurrentCode()
81
- )
82
- this.jsxHandler = new JSXHandler(
83
- config,
84
- pluginContext,
85
- this.expressionResolver,
86
- () => this.getCurrentFile(),
87
- () => this.getCurrentCode()
88
- )
89
- }
90
-
91
- /**
92
- * Main entry point for AST traversal.
93
- * Creates a root scope and begins the recursive walk through the syntax tree.
94
- *
95
- * @param node - The root module node to traverse
96
- */
97
- public visit (node: Module): void {
98
- // Reset any per-file scope state to avoid leaking scopes between files.
99
- this.scopeManager.reset()
100
- // Reset per-file captured variables in the expression resolver so variables from other files don't leak.
101
- this.expressionResolver.resetFileSymbols()
102
- this.scopeManager.enterScope() // Create the root scope for the file
103
- this.walk(node)
104
- this.scopeManager.exitScope() // Clean up the root scope
105
- }
106
-
107
- /**
108
- * Recursively walks through AST nodes, handling scoping and visiting logic.
109
- *
110
- * This is the core traversal method that:
111
- * 1. Manages function scopes (enter/exit)
112
- * 2. Dispatches to specific handlers based on node type
113
- * 3. Recursively processes child nodes
114
- * 4. Maintains proper scope cleanup
115
- *
116
- * @param node - The current AST node to process
117
- *
118
- * @private
119
- */
120
- private walk (node: Node | any): void {
121
- if (!node) return
122
-
123
- let isNewScope = false
124
- // ENTER SCOPE for functions
125
- if (node.type === 'Function' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
126
- this.scopeManager.enterScope()
127
- isNewScope = true
128
- }
129
-
130
- this.hooks.onBeforeVisitNode?.(node)
131
-
132
- // --- VISIT LOGIC ---
133
- // Handle specific node types
134
- switch (node.type) {
135
- case 'VariableDeclarator':
136
- this.scopeManager.handleVariableDeclarator(node)
137
- // Capture simple variable initializers so the expressionResolver can
138
- // resolve identifiers / member expressions that reference them.
139
- this.expressionResolver.captureVariableDeclarator(node)
140
- break
141
- case 'TSEnumDeclaration':
142
- case 'TsEnumDeclaration':
143
- case 'TsEnumDecl':
144
- // capture enums into resolver symbol table
145
- this.expressionResolver.captureEnumDeclaration(node)
146
- break
147
- case 'CallExpression':
148
- this.callExpressionHandler.handleCallExpression(node, this.scopeManager.getVarFromScope.bind(this.scopeManager))
149
- break
150
- case 'JSXElement':
151
- this.jsxHandler.handleJSXElement(node, this.scopeManager.getVarFromScope.bind(this.scopeManager))
152
- break
153
- }
154
-
155
- this.hooks.onAfterVisitNode?.(node)
156
-
157
- // --- END VISIT LOGIC ---
158
-
159
- // --- RECURSION ---
160
- // Recurse into the children of the current node
161
- for (const key in node) {
162
- if (key === 'span') continue
163
-
164
- const child = node[key]
165
- if (Array.isArray(child)) {
166
- // Pre-scan array children to register VariableDeclarator-based scopes
167
- // (e.g., `const { t } = useTranslation(...)`) before walking the rest
168
- // of the items. This ensures that functions/arrow-functions defined
169
- // earlier in the same block that reference t will resolve to the
170
- // correct scope even if the `useTranslation` declarator appears later.
171
- for (const item of child) {
172
- if (!item || typeof item !== 'object') continue
173
-
174
- // Direct declarator present in arrays (rare)
175
- if (item.type === 'VariableDeclarator') {
176
- this.scopeManager.handleVariableDeclarator(item)
177
- this.expressionResolver.captureVariableDeclarator(item)
178
- continue
179
- }
180
- // enum declarations can appear as ExportDeclaration.declaration earlier; be permissive
181
- if (item && item.id && Array.isArray(item.members)) {
182
- this.expressionResolver.captureEnumDeclaration(item)
183
- // continue to allow further traversal
184
- }
185
- // Common case: VariableDeclaration which contains .declarations (VariableDeclarator[])
186
- if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
187
- for (const decl of item.declarations) {
188
- if (decl && typeof decl === 'object' && decl.type === 'VariableDeclarator') {
189
- this.scopeManager.handleVariableDeclarator(decl)
190
- this.expressionResolver.captureVariableDeclarator(decl)
191
- }
192
- }
193
- }
194
- }
195
- for (const item of child) {
196
- // Be less strict: if it's a non-null object, walk it.
197
- // This allows traversal into nodes that might not have a `.type` property
198
- // but still contain other valid AST nodes.
199
- if (item && typeof item === 'object') {
200
- this.walk(item)
201
- }
202
- }
203
- } else if (child && typeof child === 'object') {
204
- // The condition for single objects should be the same as for array items.
205
- // Do not require `child.type`. This allows traversal into class method bodies.
206
- this.walk(child)
207
- }
208
- }
209
- // --- END RECURSION ---
210
-
211
- // LEAVE SCOPE for functions
212
- if (isNewScope) {
213
- this.scopeManager.exitScope()
214
- }
215
- }
216
-
217
- /**
218
- * Retrieves variable information from the scope chain.
219
- * Searches from innermost to outermost scope.
220
- *
221
- * @param name - Variable name to look up
222
- * @returns Scope information if found, undefined otherwise
223
- *
224
- * @private
225
- */
226
- public getVarFromScope (name: string): ScopeInfo | undefined {
227
- return this.scopeManager.getVarFromScope(name)
228
- }
229
-
230
- /**
231
- * Sets the current file path and code used by the extractor.
232
- * Also resets the search index for location tracking.
233
- */
234
- public setCurrentFile (file: string, code: string): void {
235
- this.currentFile = file
236
- this.currentCode = code
237
- // Reset search indexes when processing a new file
238
- this.callExpressionHandler.resetSearchIndex()
239
- this.jsxHandler.resetSearchIndex()
240
- }
241
-
242
- /**
243
- * Returns the currently set file path.
244
- *
245
- * @returns The current file path as a string, or `undefined` if no file has been set.
246
- * @remarks
247
- * Use this to retrieve the file context that was previously set via `setCurrentFile`.
248
- */
249
- public getCurrentFile (): string {
250
- return this.currentFile
251
- }
252
-
253
- /**
254
- * @returns The full source code string for the file currently under processing.
255
- */
256
- public getCurrentCode (): string {
257
- return this.currentCode
258
- }
259
- }
@@ -1,250 +0,0 @@
1
- import ora from 'ora'
2
- import chalk from 'chalk'
3
- import { parse } from '@swc/core'
4
- import { mkdir, readFile, writeFile } from 'node:fs/promises'
5
- import { dirname, extname } from 'node:path'
6
- import type { Logger, I18nextToolkitConfig, Plugin, PluginContext } from '../../types'
7
- import { findKeys } from './key-finder'
8
- import { getTranslations } from './translation-manager'
9
- import { validateExtractorConfig, ExtractorError } from '../../utils/validation'
10
- import { extractKeysFromComments } from '../parsers/comment-parser'
11
- import { ASTVisitors } from './ast-visitors'
12
- import { ConsoleLogger } from '../../utils/logger'
13
- import { serializeTranslationFile } from '../../utils/file-utils'
14
- import { shouldShowFunnel, recordFunnelShown } from '../../utils/funnel-msg-tracker'
15
-
16
- /**
17
- * Main extractor function that runs the complete key extraction and file generation process.
18
- *
19
- * This is the primary entry point that:
20
- * 1. Validates configuration
21
- * 2. Sets up default sync options
22
- * 3. Finds all translation keys across source files
23
- * 4. Generates/updates translation files for all locales
24
- * 5. Provides progress feedback via spinner
25
- * 6. Returns whether any files were updated
26
- *
27
- * @param config - The i18next toolkit configuration object
28
- * @param logger - Logger instance for output (defaults to ConsoleLogger)
29
- * @returns Promise resolving to boolean indicating if any files were updated
30
- *
31
- * @throws {ExtractorError} When configuration validation fails or extraction process encounters errors
32
- *
33
- * @example
34
- * ```typescript
35
- * const config = await loadConfig()
36
- * const updated = await runExtractor(config)
37
- * if (updated) {
38
- * console.log('Translation files were updated')
39
- * }
40
- * ```
41
- */
42
- export async function runExtractor (
43
- config: I18nextToolkitConfig,
44
- {
45
- isWatchMode = false,
46
- isDryRun = false,
47
- syncPrimaryWithDefaults = false
48
- }: {
49
- isWatchMode?: boolean,
50
- isDryRun?: boolean,
51
- syncPrimaryWithDefaults?: boolean,
52
- } = {},
53
- logger: Logger = new ConsoleLogger()
54
- ): Promise<boolean> {
55
- config.extract.primaryLanguage ||= config.locales[0] || 'en'
56
- config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
57
-
58
- // Ensure default function and component names are set if not provided.
59
- config.extract.functions ||= ['t', '*.t']
60
- config.extract.transComponents ||= ['Trans']
61
-
62
- validateExtractorConfig(config)
63
-
64
- const plugins = config.plugins || []
65
-
66
- const spinner = ora('Running i18next key extractor...\n').start()
67
-
68
- try {
69
- const { allKeys, objectKeys } = await findKeys(config, logger)
70
- spinner.text = `Found ${allKeys.size} unique keys. Updating translation files...`
71
-
72
- const results = await getTranslations(allKeys, objectKeys, config, { syncPrimaryWithDefaults })
73
-
74
- let anyFileUpdated = false
75
- for (const result of results) {
76
- if (result.updated) {
77
- anyFileUpdated = true
78
- // Only write files if it's not a dry run.
79
- if (!isDryRun) {
80
- const fileContent = serializeTranslationFile(
81
- result.newTranslations,
82
- config.extract.outputFormat,
83
- config.extract.indentation
84
- )
85
- await mkdir(dirname(result.path), { recursive: true })
86
- await writeFile(result.path, fileContent)
87
- logger.info(chalk.green(`Updated: ${result.path}`))
88
- }
89
- }
90
- }
91
-
92
- // Run afterSync hooks from plugins
93
- if (plugins.length > 0) {
94
- spinner.text = 'Running post-extraction plugins...'
95
- for (const plugin of plugins) {
96
- await plugin.afterSync?.(results, config)
97
- }
98
- }
99
-
100
- spinner.succeed(chalk.bold('Extraction complete!'))
101
-
102
- // Show the funnel message only if files were actually changed.
103
- if (anyFileUpdated) await printLocizeFunnel()
104
-
105
- return anyFileUpdated
106
- } catch (error) {
107
- spinner.fail(chalk.red('Extraction failed.'))
108
- // Re-throw or handle error
109
- throw error
110
- }
111
- }
112
-
113
- /**
114
- * Processes an individual source file for translation key extraction.
115
- *
116
- * This function:
117
- * 1. Reads the source file
118
- * 2. Runs plugin onLoad hooks for code transformation
119
- * 3. Parses the code into an Abstract Syntax Tree (AST) using SWC
120
- * 4. Extracts keys from comments using regex patterns
121
- * 5. Traverses the AST using visitors to find translation calls
122
- * 6. Runs plugin onVisitNode hooks for custom extraction logic
123
- *
124
- * @param file - Path to the source file to process
125
- * @param config - The i18next toolkit configuration object
126
- * @param logger - Logger instance for output
127
- * @param allKeys - Map to accumulate found translation keys
128
- *
129
- * @throws {ExtractorError} When file processing fails
130
- *
131
- * @internal
132
- */
133
- export async function processFile (
134
- file: string,
135
- plugins: Plugin[],
136
- astVisitors: ASTVisitors,
137
- pluginContext: PluginContext,
138
- config: Omit<I18nextToolkitConfig, 'plugins'>,
139
- logger: Logger = new ConsoleLogger()
140
- ): Promise<void> {
141
- try {
142
- let code = await readFile(file, 'utf-8')
143
-
144
- // Run onLoad hooks from plugins with error handling
145
- for (const plugin of plugins) {
146
- try {
147
- const result = await plugin.onLoad?.(code, file)
148
- if (result !== undefined) {
149
- code = result
150
- }
151
- } catch (err) {
152
- logger.warn(`Plugin ${plugin.name} onLoad failed:`, err)
153
- // Continue with the original code if the plugin fails
154
- }
155
- }
156
-
157
- // Determine parser options from file extension so .ts is not parsed as TSX
158
- const fileExt = extname(file).toLowerCase()
159
- const isTypeScriptFile = fileExt === '.ts' || fileExt === '.tsx' || fileExt === '.mts' || fileExt === '.cts'
160
- const isTSX = fileExt === '.tsx'
161
- const isJSX = fileExt === '.jsx'
162
-
163
- let ast: any
164
- try {
165
- ast = await parse(code, {
166
- syntax: isTypeScriptFile ? 'typescript' : 'ecmascript',
167
- tsx: isTSX,
168
- jsx: isJSX,
169
- decorators: true,
170
- dynamicImport: true,
171
- comments: true,
172
- })
173
- } catch (err) {
174
- // Some projects embed JSX/TSX inside .ts files. Try a one-time fallback parse
175
- // enabling TSX when the file extension is `.ts`. If fallback fails, surface
176
- // the original error as an ExtractorError.
177
- if (fileExt === '.ts' && !isTSX) {
178
- try {
179
- ast = await parse(code, {
180
- syntax: 'typescript',
181
- tsx: true,
182
- decorators: true,
183
- dynamicImport: true,
184
- comments: true,
185
- })
186
- logger.info?.(`Parsed ${file} using TSX fallback`)
187
- } catch (err2) {
188
- throw new ExtractorError('Failed to process file', file, err2 as Error)
189
- }
190
- } else {
191
- throw new ExtractorError('Failed to process file', file, err as Error)
192
- }
193
- }
194
-
195
- // "Wire up" the visitor's scope method to the context.
196
- // This avoids a circular dependency while giving plugins access to the scope.
197
- pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors)
198
-
199
- // Pass BOTH file and code
200
- astVisitors.setCurrentFile(file, code)
201
-
202
- // 3. FIRST: Visit the AST to build scope information
203
- astVisitors.visit(ast)
204
-
205
- // 4. THEN: Extract keys from comments with scope resolution (now scope info is available)
206
- extractKeysFromComments(code, pluginContext, config, astVisitors.getVarFromScope.bind(astVisitors))
207
- } catch (error) {
208
- throw new ExtractorError('Failed to process file', file, error as Error)
209
- }
210
- }
211
-
212
- /**
213
- * Simplified extraction function that returns translation results without file writing.
214
- * Used primarily for testing and programmatic access.
215
- *
216
- * @param config - The i18next toolkit configuration object
217
- * @returns Promise resolving to array of translation results
218
- *
219
- * @example
220
- * ```typescript
221
- * const results = await extract(config)
222
- * for (const result of results) {
223
- * console.log(`${result.path}: ${result.updated ? 'Updated' : 'No changes'}`)
224
- * }
225
- * ```
226
- */
227
- export async function extract (config: I18nextToolkitConfig, { syncPrimaryWithDefaults = false }: { syncPrimaryWithDefaults?: boolean } = {}) {
228
- config.extract.primaryLanguage ||= config.locales[0] || 'en'
229
- config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
230
- config.extract.functions ||= ['t', '*.t']
231
- config.extract.transComponents ||= ['Trans']
232
- const { allKeys, objectKeys } = await findKeys(config)
233
- return getTranslations(allKeys, objectKeys, config, { syncPrimaryWithDefaults })
234
- }
235
-
236
- /**
237
- * Prints a promotional message for the locize saveMissing workflow.
238
- * This message is shown after a successful extraction that resulted in changes.
239
- */
240
- async function printLocizeFunnel () {
241
- if (!(await shouldShowFunnel('extract'))) return
242
-
243
- console.log(chalk.yellow.bold('\n💡 Tip: Tired of running the extractor manually?'))
244
- console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,')
245
- console.log(' where keys are created and translated automatically as you code.')
246
- console.log(` Learn more: ${chalk.cyan('https://www.locize.com/blog/i18next-savemissing-ai-automation')}`)
247
- console.log(` Watch the video: ${chalk.cyan('https://youtu.be/joPsZghT3wM')}`)
248
-
249
- return recordFunnelShown('extract')
250
- }
@@ -1,142 +0,0 @@
1
- import { glob } from 'glob'
2
- import type { Expression } from '@swc/core'
3
- import type { ExtractedKey, Logger, I18nextToolkitConfig, ASTVisitorHooks } from '../../types'
4
- import { processFile } from './extractor'
5
- import { ConsoleLogger } from '../../utils/logger'
6
- import { initializePlugins, createPluginContext } from '../plugin-manager'
7
- import { ASTVisitors } from './ast-visitors'
8
- import { ExpressionResolver } from '../parsers/expression-resolver'
9
-
10
- /**
11
- * Main function for finding translation keys across all source files in a project.
12
- *
13
- * This function orchestrates the key extraction process:
14
- * 1. Processes source files based on input patterns
15
- * 2. Initializes and manages plugins
16
- * 3. Processes each file through AST parsing and key extraction
17
- * 4. Runs plugin lifecycle hooks
18
- * 5. Returns a deduplicated map of all found keys
19
- *
20
- * @param config - The i18next toolkit configuration object
21
- * @param logger - Logger instance for output (defaults to ConsoleLogger)
22
- * @returns Promise resolving to a Map of unique translation keys with metadata
23
- *
24
- * @example
25
- * ```typescript
26
- * const config = {
27
- * extract: {
28
- * input: ['src/**\/*.{ts,tsx}'],
29
- * functions: ['t', '*.t'],
30
- * transComponents: ['Trans']
31
- * }
32
- * }
33
- *
34
- * const keys = await findKeys(config)
35
- * console.log(`Found ${keys.size} unique translation keys`)
36
- * ```
37
- */
38
- export async function findKeys (
39
- config: I18nextToolkitConfig,
40
- logger: Logger = new ConsoleLogger()
41
- ): Promise<{ allKeys: Map<string, ExtractedKey>, objectKeys: Set<string> }> {
42
- const { plugins: pluginsOrUndefined, ...otherConfig } = config
43
- const plugins = pluginsOrUndefined || []
44
-
45
- const sourceFiles = await processSourceFiles(config)
46
- const allKeys = new Map<string, ExtractedKey>()
47
-
48
- // 1. Create the base context with config and logger.
49
- const pluginContext = createPluginContext(allKeys, plugins, otherConfig, logger)
50
-
51
- // 2. Create hooks for plugins to hook into AST
52
- const hooks = {
53
- onBeforeVisitNode: (node) => {
54
- for (const plugin of plugins) {
55
- try {
56
- plugin.onVisitNode?.(node, pluginContext)
57
- } catch (err) {
58
- logger.warn(`Plugin ${plugin.name} onVisitNode failed:`, err)
59
- }
60
- }
61
- },
62
- resolvePossibleKeyStringValues: (expression: Expression) => {
63
- return plugins.flatMap(plugin => {
64
- try {
65
- return plugin.extractKeysFromExpression?.(expression, config, logger) ?? []
66
- } catch (err) {
67
- logger.warn(`Plugin ${plugin.name} extractKeysFromExpression failed:`, err)
68
- return []
69
- }
70
- })
71
- },
72
- resolvePossibleContextStringValues: (expression: Expression) => {
73
- return plugins.flatMap(plugin => {
74
- try {
75
- return plugin.extractContextFromExpression?.(expression, config, logger) ?? []
76
- } catch (err) {
77
- logger.warn(`Plugin ${plugin.name} extractContextFromExpression failed:`, err)
78
- return []
79
- }
80
- })
81
- },
82
- } satisfies ASTVisitorHooks
83
-
84
- // 3. Create the visitor instance, passing it the context.
85
- // Use a shared ExpressionResolver so captured enums/objects in one file are available when resolving MemberExpressions
86
- const sharedExpressionResolver = new ExpressionResolver(hooks)
87
- const astVisitors = new ASTVisitors(otherConfig, pluginContext, logger, hooks, sharedExpressionResolver)
88
-
89
- // 4. "Wire up" the visitor's scope method to the context.
90
- // This avoids a circular dependency while giving plugins access to the scope.
91
- pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors)
92
-
93
- // 5. Initialize plugins
94
- await initializePlugins(plugins)
95
-
96
- // 6. Process each file
97
- for (const file of sourceFiles) {
98
- await processFile(file, plugins, astVisitors, pluginContext, otherConfig, logger)
99
- }
100
-
101
- // 7. Run onEnd hooks
102
- for (const plugin of plugins) {
103
- await plugin.onEnd?.(allKeys)
104
- }
105
-
106
- // Normalization: mark already-expanded plural keys
107
- const pluralSeparator = otherConfig.extract?.pluralSeparator ?? '_'
108
- const categories = ['zero', 'one', 'two', 'few', 'many', 'other']
109
- for (const ek of allKeys.values()) {
110
- const parts = String(ek.key).split(pluralSeparator)
111
- const last = parts[parts.length - 1]
112
- const isOrdinal = parts.length >= 3 && parts[parts.length - 2] === 'ordinal'
113
- if (categories.includes(last) || (isOrdinal && categories.includes(last))) {
114
- ;(ek as any).isExpandedPlural = true
115
- }
116
- }
117
- return { allKeys, objectKeys: astVisitors.objectKeys }
118
- }
119
-
120
- /**
121
- * Processes source files using glob patterns from configuration.
122
- * Excludes node_modules by default and resolves relative to current working directory.
123
- *
124
- * @param config - The i18next toolkit configuration object
125
- * @returns Promise resolving to array of file paths to process
126
- *
127
- * @internal
128
- */
129
- async function processSourceFiles (config: I18nextToolkitConfig): Promise<string[]> {
130
- const defaultIgnore = ['node_modules/**']
131
-
132
- // Normalize the user's ignore option into an array
133
- const userIgnore = Array.isArray(config.extract.ignore)
134
- ? config.extract.ignore
135
- : config.extract.ignore ? [config.extract.ignore] : []
136
-
137
- return await glob(config.extract.input, {
138
- // Combine default ignore patterns with user-configured ones
139
- ignore: [...defaultIgnore, ...userIgnore],
140
- cwd: process.cwd(),
141
- })
142
- }