i18ntk 2.6.0 → 3.1.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 +62 -16
- package/README.md +55 -19
- package/main/i18ntk-sizing.js +471 -218
- package/main/i18ntk-translate.js +833 -0
- package/main/i18ntk-validate.js +26 -14
- package/main/manage/commands/CommandRouter.js +7 -1
- package/main/manage/commands/TranslateCommand.js +463 -0
- package/main/manage/commands/ValidateCommand.js +25 -13
- package/main/manage/index.js +11 -5
- package/package.json +14 -3
- package/settings/settings-cli.js +75 -29
- package/settings/settings-manager.js +109 -1
- package/ui-locales/de.json +5 -2
- package/ui-locales/en.json +5 -2
- package/ui-locales/es.json +5 -2
- package/ui-locales/fr.json +5 -2
- package/ui-locales/ja.json +5 -2
- package/ui-locales/ru.json +7 -4
- package/ui-locales/zh.json +5 -2
- package/utils/config-manager.js +20 -4
- package/utils/security.js +4 -3
- package/utils/translate/api.js +168 -0
- package/utils/translate/cli.js +95 -0
- package/utils/translate/placeholder.js +153 -0
- package/utils/translate/protection.js +243 -0
- package/utils/translate/report.js +117 -0
- package/utils/translate/traverse.js +148 -0
- package/utils/validation-risk.js +175 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK TRANSLATION GENERATOR
|
|
5
|
+
*
|
|
6
|
+
* Zero-dependency translation utility that converts English source JSON
|
|
7
|
+
* language files into any target language via Google's free Translate API.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* i18ntk-translate <source-file> <target-lang> [options]
|
|
11
|
+
* i18ntk-translate locales/en/common.json de
|
|
12
|
+
* i18ntk-translate locales/en/common.json fr --no-confirm --preserve-placeholders
|
|
13
|
+
* i18ntk-translate locales/en/common.json es --dry-run
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --source-dir <dir> Source directory (default: ./locales/en)
|
|
17
|
+
* --output-dir <dir> Output directory (default: ./locales/<lang>)
|
|
18
|
+
* --custom-regex <regex> Additional placeholder regex pattern
|
|
19
|
+
* --no-confirm Skip all confirmation dialogs
|
|
20
|
+
* --preserve-placeholders Translate text around placeholders and reinsert tokens
|
|
21
|
+
* --skip-placeholders Skip all strings containing placeholders
|
|
22
|
+
* --send-placeholders Translate all strings including masked placeholders
|
|
23
|
+
* --protection-file <path> JSON file with protected terms, keys, values, and patterns
|
|
24
|
+
* --create-protection-file Create the protection JSON file if it does not exist
|
|
25
|
+
* --no-protection Disable protected term/key/value handling for this run
|
|
26
|
+
* --concurrency <n> Max concurrent API requests (default: 3)
|
|
27
|
+
* --batch-size <n> Number of text segments per batch (default: 50)
|
|
28
|
+
* --dry-run Preview mode without API calls
|
|
29
|
+
* --report-file <path> Write report to file
|
|
30
|
+
* --report-stdout Print report to stdout
|
|
31
|
+
* --bom Output UTF-8 with BOM
|
|
32
|
+
* --translate-fn <module> Path to custom translation function module
|
|
33
|
+
* --retry-count <n> Max retries per request (default: 3)
|
|
34
|
+
* --retry-delay <ms> Base delay for retry backoff (default: 1000)
|
|
35
|
+
* --timeout <ms> HTTP request timeout (default: 15000)
|
|
36
|
+
* --source-lang <code> Source language code (default: en)
|
|
37
|
+
* --files <pattern> Glob pattern for multiple files (e.g. *.json)
|
|
38
|
+
* -h, --help Show help
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require('fs');
|
|
42
|
+
const os = require('os');
|
|
43
|
+
const path = require('path');
|
|
44
|
+
const packageJson = require('../package.json');
|
|
45
|
+
const ExitCodes = require('../utils/exit-codes');
|
|
46
|
+
const SecurityUtils = require('../utils/security');
|
|
47
|
+
const { isInteractive } = require('../utils/prompt-helper');
|
|
48
|
+
const {
|
|
49
|
+
detectPlaceholders,
|
|
50
|
+
maskPlaceholders,
|
|
51
|
+
splitByPlaceholders,
|
|
52
|
+
unmaskPlaceholders,
|
|
53
|
+
} = require('../utils/translate/placeholder');
|
|
54
|
+
const {
|
|
55
|
+
DEFAULT_PROTECTION_FILE,
|
|
56
|
+
createProtectionFile,
|
|
57
|
+
hasProtectionRules,
|
|
58
|
+
loadProtectionConfig,
|
|
59
|
+
protectText,
|
|
60
|
+
restoreText,
|
|
61
|
+
shouldPreserveWholeValue,
|
|
62
|
+
} = require('../utils/translate/protection');
|
|
63
|
+
const { translateBatch } = require('../utils/translate/api');
|
|
64
|
+
const { collectLeaves, setLeaf, deepClone } = require('../utils/translate/traverse');
|
|
65
|
+
const { generateReport, writeReport, formatSummaryLine } = require('../utils/translate/report');
|
|
66
|
+
const {
|
|
67
|
+
confirmGlobalChoice,
|
|
68
|
+
confirmPerKey,
|
|
69
|
+
previewSkipped,
|
|
70
|
+
} = require('../utils/translate/cli');
|
|
71
|
+
|
|
72
|
+
const BOM = '\uFEFF';
|
|
73
|
+
|
|
74
|
+
function printHelp() {
|
|
75
|
+
console.log([
|
|
76
|
+
'',
|
|
77
|
+
`I18NTK Translation Generator - v${packageJson.version}`,
|
|
78
|
+
'',
|
|
79
|
+
'Usage:',
|
|
80
|
+
' i18ntk-translate <source-file> <target-lang> [options]',
|
|
81
|
+
' i18ntk-translate <source-file> <target-lang> --source-dir <dir> [options]',
|
|
82
|
+
'',
|
|
83
|
+
'Examples:',
|
|
84
|
+
' i18ntk-translate locales/en/common.json de',
|
|
85
|
+
' i18ntk-translate locales/en/common.json fr --dry-run',
|
|
86
|
+
' i18ntk-translate locales/en/ es --files "*.json"',
|
|
87
|
+
' i18ntk-translate locales/en/common.json ja --no-confirm --preserve-placeholders',
|
|
88
|
+
' i18ntk-translate locales/en/common.json ko --report-file report.txt',
|
|
89
|
+
'',
|
|
90
|
+
'Options:',
|
|
91
|
+
' --source-dir <dir> Source directory containing locale files',
|
|
92
|
+
' --output-dir <dir> Output directory for translated files',
|
|
93
|
+
' --source-lang <code> Source language code (default: en)',
|
|
94
|
+
' --custom-regex <regex> Additional placeholder regex pattern',
|
|
95
|
+
' --no-confirm Automate: skip confirmation dialogs',
|
|
96
|
+
' --preserve-placeholders Translate text around placeholders and reinsert tokens',
|
|
97
|
+
' --skip-placeholders Skip all strings with placeholder tokens',
|
|
98
|
+
' --send-placeholders Translate all strings including masked placeholders',
|
|
99
|
+
' --protection-file <path> Protected terms/keys JSON file (default: i18ntk-auto-translate.json)',
|
|
100
|
+
' --create-protection-file Create the protection JSON file if missing',
|
|
101
|
+
' --no-protection Disable protected term/key/value handling',
|
|
102
|
+
' --concurrency <n> Max concurrent API requests (default: 3)',
|
|
103
|
+
' --batch-size <n> Number of text segments per batch (default: 50)',
|
|
104
|
+
' --progress-interval <n> Progress update interval (default: 10)',
|
|
105
|
+
' --dry-run Preview: show what would be skipped',
|
|
106
|
+
' --report-file <path> Write post-translation report to file',
|
|
107
|
+
' --report-stdout Print post-translation report to stdout',
|
|
108
|
+
' --bom Write output files with UTF-8 BOM',
|
|
109
|
+
' --translate-fn <module> Path to custom translation function module',
|
|
110
|
+
' --retry-count <n> Max retries per failed request (default: 3)',
|
|
111
|
+
' --retry-delay <ms> Base backoff delay in ms (default: 1000)',
|
|
112
|
+
' --timeout <ms> HTTP request timeout in ms (default: 15000)',
|
|
113
|
+
' -h, --help Show this help',
|
|
114
|
+
].join('\n'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseArgs(argv) {
|
|
118
|
+
const args = {
|
|
119
|
+
sourceFile: null,
|
|
120
|
+
targetLang: null,
|
|
121
|
+
sourceDir: null,
|
|
122
|
+
outputDir: null,
|
|
123
|
+
sourceLang: 'en',
|
|
124
|
+
customRegex: [],
|
|
125
|
+
noConfirm: false,
|
|
126
|
+
preservePlaceholders: false,
|
|
127
|
+
skipPlaceholders: false,
|
|
128
|
+
sendPlaceholders: false,
|
|
129
|
+
protectionFile: DEFAULT_PROTECTION_FILE,
|
|
130
|
+
protectionEnabled: true,
|
|
131
|
+
createProtectionFile: false,
|
|
132
|
+
concurrency: 3,
|
|
133
|
+
batchSize: 50,
|
|
134
|
+
progressInterval: 10,
|
|
135
|
+
dryRun: false,
|
|
136
|
+
reportFile: null,
|
|
137
|
+
reportStdout: false,
|
|
138
|
+
bom: false,
|
|
139
|
+
translateFnPath: null,
|
|
140
|
+
retryCount: 3,
|
|
141
|
+
retryDelay: 1000,
|
|
142
|
+
timeout: 15000,
|
|
143
|
+
filesPattern: null,
|
|
144
|
+
help: false,
|
|
145
|
+
unknown: [],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const positional = [];
|
|
149
|
+
for (let i = 2; i < argv.length; i++) {
|
|
150
|
+
const arg = argv[i];
|
|
151
|
+
if (arg === '-h' || arg === '--help') { args.help = true; }
|
|
152
|
+
else if (arg === '--no-confirm') { args.noConfirm = true; }
|
|
153
|
+
else if (arg === '--preserve-placeholders') { args.preservePlaceholders = true; }
|
|
154
|
+
else if (arg === '--skip-placeholders') { args.skipPlaceholders = true; }
|
|
155
|
+
else if (arg === '--send-placeholders') { args.sendPlaceholders = true; }
|
|
156
|
+
else if (arg === '--no-protection') { args.protectionEnabled = false; }
|
|
157
|
+
else if (arg === '--create-protection-file') { args.createProtectionFile = true; }
|
|
158
|
+
else if (arg === '--dry-run') { args.dryRun = true; }
|
|
159
|
+
else if (arg === '--report-stdout') { args.reportStdout = true; }
|
|
160
|
+
else if (arg === '--bom') { args.bom = true; }
|
|
161
|
+
else if (arg === '--source-dir' && i + 1 < argv.length) { args.sourceDir = argv[++i]; }
|
|
162
|
+
else if (arg === '--output-dir' && i + 1 < argv.length) { args.outputDir = argv[++i]; }
|
|
163
|
+
else if (arg === '--source-lang' && i + 1 < argv.length) { args.sourceLang = argv[++i]; }
|
|
164
|
+
else if (arg === '--custom-regex' && i + 1 < argv.length) { args.customRegex.push(argv[++i]); }
|
|
165
|
+
else if (arg === '--protection-file' && i + 1 < argv.length) { args.protectionFile = argv[++i]; }
|
|
166
|
+
else if (arg === '--concurrency' && i + 1 < argv.length) { args.concurrency = parseInt(argv[++i], 10) || 3; }
|
|
167
|
+
else if (arg === '--batch-size' && i + 1 < argv.length) { args.batchSize = parseInt(argv[++i], 10) || 50; }
|
|
168
|
+
else if (arg === '--progress-interval' && i + 1 < argv.length) { args.progressInterval = parseInt(argv[++i], 10) || 10; }
|
|
169
|
+
else if (arg === '--report-file' && i + 1 < argv.length) { args.reportFile = argv[++i]; }
|
|
170
|
+
else if (arg === '--translate-fn' && i + 1 < argv.length) { args.translateFnPath = argv[++i]; }
|
|
171
|
+
else if (arg === '--retry-count' && i + 1 < argv.length) { args.retryCount = parseInt(argv[++i], 10) || 3; }
|
|
172
|
+
else if (arg === '--retry-delay' && i + 1 < argv.length) { args.retryDelay = parseInt(argv[++i], 10) || 1000; }
|
|
173
|
+
else if (arg === '--timeout' && i + 1 < argv.length) { args.timeout = parseInt(argv[++i], 10) || 15000; }
|
|
174
|
+
else if (arg === '--files' && i + 1 < argv.length) { args.filesPattern = argv[++i]; }
|
|
175
|
+
else if (arg.startsWith('-')) { args.unknown.push(arg); }
|
|
176
|
+
else { positional.push(arg); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (positional.length >= 1) args.sourceFile = positional[0];
|
|
180
|
+
if (positional.length >= 2) args.targetLang = positional[1];
|
|
181
|
+
|
|
182
|
+
const placeholderModeCount = [
|
|
183
|
+
args.preservePlaceholders,
|
|
184
|
+
args.skipPlaceholders,
|
|
185
|
+
args.sendPlaceholders,
|
|
186
|
+
].filter(Boolean).length;
|
|
187
|
+
if (placeholderModeCount > 1) {
|
|
188
|
+
console.error('Error: --preserve-placeholders, --skip-placeholders, and --send-placeholders are mutually exclusive.');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
args.concurrency = clampInt(args.concurrency, 1, 25, 3);
|
|
193
|
+
args.batchSize = clampInt(args.batchSize, 1, 10000, 50);
|
|
194
|
+
args.progressInterval = clampInt(args.progressInterval, 1, 10000, 10);
|
|
195
|
+
|
|
196
|
+
return args;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function clampInt(value, min, max, fallback) {
|
|
200
|
+
const num = parseInt(value, 10);
|
|
201
|
+
if (!Number.isInteger(num)) return fallback;
|
|
202
|
+
return Math.min(Math.max(num, min), max);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function loadCustomTranslateFn(modulePath) {
|
|
206
|
+
if (!modulePath) return null;
|
|
207
|
+
try {
|
|
208
|
+
const resolved = path.isAbsolute(modulePath) ? modulePath : path.resolve(process.cwd(), modulePath);
|
|
209
|
+
const mod = require(resolved);
|
|
210
|
+
if (typeof mod === 'function') return mod;
|
|
211
|
+
if (mod && typeof mod.translate === 'function') return mod.translate;
|
|
212
|
+
if (mod && typeof mod.default === 'function') return mod.default;
|
|
213
|
+
console.error(`Warning: Custom translate module "${modulePath}" does not export a function.`);
|
|
214
|
+
return null;
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error(`Error: Failed to load translate function module "${modulePath}": ${e.message}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveSourceFiles(sourceFile, sourceDir, filesPattern) {
|
|
222
|
+
if (sourceDir) {
|
|
223
|
+
const resolvedDir = path.resolve(process.cwd(), sourceDir);
|
|
224
|
+
const sourceDirBase = path.dirname(resolvedDir);
|
|
225
|
+
if (!SecurityUtils.safeExistsSync(resolvedDir, sourceDirBase)) {
|
|
226
|
+
console.error(`Error: Source directory "${resolvedDir}" does not exist.`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const entries = SecurityUtils.safeReaddirSync(resolvedDir, sourceDirBase);
|
|
230
|
+
const pattern = filesPattern || '*.json';
|
|
231
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
232
|
+
const files = entries.filter((f) => regex.test(f) && f.endsWith('.json')).sort();
|
|
233
|
+
if (files.length === 0) {
|
|
234
|
+
console.error(`Error: No JSON files matching "${pattern}" found in "${resolvedDir}".`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
return files.map((f) => path.join(resolvedDir, f));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (sourceFile) {
|
|
241
|
+
const resolved = path.resolve(process.cwd(), sourceFile);
|
|
242
|
+
if (!SecurityUtils.safeExistsSync(resolved, path.dirname(resolved))) {
|
|
243
|
+
console.error(`Error: Source file "${resolved}" does not exist.`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
return [resolved];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.error('Error: No source file specified. Use --source-dir or provide a source file.');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function classifyLeaves(leaves, customRegex) {
|
|
254
|
+
const withPlaceholders = [];
|
|
255
|
+
const withoutPlaceholders = [];
|
|
256
|
+
|
|
257
|
+
for (const leaf of leaves) {
|
|
258
|
+
const placeholders = detectPlaceholders(leaf.value, customRegex);
|
|
259
|
+
if (placeholders.length > 0) {
|
|
260
|
+
withPlaceholders.push({ ...leaf, placeholders });
|
|
261
|
+
} else {
|
|
262
|
+
withoutPlaceholders.push(leaf);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { withPlaceholders, withoutPlaceholders };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function resolvePlaceholderStrategy(args) {
|
|
270
|
+
const interactive = isInteractive({ noPrompt: args.noConfirm });
|
|
271
|
+
|
|
272
|
+
if (args.preservePlaceholders) {
|
|
273
|
+
return { strategy: 'preserve', interactiveMode: false };
|
|
274
|
+
}
|
|
275
|
+
if (args.sendPlaceholders) {
|
|
276
|
+
return { strategy: 'send', interactiveMode: false };
|
|
277
|
+
}
|
|
278
|
+
if (args.skipPlaceholders) {
|
|
279
|
+
return { strategy: 'skip', interactiveMode: false };
|
|
280
|
+
}
|
|
281
|
+
if (args.noConfirm) {
|
|
282
|
+
return { strategy: 'preserve', interactiveMode: false };
|
|
283
|
+
}
|
|
284
|
+
if (!interactive) {
|
|
285
|
+
return { strategy: 'preserve', interactiveMode: false };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const choice = await confirmGlobalChoice();
|
|
289
|
+
return { strategy: choice.strategy, interactiveMode: choice.interactive };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function resolvePerKeyDecisions(withPlaceholders, interactive) {
|
|
293
|
+
const decisions = {};
|
|
294
|
+
|
|
295
|
+
if (!interactive) {
|
|
296
|
+
for (const leaf of withPlaceholders) {
|
|
297
|
+
decisions[leaf.keyPath] = 'preserve';
|
|
298
|
+
}
|
|
299
|
+
return decisions;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let bulkDecision = null;
|
|
303
|
+
for (const leaf of withPlaceholders) {
|
|
304
|
+
if (bulkDecision) {
|
|
305
|
+
decisions[leaf.keyPath] = bulkDecision;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const choice = await confirmPerKey(leaf.keyPath, leaf.value, leaf.placeholders);
|
|
309
|
+
if (choice === 'skip-all') {
|
|
310
|
+
bulkDecision = 'skip';
|
|
311
|
+
decisions[leaf.keyPath] = 'skip';
|
|
312
|
+
} else if (choice === 'preserve-all') {
|
|
313
|
+
bulkDecision = 'preserve';
|
|
314
|
+
decisions[leaf.keyPath] = 'preserve';
|
|
315
|
+
} else if (choice === 'send-all') {
|
|
316
|
+
bulkDecision = 'send';
|
|
317
|
+
decisions[leaf.keyPath] = 'send';
|
|
318
|
+
} else {
|
|
319
|
+
decisions[leaf.keyPath] = choice;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return decisions;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function buildTranslateList(withPlaceholders, withoutPlaceholders, strategy, decisions) {
|
|
327
|
+
const toTranslate = [];
|
|
328
|
+
const toSkip = [];
|
|
329
|
+
|
|
330
|
+
for (const leaf of withoutPlaceholders) {
|
|
331
|
+
toTranslate.push({ ...leaf, placeholderMode: 'none' });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (strategy === 'preserve') {
|
|
335
|
+
for (const leaf of withPlaceholders) {
|
|
336
|
+
toTranslate.push({ ...leaf, placeholderMode: 'preserve' });
|
|
337
|
+
}
|
|
338
|
+
} else if (strategy === 'send') {
|
|
339
|
+
for (const leaf of withPlaceholders) {
|
|
340
|
+
toTranslate.push({ ...leaf, placeholderMode: 'mask' });
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
for (const leaf of withPlaceholders) {
|
|
344
|
+
const decision = decisions[leaf.keyPath] || 'skip';
|
|
345
|
+
if (decision === 'preserve') {
|
|
346
|
+
toTranslate.push({ ...leaf, placeholderMode: 'preserve' });
|
|
347
|
+
} else if (decision === 'send') {
|
|
348
|
+
toTranslate.push({ ...leaf, placeholderMode: 'mask' });
|
|
349
|
+
} else {
|
|
350
|
+
toSkip.push(leaf);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { toTranslate, toSkip };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function prepareDirectBatch(toTranslate, customRegex, protection) {
|
|
359
|
+
return toTranslate.map((leaf) => {
|
|
360
|
+
const protectedText = protectText(leaf.value, protection);
|
|
361
|
+
if (leaf.placeholderMode !== 'mask') {
|
|
362
|
+
return {
|
|
363
|
+
...leaf,
|
|
364
|
+
masked: protectedText.value,
|
|
365
|
+
placeholderMap: new Map(),
|
|
366
|
+
protectionMap: protectedText.map,
|
|
367
|
+
needsUnmask: false,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const { masked, map } = maskPlaceholders(protectedText.value, customRegex);
|
|
371
|
+
return {
|
|
372
|
+
...leaf,
|
|
373
|
+
masked,
|
|
374
|
+
placeholderMap: map,
|
|
375
|
+
protectionMap: protectedText.map,
|
|
376
|
+
needsUnmask: map.size > 0,
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function runTranslation(maskedBatch, targetLang, options) {
|
|
382
|
+
const batchItems = maskedBatch.map((item) => ({ value: item.masked, keyPath: item.keyPath }));
|
|
383
|
+
const results = await translateBatchInChunks(batchItems, targetLang, options);
|
|
384
|
+
return results;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function translateBatchInChunks(batch, targetLang, options) {
|
|
388
|
+
if (batch.length === 0) return [];
|
|
389
|
+
|
|
390
|
+
const batchSize = clampInt(options.batchSize, 1, 10000, 50);
|
|
391
|
+
const results = [];
|
|
392
|
+
const onProgress = options.onProgress;
|
|
393
|
+
let completed = 0;
|
|
394
|
+
|
|
395
|
+
for (let start = 0; start < batch.length; start += batchSize) {
|
|
396
|
+
const chunk = batch.slice(start, start + batchSize);
|
|
397
|
+
const chunkResults = await translateBatch(chunk, targetLang, {
|
|
398
|
+
...options,
|
|
399
|
+
onProgress: (info) => {
|
|
400
|
+
completed++;
|
|
401
|
+
if (typeof onProgress === 'function') {
|
|
402
|
+
onProgress({
|
|
403
|
+
...info,
|
|
404
|
+
completed,
|
|
405
|
+
total: batch.length,
|
|
406
|
+
chunkCompleted: info.completed,
|
|
407
|
+
chunkTotal: info.total,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
results.push(...chunkResults);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return results;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function containsAllPlaceholders(value, placeholders) {
|
|
419
|
+
if (typeof value !== 'string') return false;
|
|
420
|
+
return (placeholders || []).every((placeholder) => value.includes(placeholder));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function createPlaceholderManifest(sourcePath, targetLang, leaves) {
|
|
424
|
+
const records = leaves
|
|
425
|
+
.filter((leaf) => Array.isArray(leaf.placeholders) && leaf.placeholders.length > 0)
|
|
426
|
+
.map((leaf) => ({
|
|
427
|
+
keyPath: leaf.keyPath,
|
|
428
|
+
placeholders: leaf.placeholders,
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
if (records.length === 0) return null;
|
|
432
|
+
|
|
433
|
+
const safeName = path.basename(sourcePath).replace(/[^a-z0-9_.-]/gi, '_');
|
|
434
|
+
const manifestPath = path.join(os.tmpdir(), `i18ntk-placeholders-${process.pid}-${Date.now()}-${targetLang}-${safeName}.json`);
|
|
435
|
+
SecurityUtils.safeWriteFileSync(manifestPath, JSON.stringify({
|
|
436
|
+
version: 1,
|
|
437
|
+
sourceFile: sourcePath,
|
|
438
|
+
targetLang,
|
|
439
|
+
createdAt: new Date().toISOString(),
|
|
440
|
+
records,
|
|
441
|
+
}, null, 2), os.tmpdir(), 'utf8');
|
|
442
|
+
return manifestPath;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function cleanupPlaceholderManifest(manifestPath) {
|
|
446
|
+
if (!manifestPath) return;
|
|
447
|
+
try {
|
|
448
|
+
fs.unlinkSync(manifestPath);
|
|
449
|
+
} catch (_) {
|
|
450
|
+
// Best-effort cleanup only.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function makeTextJob(item, segment, segmentIndex, protection) {
|
|
455
|
+
const leading = segment.value.match(/^\s*/)[0];
|
|
456
|
+
const trailing = segment.value.match(/\s*$/)[0];
|
|
457
|
+
const core = segment.value.slice(leading.length, segment.value.length - trailing.length);
|
|
458
|
+
const protectedText = protectText(core, protection);
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
value: protectedText.value,
|
|
462
|
+
leading,
|
|
463
|
+
trailing,
|
|
464
|
+
protectionMap: protectedText.map,
|
|
465
|
+
keyPath: `${item.keyPath}#segment${segmentIndex}`,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function translatePreservedItems(items, targetLang, options, customRegex, protection) {
|
|
470
|
+
const segmentJobs = [];
|
|
471
|
+
const plans = items.map((item) => {
|
|
472
|
+
const segments = splitByPlaceholders(item.value, customRegex);
|
|
473
|
+
const plan = { item, segments: [] };
|
|
474
|
+
|
|
475
|
+
segments.forEach((segment, index) => {
|
|
476
|
+
if (segment.type !== 'text' || !/[A-Za-z0-9]/.test(segment.value)) {
|
|
477
|
+
plan.segments.push({ type: segment.type, value: segment.value });
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const job = makeTextJob(item, segment, index, protection);
|
|
482
|
+
if (!job.value || !/[A-Za-z0-9]/.test(job.value)) {
|
|
483
|
+
plan.segments.push({ type: 'text', value: segment.value });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const jobIndex = segmentJobs.length;
|
|
488
|
+
segmentJobs.push(job);
|
|
489
|
+
plan.segments.push({
|
|
490
|
+
type: 'translated-text',
|
|
491
|
+
jobIndex,
|
|
492
|
+
leading: job.leading,
|
|
493
|
+
trailing: job.trailing,
|
|
494
|
+
protectionMap: job.protectionMap,
|
|
495
|
+
fallback: segment.value,
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return plan;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const translatedSegments = await translateBatchInChunks(segmentJobs, targetLang, options);
|
|
503
|
+
|
|
504
|
+
return plans.map((plan) => {
|
|
505
|
+
const value = plan.segments.map((segment) => {
|
|
506
|
+
if (segment.type === 'translated-text') {
|
|
507
|
+
const translated = translatedSegments[segment.jobIndex];
|
|
508
|
+
const restored = restoreText(translated || segment.fallback.trim(), segment.protectionMap);
|
|
509
|
+
return `${segment.leading}${restored}${segment.trailing}`;
|
|
510
|
+
}
|
|
511
|
+
return segment.value;
|
|
512
|
+
}).join('');
|
|
513
|
+
|
|
514
|
+
return containsAllPlaceholders(value, plan.item.placeholders) ? value : plan.item.value;
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function translateItems(toTranslate, targetLang, options, customRegex, protection) {
|
|
519
|
+
const finalResults = new Array(toTranslate.length);
|
|
520
|
+
const directItems = [];
|
|
521
|
+
const directIndexes = [];
|
|
522
|
+
const preserveItems = [];
|
|
523
|
+
const preserveIndexes = [];
|
|
524
|
+
|
|
525
|
+
toTranslate.forEach((item, index) => {
|
|
526
|
+
if (item.placeholderMode === 'preserve') {
|
|
527
|
+
preserveItems.push(item);
|
|
528
|
+
preserveIndexes.push(index);
|
|
529
|
+
} else {
|
|
530
|
+
directItems.push(item);
|
|
531
|
+
directIndexes.push(index);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const preparedDirect = prepareDirectBatch(directItems, customRegex, protection);
|
|
536
|
+
const directResults = await runTranslation(preparedDirect, targetLang, options);
|
|
537
|
+
for (let i = 0; i < preparedDirect.length; i++) {
|
|
538
|
+
const item = preparedDirect[i];
|
|
539
|
+
let finalValue = item.needsUnmask
|
|
540
|
+
? unmaskPlaceholders(directResults[i], item.placeholderMap)
|
|
541
|
+
: directResults[i];
|
|
542
|
+
finalValue = restoreText(finalValue, item.protectionMap);
|
|
543
|
+
|
|
544
|
+
if (item.placeholderMode === 'mask' && !containsAllPlaceholders(finalValue, item.placeholders)) {
|
|
545
|
+
const fallback = await translatePreservedItems([item], targetLang, options, customRegex, protection);
|
|
546
|
+
finalValue = fallback[0];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
finalResults[directIndexes[i]] = finalValue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const preservedResults = await translatePreservedItems(preserveItems, targetLang, options, customRegex, protection);
|
|
553
|
+
for (let i = 0; i < preserveItems.length; i++) {
|
|
554
|
+
finalResults[preserveIndexes[i]] = preservedResults[i];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return finalResults;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function applyResults(sourceData, translatedResults, toTranslate, toSkip) {
|
|
561
|
+
const output = deepClone(sourceData);
|
|
562
|
+
|
|
563
|
+
for (let i = 0; i < toTranslate.length; i++) {
|
|
564
|
+
setLeaf(output, toTranslate[i].keyPath, translatedResults[i]);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
for (const leaf of toSkip) {
|
|
568
|
+
setLeaf(output, leaf.keyPath, leaf.value);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return output;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function writeOutput(outputData, outputPath, bom) {
|
|
575
|
+
const resolvedOutputPath = path.resolve(process.cwd(), outputPath);
|
|
576
|
+
const dir = path.dirname(resolvedOutputPath);
|
|
577
|
+
if (!SecurityUtils.safeExistsSync(dir, path.dirname(dir))) {
|
|
578
|
+
SecurityUtils.safeMkdirSync(dir, path.dirname(dir), { recursive: true });
|
|
579
|
+
}
|
|
580
|
+
let content = JSON.stringify(outputData, null, 2) + '\n';
|
|
581
|
+
if (bom) {
|
|
582
|
+
content = BOM + content;
|
|
583
|
+
}
|
|
584
|
+
SecurityUtils.safeWriteFileSync(resolvedOutputPath, content, dir, 'utf-8');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function processFile(sourcePath, targetLang, args) {
|
|
588
|
+
const fileName = path.basename(sourcePath);
|
|
589
|
+
const targetDir = args.outputDir || path.join(path.dirname(path.dirname(sourcePath)), targetLang);
|
|
590
|
+
const targetPath = path.join(targetDir, fileName);
|
|
591
|
+
|
|
592
|
+
let sourceData;
|
|
593
|
+
try {
|
|
594
|
+
const raw = SecurityUtils.safeReadFileSync(sourcePath, path.dirname(sourcePath), 'utf-8').replace(/^\uFEFF/, '');
|
|
595
|
+
sourceData = JSON.parse(raw);
|
|
596
|
+
} catch (e) {
|
|
597
|
+
console.error(`Error reading "${sourcePath}": ${e.message}`);
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const leaves = collectLeaves(sourceData);
|
|
602
|
+
if (leaves.length === 0) {
|
|
603
|
+
console.log(`[${fileName}] No translatable strings found.`);
|
|
604
|
+
writeOutput(sourceData, targetPath, args.bom);
|
|
605
|
+
return { total: 0, translated: 0, skipped: 0, skippedKeys: [] };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const protection = args.protection || loadProtectionConfig(args.protectionFile, {
|
|
609
|
+
enabled: args.protectionEnabled,
|
|
610
|
+
create: args.createProtectionFile,
|
|
611
|
+
});
|
|
612
|
+
const protectedLeaves = leaves
|
|
613
|
+
.filter((leaf) => shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection))
|
|
614
|
+
.map((leaf) => ({ ...leaf, skipReason: 'protected' }));
|
|
615
|
+
const translatableLeaves = leaves.filter((leaf) => !shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection));
|
|
616
|
+
const { withPlaceholders, withoutPlaceholders } = classifyLeaves(translatableLeaves, args.customRegex);
|
|
617
|
+
const { strategy, interactiveMode } = await resolvePlaceholderStrategy(args);
|
|
618
|
+
|
|
619
|
+
if (args.dryRun && strategy === 'skip' && withPlaceholders.length > 0) {
|
|
620
|
+
await previewSkipped(withPlaceholders);
|
|
621
|
+
const skippedKeys = withPlaceholders.concat(protectedLeaves);
|
|
622
|
+
return {
|
|
623
|
+
total: leaves.length,
|
|
624
|
+
translated: withoutPlaceholders.length,
|
|
625
|
+
skipped: skippedKeys.length,
|
|
626
|
+
skippedKeys,
|
|
627
|
+
placeholderProtected: 0,
|
|
628
|
+
protectedSkipped: protectedLeaves.length,
|
|
629
|
+
dryRun: true,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (args.dryRun) {
|
|
634
|
+
const protectedCount = strategy === 'send' ? 0 : withPlaceholders.length;
|
|
635
|
+
console.log(`[${fileName}] Dry-run: ${leaves.length} strings would be translated.`);
|
|
636
|
+
if (protectedLeaves.length > 0) {
|
|
637
|
+
console.log(`[${fileName}] Dry-run: ${protectedLeaves.length} protected keys/values would be copied unchanged.`);
|
|
638
|
+
}
|
|
639
|
+
if (hasProtectionRules(protection)) {
|
|
640
|
+
console.log(`[${fileName}] Dry-run: protected terms would be masked from ${protection.filePath}.`);
|
|
641
|
+
}
|
|
642
|
+
if (protectedCount > 0) {
|
|
643
|
+
console.log(`[${fileName}] Dry-run: ${protectedCount} placeholder strings would use preserve mode.`);
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
total: leaves.length,
|
|
647
|
+
translated: leaves.length - protectedLeaves.length,
|
|
648
|
+
skipped: protectedLeaves.length,
|
|
649
|
+
skippedKeys: protectedLeaves,
|
|
650
|
+
placeholderProtected: protectedCount,
|
|
651
|
+
termProtected: hasProtectionRules(protection),
|
|
652
|
+
dryRun: true,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const decisions = await resolvePerKeyDecisions(withPlaceholders, interactiveMode);
|
|
657
|
+
const { toTranslate, toSkip } = buildTranslateList(withPlaceholders, withoutPlaceholders, strategy, decisions);
|
|
658
|
+
toSkip.push(...protectedLeaves);
|
|
659
|
+
const placeholderProtected = toTranslate.filter((leaf) => leaf.placeholderMode === 'preserve').length;
|
|
660
|
+
const placeholderSkipped = toSkip.filter((leaf) => leaf.skipReason !== 'protected').length;
|
|
661
|
+
|
|
662
|
+
if (placeholderSkipped > 0) {
|
|
663
|
+
console.log(`[${fileName}] Skipping ${placeholderSkipped} keys with placeholders.`);
|
|
664
|
+
}
|
|
665
|
+
if (placeholderProtected > 0) {
|
|
666
|
+
console.log(`[${fileName}] Preserving placeholders for ${placeholderProtected} keys.`);
|
|
667
|
+
}
|
|
668
|
+
if (protectedLeaves.length > 0) {
|
|
669
|
+
console.log(`[${fileName}] Copying ${protectedLeaves.length} protected keys/values unchanged.`);
|
|
670
|
+
}
|
|
671
|
+
if (hasProtectionRules(protection)) {
|
|
672
|
+
console.log(`[${fileName}] Protecting terms from: ${protection.filePath}`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const manifestPath = createPlaceholderManifest(sourcePath, targetLang, toTranslate);
|
|
676
|
+
|
|
677
|
+
const translateOptions = {
|
|
678
|
+
sourceLang: args.sourceLang,
|
|
679
|
+
concurrency: args.concurrency,
|
|
680
|
+
batchSize: args.batchSize,
|
|
681
|
+
retryCount: args.retryCount,
|
|
682
|
+
retryDelay: args.retryDelay,
|
|
683
|
+
timeout: args.timeout,
|
|
684
|
+
customFn: args.translateFn,
|
|
685
|
+
onProgress: (info) => {
|
|
686
|
+
if (info.completed % args.progressInterval === 0 || info.completed === info.total) {
|
|
687
|
+
process.stdout.write(`\r[${fileName}] Translating... ${info.completed}/${info.total}`);
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
onError: (err) => {
|
|
691
|
+
console.error(`\n[${fileName}] Warning: Failed to translate key "${err.item.keyPath}": ${err.message}`);
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
let translatedResults;
|
|
696
|
+
try {
|
|
697
|
+
if (toTranslate.length > 0) {
|
|
698
|
+
translatedResults = await translateItems(toTranslate, targetLang, translateOptions, args.customRegex, protection);
|
|
699
|
+
process.stdout.write('\n');
|
|
700
|
+
} else {
|
|
701
|
+
translatedResults = [];
|
|
702
|
+
}
|
|
703
|
+
} finally {
|
|
704
|
+
cleanupPlaceholderManifest(manifestPath);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const output = applyResults(sourceData, translatedResults, toTranslate, toSkip);
|
|
708
|
+
writeOutput(output, targetPath, args.bom);
|
|
709
|
+
|
|
710
|
+
console.log(`[${fileName}] Written: ${targetPath}`);
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
total: leaves.length,
|
|
714
|
+
translated: translatedResults.length,
|
|
715
|
+
skipped: toSkip.length,
|
|
716
|
+
skippedKeys: toSkip,
|
|
717
|
+
placeholderProtected,
|
|
718
|
+
protectedSkipped: protectedLeaves.length,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function run(args) {
|
|
723
|
+
if (args.help) {
|
|
724
|
+
printHelp();
|
|
725
|
+
return { success: true, exitCode: ExitCodes.SUCCESS };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (args.unknown.length > 0) {
|
|
729
|
+
console.error(`Unknown options: ${args.unknown.join(', ')}`);
|
|
730
|
+
console.error('Use --help for usage information.');
|
|
731
|
+
return { success: false, exitCode: 1, error: 'Unknown options' };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!args.targetLang) {
|
|
735
|
+
console.error('Error: Target language code is required.');
|
|
736
|
+
console.error('Usage: i18ntk-translate <source-file> <target-lang> [options]');
|
|
737
|
+
return { success: false, exitCode: 1, error: 'Target language code is required' };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (args.translateFnPath) {
|
|
741
|
+
args.translateFn = loadCustomTranslateFn(args.translateFnPath);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (args.protectionEnabled !== false) {
|
|
745
|
+
if (args.createProtectionFile) {
|
|
746
|
+
const protectionPath = createProtectionFile(args.protectionFile);
|
|
747
|
+
console.log(`Protection file ready: ${protectionPath}`);
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
args.protection = loadProtectionConfig(args.protectionFile, {
|
|
751
|
+
enabled: args.protectionEnabled,
|
|
752
|
+
});
|
|
753
|
+
} catch (error) {
|
|
754
|
+
return { success: false, exitCode: 1, error: error.message };
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const sourceFiles = resolveSourceFiles(args.sourceFile, args.sourceDir, args.filesPattern);
|
|
759
|
+
|
|
760
|
+
const allSkippedKeys = [];
|
|
761
|
+
let grandTotal = 0;
|
|
762
|
+
let grandTranslated = 0;
|
|
763
|
+
let grandSkipped = 0;
|
|
764
|
+
let grandPlaceholderProtected = 0;
|
|
765
|
+
let grandProtectedSkipped = 0;
|
|
766
|
+
|
|
767
|
+
for (const srcPath of sourceFiles) {
|
|
768
|
+
const result = await processFile(srcPath, args.targetLang, args);
|
|
769
|
+
if (result) {
|
|
770
|
+
grandTotal += result.total;
|
|
771
|
+
grandTranslated += result.translated;
|
|
772
|
+
grandSkipped += result.skipped;
|
|
773
|
+
grandPlaceholderProtected += result.placeholderProtected || 0;
|
|
774
|
+
grandProtectedSkipped += result.protectedSkipped || 0;
|
|
775
|
+
if (result.skippedKeys && result.skippedKeys.length > 0) {
|
|
776
|
+
allSkippedKeys.push(...result.skippedKeys);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
console.log('');
|
|
782
|
+
console.log(formatSummaryLine(grandSkipped, grandTranslated, grandTotal, grandPlaceholderProtected, grandProtectedSkipped));
|
|
783
|
+
|
|
784
|
+
if (allSkippedKeys.length > 0 || args.reportFile || args.reportStdout) {
|
|
785
|
+
const report = generateReport(allSkippedKeys, grandTranslated, grandTotal, {
|
|
786
|
+
sourceFile: sourceFiles.length === 1 ? sourceFiles[0] : `${sourceFiles.length} files`,
|
|
787
|
+
targetLang: args.targetLang,
|
|
788
|
+
dryRun: args.dryRun,
|
|
789
|
+
placeholderProtected: grandPlaceholderProtected,
|
|
790
|
+
protectedSkipped: grandProtectedSkipped,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
if (args.reportStdout || (!args.reportFile && allSkippedKeys.length > 0)) {
|
|
794
|
+
console.log('');
|
|
795
|
+
console.log(report);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (args.reportFile) {
|
|
799
|
+
const reportPath = path.resolve(process.cwd(), args.reportFile);
|
|
800
|
+
writeReport(report, reportPath);
|
|
801
|
+
console.log(`Report written: ${reportPath}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
success: true,
|
|
807
|
+
exitCode: ExitCodes.SUCCESS,
|
|
808
|
+
total: grandTotal,
|
|
809
|
+
translated: grandTranslated,
|
|
810
|
+
skipped: grandSkipped,
|
|
811
|
+
placeholderProtected: grandPlaceholderProtected,
|
|
812
|
+
protectedSkipped: grandProtectedSkipped,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function main() {
|
|
817
|
+
const result = await run(parseArgs(process.argv));
|
|
818
|
+
process.exit(result.exitCode || (result.success ? ExitCodes.SUCCESS : 1));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (require.main === module) {
|
|
822
|
+
main().catch((err) => {
|
|
823
|
+
console.error('Fatal error:', err.message);
|
|
824
|
+
process.exit(1);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
module.exports = {
|
|
829
|
+
parseArgs,
|
|
830
|
+
resolveSourceFiles,
|
|
831
|
+
processFile,
|
|
832
|
+
run,
|
|
833
|
+
};
|