i18next-cli 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +489 -0
- package/dist/cjs/cli.js +2 -0
- package/dist/cjs/config.js +1 -0
- package/dist/cjs/extractor/core/extractor.js +1 -0
- package/dist/cjs/extractor/core/key-finder.js +1 -0
- package/dist/cjs/extractor/core/translation-manager.js +1 -0
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
- package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
- package/dist/cjs/extractor/plugin-manager.js +1 -0
- package/dist/cjs/heuristic-config.js +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/init.js +1 -0
- package/dist/cjs/linter.js +1 -0
- package/dist/cjs/locize.js +1 -0
- package/dist/cjs/migrator.js +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/status.js +1 -0
- package/dist/cjs/syncer.js +1 -0
- package/dist/cjs/types-generator.js +1 -0
- package/dist/cjs/utils/file-utils.js +1 -0
- package/dist/cjs/utils/logger.js +1 -0
- package/dist/cjs/utils/nested-object.js +1 -0
- package/dist/cjs/utils/validation.js +1 -0
- package/dist/esm/cli.js +2 -0
- package/dist/esm/config.js +1 -0
- package/dist/esm/extractor/core/extractor.js +1 -0
- package/dist/esm/extractor/core/key-finder.js +1 -0
- package/dist/esm/extractor/core/translation-manager.js +1 -0
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
- package/dist/esm/extractor/parsers/comment-parser.js +1 -0
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
- package/dist/esm/extractor/plugin-manager.js +1 -0
- package/dist/esm/heuristic-config.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/init.js +1 -0
- package/dist/esm/linter.js +1 -0
- package/dist/esm/locize.js +1 -0
- package/dist/esm/migrator.js +1 -0
- package/dist/esm/status.js +1 -0
- package/dist/esm/syncer.js +1 -0
- package/dist/esm/types-generator.js +1 -0
- package/dist/esm/utils/file-utils.js +1 -0
- package/dist/esm/utils/logger.js +1 -0
- package/dist/esm/utils/nested-object.js +1 -0
- package/dist/esm/utils/validation.js +1 -0
- package/package.json +81 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +158 -0
- package/src/extractor/core/extractor.ts +195 -0
- package/src/extractor/core/key-finder.ts +70 -0
- package/src/extractor/core/translation-manager.ts +115 -0
- package/src/extractor/index.ts +7 -0
- package/src/extractor/parsers/ast-visitors.ts +637 -0
- package/src/extractor/parsers/comment-parser.ts +125 -0
- package/src/extractor/parsers/jsx-parser.ts +166 -0
- package/src/extractor/plugin-manager.ts +54 -0
- package/src/extractor.ts +15 -0
- package/src/heuristic-config.ts +64 -0
- package/src/index.ts +12 -0
- package/src/init.ts +156 -0
- package/src/linter.ts +191 -0
- package/src/locize.ts +251 -0
- package/src/migrator.ts +139 -0
- package/src/status.ts +192 -0
- package/src/syncer.ts +114 -0
- package/src/types-generator.ts +116 -0
- package/src/types.ts +312 -0
- package/src/utils/file-utils.ts +81 -0
- package/src/utils/logger.ts +36 -0
- package/src/utils/nested-object.ts +113 -0
- package/src/utils/validation.ts +69 -0
- package/tryme.js +8 -0
- package/tsconfig.json +71 -0
- package/types/cli.d.ts +3 -0
- package/types/cli.d.ts.map +1 -0
- package/types/config.d.ts +50 -0
- package/types/config.d.ts.map +1 -0
- package/types/extractor/core/extractor.d.ts +66 -0
- package/types/extractor/core/extractor.d.ts.map +1 -0
- package/types/extractor/core/key-finder.d.ts +31 -0
- package/types/extractor/core/key-finder.d.ts.map +1 -0
- package/types/extractor/core/translation-manager.d.ts +31 -0
- package/types/extractor/core/translation-manager.d.ts.map +1 -0
- package/types/extractor/index.d.ts +8 -0
- package/types/extractor/index.d.ts.map +1 -0
- package/types/extractor/parsers/ast-visitors.d.ts +235 -0
- package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
- package/types/extractor/parsers/comment-parser.d.ts +24 -0
- package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
- package/types/extractor/parsers/jsx-parser.d.ts +35 -0
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
- package/types/extractor/plugin-manager.d.ts +37 -0
- package/types/extractor/plugin-manager.d.ts.map +1 -0
- package/types/extractor.d.ts +7 -0
- package/types/extractor.d.ts.map +1 -0
- package/types/heuristic-config.d.ts +10 -0
- package/types/heuristic-config.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/init.d.ts +29 -0
- package/types/init.d.ts.map +1 -0
- package/types/linter.d.ts +33 -0
- package/types/linter.d.ts.map +1 -0
- package/types/locize.d.ts +5 -0
- package/types/locize.d.ts.map +1 -0
- package/types/migrator.d.ts +37 -0
- package/types/migrator.d.ts.map +1 -0
- package/types/status.d.ts +20 -0
- package/types/status.d.ts.map +1 -0
- package/types/syncer.d.ts +33 -0
- package/types/syncer.d.ts.map +1 -0
- package/types/types-generator.d.ts +29 -0
- package/types/types-generator.d.ts.map +1 -0
- package/types/types.d.ts +268 -0
- package/types/types.d.ts.map +1 -0
- package/types/utils/file-utils.d.ts +61 -0
- package/types/utils/file-utils.d.ts.map +1 -0
- package/types/utils/logger.d.ts +34 -0
- package/types/utils/logger.d.ts.map +1 -0
- package/types/utils/nested-object.d.ts +71 -0
- package/types/utils/nested-object.d.ts.map +1 -0
- package/types/utils/validation.d.ts +47 -0
- package/types/utils/validation.d.ts.map +1 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,195 @@
|
|
|
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 } from 'node:path'
|
|
6
|
+
import type { Logger, ExtractedKey, PluginContext, I18nextToolkitConfig } from '../../types'
|
|
7
|
+
import { findKeys } from './key-finder'
|
|
8
|
+
import { getTranslations } from './translation-manager'
|
|
9
|
+
import { validateExtractorConfig, ExtractorError } from '../../utils/validation'
|
|
10
|
+
import { createPluginContext } from '../plugin-manager'
|
|
11
|
+
import { extractKeysFromComments } from '../parsers/comment-parser'
|
|
12
|
+
import { ASTVisitors } from '../parsers/ast-visitors'
|
|
13
|
+
import { ConsoleLogger } from '../../utils/logger'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Main extractor function that runs the complete key extraction and file generation process.
|
|
17
|
+
*
|
|
18
|
+
* This is the primary entry point that:
|
|
19
|
+
* 1. Validates configuration
|
|
20
|
+
* 2. Sets up default sync options
|
|
21
|
+
* 3. Finds all translation keys across source files
|
|
22
|
+
* 4. Generates/updates translation files for all locales
|
|
23
|
+
* 5. Provides progress feedback via spinner
|
|
24
|
+
* 6. Returns whether any files were updated
|
|
25
|
+
*
|
|
26
|
+
* @param config - The i18next toolkit configuration object
|
|
27
|
+
* @param logger - Logger instance for output (defaults to ConsoleLogger)
|
|
28
|
+
* @returns Promise resolving to boolean indicating if any files were updated
|
|
29
|
+
*
|
|
30
|
+
* @throws {ExtractorError} When configuration validation fails or extraction process encounters errors
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const config = await loadConfig()
|
|
35
|
+
* const updated = await runExtractor(config)
|
|
36
|
+
* if (updated) {
|
|
37
|
+
* console.log('Translation files were updated')
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export async function runExtractor (
|
|
42
|
+
config: I18nextToolkitConfig,
|
|
43
|
+
logger: Logger = new ConsoleLogger()
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0] || 'en'
|
|
46
|
+
if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
47
|
+
|
|
48
|
+
validateExtractorConfig(config)
|
|
49
|
+
|
|
50
|
+
const spinner = ora('Running i18next key extractor...\n').start()
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const allKeys = await findKeys(config, logger)
|
|
54
|
+
spinner.text = `Found ${allKeys.size} unique keys. Updating translation files...`
|
|
55
|
+
|
|
56
|
+
const results = await getTranslations(allKeys, config)
|
|
57
|
+
|
|
58
|
+
let anyFileUpdated = false
|
|
59
|
+
for (const result of results) {
|
|
60
|
+
if (result.updated) {
|
|
61
|
+
anyFileUpdated = true
|
|
62
|
+
await mkdir(dirname(result.path), { recursive: true })
|
|
63
|
+
await writeFile(result.path, JSON.stringify(result.newTranslations, null, 2))
|
|
64
|
+
logger.info(chalk.green(`Updated: ${result.path}`))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
spinner.succeed(chalk.bold('Extraction complete!'))
|
|
69
|
+
return anyFileUpdated
|
|
70
|
+
} catch (error) {
|
|
71
|
+
spinner.fail(chalk.red('Extraction failed.'))
|
|
72
|
+
// Re-throw or handle error
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Processes an individual source file for translation key extraction.
|
|
79
|
+
*
|
|
80
|
+
* This function:
|
|
81
|
+
* 1. Reads the source file
|
|
82
|
+
* 2. Runs plugin onLoad hooks for code transformation
|
|
83
|
+
* 3. Parses the code into an Abstract Syntax Tree (AST) using SWC
|
|
84
|
+
* 4. Extracts keys from comments using regex patterns
|
|
85
|
+
* 5. Traverses the AST using visitors to find translation calls
|
|
86
|
+
* 6. Runs plugin onVisitNode hooks for custom extraction logic
|
|
87
|
+
*
|
|
88
|
+
* @param file - Path to the source file to process
|
|
89
|
+
* @param config - The i18next toolkit configuration object
|
|
90
|
+
* @param logger - Logger instance for output
|
|
91
|
+
* @param allKeys - Map to accumulate found translation keys
|
|
92
|
+
*
|
|
93
|
+
* @throws {ExtractorError} When file processing fails
|
|
94
|
+
*
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export async function processFile (
|
|
98
|
+
file: string,
|
|
99
|
+
config: I18nextToolkitConfig,
|
|
100
|
+
logger: Logger,
|
|
101
|
+
allKeys: Map<string, ExtractedKey>
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
let code = await readFile(file, 'utf-8')
|
|
105
|
+
|
|
106
|
+
// Run onLoad hooks from plugins
|
|
107
|
+
for (const plugin of (config.plugins || [])) {
|
|
108
|
+
code = (await plugin.onLoad?.(code, file)) ?? code
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ast = await parse(code, {
|
|
112
|
+
syntax: 'typescript',
|
|
113
|
+
tsx: true,
|
|
114
|
+
comments: true
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const pluginContext = createPluginContext(allKeys)
|
|
118
|
+
|
|
119
|
+
// Extract keys from comments
|
|
120
|
+
extractKeysFromComments(code, config.extract.functions || ['t'], pluginContext, config)
|
|
121
|
+
|
|
122
|
+
// Extract keys from AST using visitors
|
|
123
|
+
const astVisitors = new ASTVisitors(
|
|
124
|
+
config,
|
|
125
|
+
pluginContext,
|
|
126
|
+
logger
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
astVisitors.visit(ast)
|
|
130
|
+
|
|
131
|
+
// Run plugin visitors
|
|
132
|
+
if ((config.plugins || []).length > 0) {
|
|
133
|
+
traverseEveryNode(ast, (config.plugins || []), pluginContext)
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new ExtractorError('Failed to process file', file, error as Error)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Recursively traverses AST nodes and calls plugin onVisitNode hooks.
|
|
142
|
+
*
|
|
143
|
+
* @param node - The AST node to traverse
|
|
144
|
+
* @param plugins - Array of plugins to run hooks for
|
|
145
|
+
* @param pluginContext - Context object with helper methods for plugins
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
function traverseEveryNode (node: any, plugins: any[], pluginContext: PluginContext): void {
|
|
150
|
+
if (!node || typeof node !== 'object') return
|
|
151
|
+
|
|
152
|
+
// Call plugins for this node
|
|
153
|
+
for (const plugin of plugins) {
|
|
154
|
+
try {
|
|
155
|
+
plugin.onVisitNode?.(node, pluginContext)
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.warn(`Plugin ${plugin.name} onVisitNode failed:`, err)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const key of Object.keys(node)) {
|
|
162
|
+
const child = node[key]
|
|
163
|
+
if (Array.isArray(child)) {
|
|
164
|
+
for (const c of child) {
|
|
165
|
+
if (c && typeof c === 'object') traverseEveryNode(c, plugins, pluginContext)
|
|
166
|
+
}
|
|
167
|
+
} else if (child && typeof child === 'object') {
|
|
168
|
+
traverseEveryNode(child, plugins, pluginContext)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Simplified extraction function that returns translation results without file writing.
|
|
175
|
+
* Used primarily for testing and programmatic access.
|
|
176
|
+
*
|
|
177
|
+
* @param config - The i18next toolkit configuration object
|
|
178
|
+
* @returns Promise resolving to array of translation results
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const results = await extract(config)
|
|
183
|
+
* for (const result of results) {
|
|
184
|
+
* console.log(`${result.path}: ${result.updated ? 'Updated' : 'No changes'}`)
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export async function extract (config: I18nextToolkitConfig) {
|
|
189
|
+
if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0]
|
|
190
|
+
if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
191
|
+
if (!config.extract.functions) config.extract.functions = ['t']
|
|
192
|
+
if (!config.extract.transComponents) config.extract.transComponents = ['Trans']
|
|
193
|
+
const allKeys = await findKeys(config)
|
|
194
|
+
return getTranslations(allKeys, config)
|
|
195
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { glob } from 'glob'
|
|
2
|
+
import type { ExtractedKey, Logger, I18nextToolkitConfig } from '../../types'
|
|
3
|
+
import { processFile } from './extractor'
|
|
4
|
+
import { ConsoleLogger } from '../../utils/logger'
|
|
5
|
+
import { initializePlugins } from '../plugin-manager'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Main function for finding translation keys across all source files in a project.
|
|
9
|
+
*
|
|
10
|
+
* This function orchestrates the key extraction process:
|
|
11
|
+
* 1. Processes source files based on input patterns
|
|
12
|
+
* 2. Initializes and manages plugins
|
|
13
|
+
* 3. Processes each file through AST parsing and key extraction
|
|
14
|
+
* 4. Runs plugin lifecycle hooks
|
|
15
|
+
* 5. Returns a deduplicated map of all found keys
|
|
16
|
+
*
|
|
17
|
+
* @param config - The i18next toolkit configuration object
|
|
18
|
+
* @param logger - Logger instance for output (defaults to ConsoleLogger)
|
|
19
|
+
* @returns Promise resolving to a Map of unique translation keys with metadata
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const config = {
|
|
24
|
+
* extract: {
|
|
25
|
+
* input: ['src/**\/*.{ts,tsx}'],
|
|
26
|
+
* functions: ['t'],
|
|
27
|
+
* transComponents: ['Trans']
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* const keys = await findKeys(config)
|
|
32
|
+
* console.log(`Found ${keys.size} unique translation keys`)
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export async function findKeys (
|
|
36
|
+
config: I18nextToolkitConfig,
|
|
37
|
+
logger: Logger = new ConsoleLogger()
|
|
38
|
+
): Promise<Map<string, ExtractedKey>> {
|
|
39
|
+
const sourceFiles = await processSourceFiles(config)
|
|
40
|
+
const allKeys = new Map<string, ExtractedKey>()
|
|
41
|
+
|
|
42
|
+
await initializePlugins(config.plugins || [])
|
|
43
|
+
|
|
44
|
+
for (const file of sourceFiles) {
|
|
45
|
+
await processFile(file, config, logger, allKeys)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Run onEnd hooks
|
|
49
|
+
for (const plugin of (config.plugins || [])) {
|
|
50
|
+
await plugin.onEnd?.(allKeys)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return allKeys
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Processes source files using glob patterns from configuration.
|
|
58
|
+
* Excludes node_modules by default and resolves relative to current working directory.
|
|
59
|
+
*
|
|
60
|
+
* @param config - The i18next toolkit configuration object
|
|
61
|
+
* @returns Promise resolving to array of file paths to process
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
async function processSourceFiles (config: I18nextToolkitConfig): Promise<string[]> {
|
|
66
|
+
return await glob(config.extract.input, {
|
|
67
|
+
ignore: 'node_modules/**',
|
|
68
|
+
cwd: process.cwd(),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { TranslationResult, ExtractedKey, I18nextToolkitConfig } from '../../types'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { getNestedValue, setNestedValue, getNestedKeys } from '../../utils/nested-object'
|
|
5
|
+
import { getOutputPath } from '../../utils/file-utils'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts a glob pattern to a regular expression for matching keys
|
|
9
|
+
* @param glob - The glob pattern to convert
|
|
10
|
+
* @returns A RegExp object that matches the glob pattern
|
|
11
|
+
*/
|
|
12
|
+
function globToRegex (glob: string): RegExp {
|
|
13
|
+
const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
14
|
+
const regexString = `^${escaped.replace(/\*/g, '.*')}$`
|
|
15
|
+
return new RegExp(regexString)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Processes extracted translation keys and generates translation files for all configured locales.
|
|
20
|
+
*
|
|
21
|
+
* This function:
|
|
22
|
+
* 1. Groups keys by namespace
|
|
23
|
+
* 2. For each locale and namespace combination:
|
|
24
|
+
* - Reads existing translation files
|
|
25
|
+
* - Preserves keys matching `preservePatterns`
|
|
26
|
+
* - Merges in newly extracted keys
|
|
27
|
+
* - Uses primary language defaults or empty strings for secondary languages
|
|
28
|
+
* - Maintains key sorting based on configuration
|
|
29
|
+
* 3. Determines if files need updating by comparing content
|
|
30
|
+
*
|
|
31
|
+
* @param keys - Map of extracted translation keys with metadata
|
|
32
|
+
* @param config - The i18next toolkit configuration object
|
|
33
|
+
* @returns Promise resolving to array of translation results with update status
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const keys = new Map([
|
|
38
|
+
* ['translation:welcome', { key: 'welcome', defaultValue: 'Welcome!', ns: 'translation' }],
|
|
39
|
+
* ['common:button.save', { key: 'button.save', defaultValue: 'Save', ns: 'common' }]
|
|
40
|
+
* ])
|
|
41
|
+
*
|
|
42
|
+
* const results = await getTranslations(keys, config)
|
|
43
|
+
* // Results contain update status and new/existing translations for each locale
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export async function getTranslations (
|
|
47
|
+
keys: Map<string, ExtractedKey>,
|
|
48
|
+
config: I18nextToolkitConfig
|
|
49
|
+
): Promise<TranslationResult[]> {
|
|
50
|
+
const defaultNS = config.extract.defaultNS ?? 'translation'
|
|
51
|
+
const keySeparator = config.extract.keySeparator ?? '.'
|
|
52
|
+
const preservePatterns = (config.extract.preservePatterns ?? []).map(globToRegex)
|
|
53
|
+
if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0] || 'en'
|
|
54
|
+
if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config.extract.primaryLanguage)
|
|
55
|
+
|
|
56
|
+
// Group keys by namespace
|
|
57
|
+
const keysByNS = new Map<string, ExtractedKey[]>()
|
|
58
|
+
for (const key of keys.values()) {
|
|
59
|
+
const ns = key.ns || defaultNS
|
|
60
|
+
if (!keysByNS.has(ns)) {
|
|
61
|
+
keysByNS.set(ns, [])
|
|
62
|
+
}
|
|
63
|
+
keysByNS.get(ns)!.push(key)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const results: TranslationResult[] = []
|
|
67
|
+
|
|
68
|
+
for (const locale of config.locales) {
|
|
69
|
+
for (const [ns, nsKeys] of keysByNS.entries()) {
|
|
70
|
+
const outputPath = getOutputPath(config.extract.output, locale, ns)
|
|
71
|
+
|
|
72
|
+
const fullPath = resolve(process.cwd(), outputPath)
|
|
73
|
+
|
|
74
|
+
let oldContent = ''
|
|
75
|
+
let existingTranslations: Record<string, any> = {}
|
|
76
|
+
try {
|
|
77
|
+
oldContent = await readFile(fullPath, 'utf-8')
|
|
78
|
+
existingTranslations = JSON.parse(oldContent)
|
|
79
|
+
} catch (e) { /* File doesn't exist, which is fine */ }
|
|
80
|
+
|
|
81
|
+
const newTranslations: Record<string, any> = {}
|
|
82
|
+
|
|
83
|
+
// 1. Preserve keys from existing translations that match patterns
|
|
84
|
+
const existingKeys = getNestedKeys(existingTranslations, keySeparator)
|
|
85
|
+
for (const existingKey of existingKeys) {
|
|
86
|
+
if (preservePatterns.some(re => re.test(existingKey))) {
|
|
87
|
+
const value = getNestedValue(existingTranslations, existingKey, keySeparator)
|
|
88
|
+
setNestedValue(newTranslations, existingKey, value, keySeparator)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Merge in newly found keys for this namespace
|
|
93
|
+
const sortedKeys = (config.extract.sort === false)
|
|
94
|
+
? nsKeys
|
|
95
|
+
: nsKeys.sort((a, b) => a.key.localeCompare(b.key))
|
|
96
|
+
for (const { key, defaultValue } of sortedKeys) {
|
|
97
|
+
const existingValue = getNestedValue(existingTranslations, key, keySeparator)
|
|
98
|
+
const valueToSet = existingValue ?? (locale === config.extract?.primaryLanguage ? defaultValue : '')
|
|
99
|
+
setNestedValue(newTranslations, key, valueToSet, keySeparator)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const indentation = config.extract.indentation ?? 2
|
|
103
|
+
const newContent = JSON.stringify(newTranslations, null, indentation)
|
|
104
|
+
|
|
105
|
+
results.push({
|
|
106
|
+
path: fullPath,
|
|
107
|
+
updated: newContent !== oldContent,
|
|
108
|
+
newTranslations,
|
|
109
|
+
existingTranslations,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return results
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './core/extractor'
|
|
2
|
+
export * from './core/key-finder'
|
|
3
|
+
export * from './core/translation-manager'
|
|
4
|
+
export * from './parsers/ast-visitors'
|
|
5
|
+
export * from './parsers/comment-parser'
|
|
6
|
+
export * from './parsers/jsx-parser'
|
|
7
|
+
export * from './plugin-manager'
|