i18next-cli 1.24.12 → 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/cjs/extractor/parsers/expression-resolver.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/parsers/expression-resolver.js +1 -1
- package/package.json +6 -6
- package/types/cli.d.ts +3 -1
- package/types/cli.d.ts.map +1 -1
- package/types/extractor/parsers/expression-resolver.d.ts.map +1 -1
- package/CHANGELOG.md +0 -595
- 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 -353
- 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,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
|
-
}
|