locai 2.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/README.md +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- package/package.json +80 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import { promises as fs } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import { program } from "commander";
|
|
8
|
+
import { translateFile, findLocaleFiles, validateAndFixExistingTranslations, } from "./commands/translator.js";
|
|
9
|
+
import ProviderFactory from "./core/provider-factory.js";
|
|
10
|
+
import { FileManager } from "./utils/file-manager.js";
|
|
11
|
+
import rateLimiter from "./utils/rate-limiter.js";
|
|
12
|
+
import Orchestrator from "./core/orchestrator.js";
|
|
13
|
+
import InputValidator from "./utils/input-validator.js";
|
|
14
|
+
import ErrorHelper from "./utils/error-helper.js";
|
|
15
|
+
import { getLogger } from "./utils/logger.js";
|
|
16
|
+
import { FrameworkDetector } from "./core/detection/framework-detector.js";
|
|
17
|
+
import { StructureDetector, LocaleStructure } from "./core/detection/structure-detector.js";
|
|
18
|
+
import { loadConfig } from "./config/index.js";
|
|
19
|
+
// Use createRequire to load package.json in ESM context
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { version } = require("../package.json");
|
|
22
|
+
/**
|
|
23
|
+
* Load environment variables from .env files.
|
|
24
|
+
* Prioritizes .env.local over .env.
|
|
25
|
+
*/
|
|
26
|
+
const loadEnvironmentVariables = async () => {
|
|
27
|
+
// Load environment files in priority order
|
|
28
|
+
// First load .env (base defaults), then .env.local (local overrides)
|
|
29
|
+
// This ensures .env.local values take precedence over .env values
|
|
30
|
+
const envFiles = [
|
|
31
|
+
{ file: ".env", override: false },
|
|
32
|
+
{ file: ".env.local", override: true },
|
|
33
|
+
];
|
|
34
|
+
let loadedCount = 0;
|
|
35
|
+
for (const { file: envFile, override } of envFiles) {
|
|
36
|
+
const envPath = path.resolve(process.cwd(), envFile);
|
|
37
|
+
try {
|
|
38
|
+
await fs.access(envPath);
|
|
39
|
+
const result = dotenv.config({ path: envPath, override });
|
|
40
|
+
if (!result.error) {
|
|
41
|
+
loadedCount++;
|
|
42
|
+
// Only log in verbose/debug mode to reduce noise
|
|
43
|
+
if (process.env.VERBOSE || process.env.DEBUG) {
|
|
44
|
+
console.log(`Loaded environment variables from ${envFile}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// ENOENT is expected when files don't exist - silently skip
|
|
50
|
+
if (error.code !== "ENOENT") {
|
|
51
|
+
console.warn(`Warning: Could not load ${envFile}: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Informative message only if verbose/debug and at least one file was loaded
|
|
56
|
+
if ((process.env.VERBOSE || process.env.DEBUG) && loadedCount > 0) {
|
|
57
|
+
console.log(`Environment: ${loadedCount} file(s) loaded`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Local config loader replaced by src/config/index.ts
|
|
61
|
+
/**
|
|
62
|
+
* Configure global components (logger, rate limiter, etc.).
|
|
63
|
+
* @param {any} config - Configuration object.
|
|
64
|
+
*/
|
|
65
|
+
const configureComponents = (config) => {
|
|
66
|
+
// Configure FileManager with config settings
|
|
67
|
+
if (config.fileOperations) {
|
|
68
|
+
FileManager.configure(config.fileOperations);
|
|
69
|
+
}
|
|
70
|
+
// Initialize logger with config
|
|
71
|
+
if (config.logging) {
|
|
72
|
+
const logger = getLogger(config.logging);
|
|
73
|
+
if (config.logging.verbose || config.debug) {
|
|
74
|
+
logger.info("Logger initialized", {
|
|
75
|
+
saveErrorLogs: config.logging.saveErrorLogs,
|
|
76
|
+
logDirectory: config.logging.logDirectory,
|
|
77
|
+
rotationEnabled: config.logging.logRotation?.enabled,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (config.rateLimiter) {
|
|
82
|
+
rateLimiter.updateConfig({
|
|
83
|
+
queueStrategy: config.rateLimiter.queueStrategy || "priority",
|
|
84
|
+
queueTimeout: config.rateLimiter.queueTimeout || 30000,
|
|
85
|
+
adaptiveThrottling: config.rateLimiter.adaptiveThrottling !== false,
|
|
86
|
+
providerLimits: config.rateLimiter.providerLimits,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (config.advanced?.debug) {
|
|
90
|
+
process.env.DEBUG = "true";
|
|
91
|
+
}
|
|
92
|
+
if (config.logging?.verbose) {
|
|
93
|
+
process.env.VERBOSE = "true";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Configure and run the CLI program.
|
|
98
|
+
* @param {any} defaultConfig - Default configuration.
|
|
99
|
+
* @returns {Promise<any>} - Final configuration.
|
|
100
|
+
*/
|
|
101
|
+
const configureCLI = async (defaultConfig) => {
|
|
102
|
+
// Auto-detect framework and configuration if not explicitly set
|
|
103
|
+
// This makes the tool framework-agnostic
|
|
104
|
+
const detected = await FrameworkDetector.detect(process.cwd());
|
|
105
|
+
if (detected) {
|
|
106
|
+
// Set locales directory if not configured
|
|
107
|
+
if (!defaultConfig.localesDir && detected.localesDir) {
|
|
108
|
+
defaultConfig.localesDir = detected.localesDir;
|
|
109
|
+
if (process.env.VERBOSE) {
|
|
110
|
+
console.log(`Auto-detected locales directory: ${detected.localesDir}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Set file format if not configured
|
|
114
|
+
if (!defaultConfig.fileOperations)
|
|
115
|
+
defaultConfig.fileOperations = {};
|
|
116
|
+
if ((!defaultConfig.fileOperations.format ||
|
|
117
|
+
defaultConfig.fileOperations.format === "auto") &&
|
|
118
|
+
detected.fileFormat) {
|
|
119
|
+
defaultConfig.fileOperations.format = detected.fileFormat;
|
|
120
|
+
if (process.env.VERBOSE) {
|
|
121
|
+
console.log(`Auto-detected file format: ${detected.fileFormat}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Auto-detect file structure (flat vs nested)
|
|
126
|
+
if (!defaultConfig.fileOperations)
|
|
127
|
+
defaultConfig.fileOperations = {};
|
|
128
|
+
if (!defaultConfig.fileOperations.fileStructure ||
|
|
129
|
+
defaultConfig.fileOperations.fileStructure === "auto") {
|
|
130
|
+
const targetDir = defaultConfig.localesDir || "./locales";
|
|
131
|
+
const structure = await StructureDetector.detect(targetDir);
|
|
132
|
+
if (structure !== LocaleStructure.UNKNOWN) {
|
|
133
|
+
defaultConfig.fileOperations.fileStructure = structure;
|
|
134
|
+
if (process.env.VERBOSE) {
|
|
135
|
+
console.log(`Auto-detected file structure: ${structure}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Default to flat if unknown
|
|
140
|
+
defaultConfig.fileOperations.fileStructure = LocaleStructure.FLAT;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
configureComponents(defaultConfig);
|
|
144
|
+
// Use config version if available, otherwise use package.json version
|
|
145
|
+
const toolVersion = defaultConfig.version || version;
|
|
146
|
+
program
|
|
147
|
+
.name("locai")
|
|
148
|
+
.description("AI-powered localization tool for Next.js projects")
|
|
149
|
+
.version(toolVersion);
|
|
150
|
+
program
|
|
151
|
+
.option("-s, --source <lang>", "Source language", defaultConfig.source)
|
|
152
|
+
.option("-t, --targets <langs>", "Target languages (comma separated)", (val) => val.split(","), defaultConfig.targets)
|
|
153
|
+
.option("--localesDir <dir>", "Localization files directory", defaultConfig.localesDir)
|
|
154
|
+
.option("--debug", "Enable debug mode with verbose logging", false)
|
|
155
|
+
.option("--verbose", "Enable detailed diagnostic output", false);
|
|
156
|
+
program.on("option:debug", function () {
|
|
157
|
+
process.env.DEBUG = "true";
|
|
158
|
+
console.log("Debug mode: ENABLED (verbose logging)");
|
|
159
|
+
});
|
|
160
|
+
program.on("option:verbose", function () {
|
|
161
|
+
process.env.VERBOSE = "true";
|
|
162
|
+
console.log("Verbose mode: ENABLED (detailed diagnostics)");
|
|
163
|
+
});
|
|
164
|
+
const runCommand = async (options, commandOptions, commandName) => {
|
|
165
|
+
try {
|
|
166
|
+
const validCommands = ["translate", "fix", "analyze", "advanced"];
|
|
167
|
+
if (!validCommands.includes(commandName)) {
|
|
168
|
+
throw new Error(`Invalid command: ${commandName}`);
|
|
169
|
+
}
|
|
170
|
+
const globalOpts = program.opts();
|
|
171
|
+
if (globalOpts.debug) {
|
|
172
|
+
const safeGlobalOpts = { ...globalOpts };
|
|
173
|
+
const safeCommandOptions = { ...commandOptions };
|
|
174
|
+
if (safeGlobalOpts.targets) {
|
|
175
|
+
safeGlobalOpts.targets = `[${safeGlobalOpts.targets.length} languages]`;
|
|
176
|
+
}
|
|
177
|
+
if (safeGlobalOpts.localesDir) {
|
|
178
|
+
safeGlobalOpts.localesDir = "[directory_path]";
|
|
179
|
+
}
|
|
180
|
+
if (safeCommandOptions.provider) {
|
|
181
|
+
safeCommandOptions.provider = "[provider_name]";
|
|
182
|
+
}
|
|
183
|
+
console.log("CLI Command:", commandName);
|
|
184
|
+
console.log("Global Options:", JSON.stringify(safeGlobalOpts, null, 2));
|
|
185
|
+
console.log("Command Options:", JSON.stringify(safeCommandOptions, null, 2));
|
|
186
|
+
}
|
|
187
|
+
const sanitizedGlobalOpts = { ...globalOpts };
|
|
188
|
+
const sanitizedCommandOptions = { ...commandOptions };
|
|
189
|
+
if (sanitizedGlobalOpts.source) {
|
|
190
|
+
sanitizedGlobalOpts.source = InputValidator.validateLanguageCode(sanitizedGlobalOpts.source, "source language");
|
|
191
|
+
}
|
|
192
|
+
if (sanitizedGlobalOpts.targets && Array.isArray(sanitizedGlobalOpts.targets)) {
|
|
193
|
+
sanitizedGlobalOpts.targets = InputValidator.validateLanguageCodes(sanitizedGlobalOpts.targets, "target languages");
|
|
194
|
+
}
|
|
195
|
+
if (sanitizedGlobalOpts.localesDir) {
|
|
196
|
+
sanitizedGlobalOpts.localesDir = InputValidator.validateDirectoryPath(sanitizedGlobalOpts.localesDir, "locales directory");
|
|
197
|
+
}
|
|
198
|
+
if (sanitizedCommandOptions.provider) {
|
|
199
|
+
sanitizedCommandOptions.provider = InputValidator.validateProvider(sanitizedCommandOptions.provider, "API provider");
|
|
200
|
+
}
|
|
201
|
+
if (sanitizedCommandOptions.concurrency !== undefined) {
|
|
202
|
+
const concurrency = parseInt(sanitizedCommandOptions.concurrency);
|
|
203
|
+
if (isNaN(concurrency) || concurrency < 1 || concurrency > 20) {
|
|
204
|
+
throw new Error("Concurrency must be a number between 1 and 20");
|
|
205
|
+
}
|
|
206
|
+
sanitizedCommandOptions.concurrency = concurrency;
|
|
207
|
+
}
|
|
208
|
+
if (sanitizedCommandOptions.contextThreshold !== undefined) {
|
|
209
|
+
const threshold = parseInt(sanitizedCommandOptions.contextThreshold);
|
|
210
|
+
if (isNaN(threshold) || threshold < 1 || threshold > 10) {
|
|
211
|
+
throw new Error("Context threshold must be a number between 1 and 10");
|
|
212
|
+
}
|
|
213
|
+
sanitizedCommandOptions.contextThreshold = threshold;
|
|
214
|
+
}
|
|
215
|
+
if (sanitizedCommandOptions.contextConfidence !== undefined) {
|
|
216
|
+
const confidence = parseFloat(sanitizedCommandOptions.contextConfidence);
|
|
217
|
+
if (isNaN(confidence) || confidence < 0 || confidence > 1) {
|
|
218
|
+
throw new Error("Context confidence must be a number between 0 and 1");
|
|
219
|
+
}
|
|
220
|
+
sanitizedCommandOptions.contextConfidence = confidence;
|
|
221
|
+
}
|
|
222
|
+
if (sanitizedCommandOptions.length) {
|
|
223
|
+
const validLengthModes = ["strict", "flexible", "exact", "relaxed", "smart"];
|
|
224
|
+
if (!validLengthModes.includes(sanitizedCommandOptions.length)) {
|
|
225
|
+
throw new Error(`Invalid length mode: ${sanitizedCommandOptions.length}. Valid modes: ${validLengthModes.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const mergedOpts = {
|
|
229
|
+
source: defaultConfig.source,
|
|
230
|
+
targets: defaultConfig.targets,
|
|
231
|
+
localesDir: defaultConfig.localesDir,
|
|
232
|
+
apiProvider: defaultConfig.apiProvider,
|
|
233
|
+
...(sanitizedGlobalOpts.source && { source: sanitizedGlobalOpts.source }),
|
|
234
|
+
...(sanitizedGlobalOpts.targets && { targets: sanitizedGlobalOpts.targets }),
|
|
235
|
+
...(sanitizedGlobalOpts.localesDir && {
|
|
236
|
+
localesDir: sanitizedGlobalOpts.localesDir,
|
|
237
|
+
}),
|
|
238
|
+
...sanitizedCommandOptions,
|
|
239
|
+
};
|
|
240
|
+
let concurrencyLimit = parseInt(mergedOpts.concurrency) || defaultConfig.concurrencyLimit || 5;
|
|
241
|
+
if (mergedOpts.autoOptimize) {
|
|
242
|
+
const cpuCount = os.cpus().length;
|
|
243
|
+
const memoryGB = Math.floor(os.totalmem() / (1024 * 1024 * 1024));
|
|
244
|
+
if (memoryGB < 4) {
|
|
245
|
+
concurrencyLimit = Math.min(3, cpuCount);
|
|
246
|
+
}
|
|
247
|
+
else if (memoryGB < 8) {
|
|
248
|
+
concurrencyLimit = Math.min(5, Math.ceil(cpuCount * 0.5));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
concurrencyLimit = Math.min(10, Math.ceil(cpuCount * 0.75));
|
|
252
|
+
}
|
|
253
|
+
console.log(`Auto-optimized settings for your system (${cpuCount} CPUs, ${memoryGB}GB RAM):`);
|
|
254
|
+
console.log(` - Concurrency: ${concurrencyLimit}`);
|
|
255
|
+
}
|
|
256
|
+
if (concurrencyLimit < 1 || concurrencyLimit > 20) {
|
|
257
|
+
throw new Error("Invalid concurrency limit after optimization");
|
|
258
|
+
}
|
|
259
|
+
const finalConfig = {
|
|
260
|
+
...defaultConfig,
|
|
261
|
+
command: commandName,
|
|
262
|
+
source: mergedOpts.source,
|
|
263
|
+
targets: mergedOpts.targets,
|
|
264
|
+
localesDir: mergedOpts.localesDir,
|
|
265
|
+
apiProvider: mergedOpts.provider || defaultConfig.apiProvider,
|
|
266
|
+
concurrencyLimit: concurrencyLimit,
|
|
267
|
+
cacheEnabled: mergedOpts.noCache === undefined
|
|
268
|
+
? defaultConfig.cacheEnabled
|
|
269
|
+
: !mergedOpts.noCache,
|
|
270
|
+
debug: mergedOpts.debug,
|
|
271
|
+
verbose: mergedOpts.verbose || defaultConfig.logging?.verbose || false,
|
|
272
|
+
forceUpdate: mergedOpts.force || false,
|
|
273
|
+
showDetailedStats: mergedOpts.stats || false,
|
|
274
|
+
autoOptimize: mergedOpts.autoOptimize || defaultConfig.advanced?.autoOptimize || false,
|
|
275
|
+
fixLength: commandName === "fix",
|
|
276
|
+
apiConfig: defaultConfig.apiConfig || {},
|
|
277
|
+
styleGuide: defaultConfig.styleGuide,
|
|
278
|
+
qualityChecks: defaultConfig.qualityChecks,
|
|
279
|
+
lengthControl: {
|
|
280
|
+
...defaultConfig.lengthControl,
|
|
281
|
+
mode: mergedOpts.length || defaultConfig.lengthControl?.mode || "smart",
|
|
282
|
+
},
|
|
283
|
+
retryOptions: {
|
|
284
|
+
...defaultConfig.retryOptions,
|
|
285
|
+
maxRetries: mergedOpts.maxRetries || defaultConfig.retryOptions?.maxRetries || 2,
|
|
286
|
+
initialDelay: mergedOpts.initialDelay || defaultConfig.retryOptions?.initialDelay || 1000,
|
|
287
|
+
maxDelay: mergedOpts.maxDelay || defaultConfig.retryOptions?.maxDelay || 10000,
|
|
288
|
+
jitter: defaultConfig.retryOptions?.jitter !== false,
|
|
289
|
+
},
|
|
290
|
+
context: {
|
|
291
|
+
...defaultConfig.context,
|
|
292
|
+
enabled: true,
|
|
293
|
+
debug: mergedOpts.contextDebug || defaultConfig.context.debug || false,
|
|
294
|
+
useAI: mergedOpts.useAi ||
|
|
295
|
+
mergedOpts.contextProvider !== undefined ||
|
|
296
|
+
defaultConfig.context.useAI ||
|
|
297
|
+
false,
|
|
298
|
+
aiProvider: mergedOpts.contextProvider || defaultConfig.context.aiProvider,
|
|
299
|
+
minTextLength: mergedOpts.minTextLength || defaultConfig.context.minTextLength,
|
|
300
|
+
allowNewCategories: mergedOpts.allowNewCategories !== undefined
|
|
301
|
+
? mergedOpts.allowNewCategories
|
|
302
|
+
: defaultConfig.context.allowNewCategories,
|
|
303
|
+
detection: {
|
|
304
|
+
threshold: mergedOpts.contextThreshold ||
|
|
305
|
+
defaultConfig.context.detection?.threshold ||
|
|
306
|
+
2,
|
|
307
|
+
minConfidence: mergedOpts.contextConfidence ||
|
|
308
|
+
defaultConfig.context.detection?.minConfidence ||
|
|
309
|
+
0.6,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
advanced: {
|
|
313
|
+
...defaultConfig.advanced,
|
|
314
|
+
timeoutMs: mergedOpts.timeout || defaultConfig.advanced?.timeoutMs || 60000,
|
|
315
|
+
maxKeyLength: defaultConfig.advanced?.maxKeyLength || 10000,
|
|
316
|
+
maxBatchSize: defaultConfig.advanced?.maxBatchSize || 50,
|
|
317
|
+
autoOptimize: mergedOpts.autoOptimize || defaultConfig.advanced?.autoOptimize || false,
|
|
318
|
+
debug: mergedOpts.debug || defaultConfig.advanced?.debug || false,
|
|
319
|
+
},
|
|
320
|
+
rateLimiter: {
|
|
321
|
+
...defaultConfig.rateLimiter,
|
|
322
|
+
enabled: defaultConfig.rateLimiter?.enabled !== false,
|
|
323
|
+
},
|
|
324
|
+
fileOperations: defaultConfig.fileOperations || {},
|
|
325
|
+
logging: {
|
|
326
|
+
...defaultConfig.logging,
|
|
327
|
+
verbose: mergedOpts.verbose || defaultConfig.logging?.verbose || false,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
InputValidator.validateConfig(finalConfig);
|
|
332
|
+
if (finalConfig.debug || finalConfig.verbose) {
|
|
333
|
+
console.log("Configuration validated successfully");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (configError) {
|
|
337
|
+
const error = ErrorHelper.configValidationError(configError.message.includes("validation failed")
|
|
338
|
+
? configError.message.split("\n").slice(1)
|
|
339
|
+
: [configError.message]);
|
|
340
|
+
console.error(ErrorHelper.formatError(error, { showDebug: globalOpts.debug }));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
configureComponents(finalConfig);
|
|
344
|
+
validateEnvironment();
|
|
345
|
+
if (finalConfig.debug) {
|
|
346
|
+
console.log("\nConfiguration details:");
|
|
347
|
+
const safeConfig = {
|
|
348
|
+
...finalConfig,
|
|
349
|
+
apiConfig: Object.keys(finalConfig.apiConfig || {}).reduce((acc, provider) => {
|
|
350
|
+
acc[provider] = {
|
|
351
|
+
model: finalConfig.apiConfig[provider]?.model || "configured",
|
|
352
|
+
temperature: finalConfig.apiConfig[provider]?.temperature,
|
|
353
|
+
maxTokens: finalConfig.apiConfig[provider]?.maxTokens,
|
|
354
|
+
};
|
|
355
|
+
return acc;
|
|
356
|
+
}, {}),
|
|
357
|
+
advanced: {
|
|
358
|
+
...finalConfig.advanced,
|
|
359
|
+
timeoutMs: finalConfig.advanced?.timeoutMs,
|
|
360
|
+
maxKeyLength: finalConfig.advanced?.maxKeyLength,
|
|
361
|
+
maxBatchSize: finalConfig.advanced?.maxBatchSize,
|
|
362
|
+
autoOptimize: finalConfig.advanced?.autoOptimize,
|
|
363
|
+
debug: finalConfig.advanced?.debug,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
delete safeConfig.apiProvider; // Could leak preferred provider info
|
|
367
|
+
delete safeConfig.localesDir; // Could leak file system structure
|
|
368
|
+
console.log(JSON.stringify(safeConfig, null, 2));
|
|
369
|
+
}
|
|
370
|
+
await displayPerformanceTips(finalConfig);
|
|
371
|
+
const localesDir = path.resolve(finalConfig.localesDir);
|
|
372
|
+
console.log(`\nLooking for source files in: ${localesDir}`);
|
|
373
|
+
const files = await findLocaleFiles(localesDir, finalConfig.source);
|
|
374
|
+
if (!files || !files.length) {
|
|
375
|
+
const error = ErrorHelper.fileNotFoundError(`${localesDir}/${finalConfig.source}.json`, `${finalConfig.source}.json`);
|
|
376
|
+
console.error(ErrorHelper.formatError(error, {
|
|
377
|
+
showDebug: finalConfig.debug,
|
|
378
|
+
showContext: true,
|
|
379
|
+
}));
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
switch (commandName) {
|
|
383
|
+
case "fix":
|
|
384
|
+
console.log("\nRunning in FIX mode");
|
|
385
|
+
await Promise.all(files.map((file) => validateAndFixExistingTranslations(file, finalConfig)));
|
|
386
|
+
break;
|
|
387
|
+
case "analyze":
|
|
388
|
+
console.log("\nRunning in ANALYZE mode");
|
|
389
|
+
console.log("Context analysis mode is not fully implemented yet.");
|
|
390
|
+
break;
|
|
391
|
+
case "advanced":
|
|
392
|
+
console.log("\nRunning ADVANCED configuration");
|
|
393
|
+
// Fall through to translate
|
|
394
|
+
case "translate":
|
|
395
|
+
default:
|
|
396
|
+
console.log("\nRunning in TRANSLATION mode");
|
|
397
|
+
if (finalConfig.context.useAI) {
|
|
398
|
+
console.log(`AI Context Analysis: ENABLED (Provider: ${finalConfig.context.aiProvider || finalConfig.apiProvider})`);
|
|
399
|
+
if (finalConfig.context.allowNewCategories) {
|
|
400
|
+
console.log("New category suggestions: ENABLED");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
console.log(`Performance settings:`);
|
|
404
|
+
console.log(` - Concurrency: ${finalConfig.concurrencyLimit} parallel operations`);
|
|
405
|
+
console.log(` - Caching: ${finalConfig.cacheEnabled ? "Enabled" : "Disabled"}`);
|
|
406
|
+
console.log(` - Retries: ${finalConfig.retryOptions.maxRetries} max retries`);
|
|
407
|
+
if (finalConfig.rateLimiter?.adaptiveThrottling) {
|
|
408
|
+
console.log(` - Adaptive Rate Limiting: Enabled`);
|
|
409
|
+
}
|
|
410
|
+
if (finalConfig.forceUpdate) {
|
|
411
|
+
console.log(`Warning: Force update mode ENABLED (will update existing translations)`);
|
|
412
|
+
}
|
|
413
|
+
for (const file of files) {
|
|
414
|
+
await translateFile(file, finalConfig);
|
|
415
|
+
}
|
|
416
|
+
// Exit after successful translation in CLI mode
|
|
417
|
+
// This allows translateFile to be used as library without forcing exit
|
|
418
|
+
process.exit(0);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
// Other commands (fix, analyze) handle their own completion
|
|
422
|
+
}
|
|
423
|
+
catch (validationError) {
|
|
424
|
+
if (validationError.message.includes("Configuration validation")) {
|
|
425
|
+
// Already handled above with better formatting
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.error(`\nInput validation error: ${validationError.message}`);
|
|
429
|
+
if (validationError.stack && process.env.DEBUG) {
|
|
430
|
+
console.error("\nStack trace:");
|
|
431
|
+
console.error(validationError.stack);
|
|
432
|
+
}
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
program
|
|
437
|
+
.command("translate")
|
|
438
|
+
.description("Translate missing strings (default command)")
|
|
439
|
+
.option("--provider <provider>", "Translation provider", defaultConfig.apiProvider)
|
|
440
|
+
.option("--concurrency <number>", "Number of concurrent translations", Number, defaultConfig.concurrencyLimit)
|
|
441
|
+
.option("--no-cache", "Disable translation caching")
|
|
442
|
+
.option("--force", "Force update of existing translations", false)
|
|
443
|
+
.option("--length <mode>", "Length control mode", defaultConfig.lengthControl?.mode || "smart")
|
|
444
|
+
.option("--auto-optimize", "Auto-optimize system parameters based on hardware", false)
|
|
445
|
+
.option("--stats", "Show detailed stats after completion", false)
|
|
446
|
+
.option("--min-confidence <score>", "Minimum confidence threshold (0-1)", parseFloat)
|
|
447
|
+
.option("--save-review-queue", "Save low-confidence items for manual review", false)
|
|
448
|
+
.action(async (options) => {
|
|
449
|
+
try {
|
|
450
|
+
await runCommand(program.opts(), options, "translate");
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
// Handle errors with better formatting
|
|
454
|
+
if (error.code && error.code.startsWith("ERR_")) {
|
|
455
|
+
console.error(ErrorHelper.formatError(error, {
|
|
456
|
+
showDebug: process.env.DEBUG === "true",
|
|
457
|
+
showSolutions: true,
|
|
458
|
+
showContext: true,
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
console.error(`\nError: ${error.message}`);
|
|
463
|
+
if (error.stack && process.env.DEBUG) {
|
|
464
|
+
console.error(error.stack);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
program
|
|
471
|
+
.command("fix")
|
|
472
|
+
.description("Fix issues in existing translations")
|
|
473
|
+
.option("--length", "Fix length issues in existing translations", true)
|
|
474
|
+
.action(async (options) => {
|
|
475
|
+
try {
|
|
476
|
+
await runCommand(program.opts(), options, "fix");
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (error.code && error.code.startsWith("ERR_")) {
|
|
480
|
+
console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
console.error(`\nError: ${error.message}`);
|
|
484
|
+
if (error.stack && process.env.DEBUG) {
|
|
485
|
+
console.error(error.stack);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
program
|
|
492
|
+
.command("analyze")
|
|
493
|
+
.description("Analyze context patterns")
|
|
494
|
+
.option("--use-ai", "Enable AI-based context analysis", defaultConfig.context.useAI)
|
|
495
|
+
.option("--context-provider <provider>", "AI provider for analysis", defaultConfig.context.aiProvider)
|
|
496
|
+
.option("--context-threshold <number>", "Minimum match count", Number, defaultConfig.context.detection.threshold)
|
|
497
|
+
.action(async (options) => {
|
|
498
|
+
try {
|
|
499
|
+
await runCommand(program.opts(), options, "analyze");
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
if (error.code && error.code.startsWith("ERR_")) {
|
|
503
|
+
console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
console.error(`\nError: ${error.message}`);
|
|
507
|
+
if (error.stack && process.env.DEBUG) {
|
|
508
|
+
console.error(error.stack);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
program
|
|
515
|
+
.command("advanced")
|
|
516
|
+
.description("Access advanced configuration options")
|
|
517
|
+
.option("--context-confidence <number>", "Minimum confidence score", Number, defaultConfig.context.detection.minConfidence)
|
|
518
|
+
.option("--context-debug", "Show context details", defaultConfig.context.debug)
|
|
519
|
+
.option("--min-text-length <number>", "Minimum text length for AI analysis", Number, defaultConfig.context.minTextLength)
|
|
520
|
+
.option("--allow-new-categories", "Allow AI to suggest new categories", defaultConfig.context.allowNewCategories)
|
|
521
|
+
.option("--max-retries <number>", "Maximum number of retries for API calls", Number, defaultConfig.retryOptions?.maxRetries || 2)
|
|
522
|
+
.action(async (options) => {
|
|
523
|
+
try {
|
|
524
|
+
await runCommand(program.opts(), options, "advanced");
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
if (error.code && error.code.startsWith("ERR_")) {
|
|
528
|
+
console.error(ErrorHelper.formatError(error, { showDebug: process.env.DEBUG === "true" }));
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
console.error(`\nError: ${error.message}`);
|
|
532
|
+
if (error.stack && process.env.DEBUG) {
|
|
533
|
+
console.error(error.stack);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
program
|
|
540
|
+
.command("validate-config")
|
|
541
|
+
.description("Validate configuration file without running translations")
|
|
542
|
+
.option("--show-warnings", "Show configuration warnings", false)
|
|
543
|
+
.action(async (options) => {
|
|
544
|
+
try {
|
|
545
|
+
console.log("Validating configuration...\n");
|
|
546
|
+
// Temporarily enable verbose to show warnings if requested
|
|
547
|
+
const originalDebug = defaultConfig.debug;
|
|
548
|
+
const originalVerbose = defaultConfig.verbose;
|
|
549
|
+
if (options.showWarnings) {
|
|
550
|
+
defaultConfig.debug = true;
|
|
551
|
+
defaultConfig.verbose = true;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
InputValidator.validateConfig(defaultConfig);
|
|
555
|
+
console.log("Configuration is valid!\n");
|
|
556
|
+
// Show summary
|
|
557
|
+
console.log("Configuration Summary:");
|
|
558
|
+
if (defaultConfig.version) {
|
|
559
|
+
console.log(` Version: ${defaultConfig.version}`);
|
|
560
|
+
}
|
|
561
|
+
console.log(` Source: ${defaultConfig.source}`);
|
|
562
|
+
console.log(` Targets: ${defaultConfig.targets.length} languages (${defaultConfig.targets.slice(0, 5).join(", ")}${defaultConfig.targets.length > 5 ? "..." : ""})`);
|
|
563
|
+
console.log(` API Provider: ${defaultConfig.apiProvider || "auto-detect"}`);
|
|
564
|
+
console.log(` Concurrency: ${defaultConfig.concurrencyLimit || 5} parallel operations`);
|
|
565
|
+
console.log(` Cache: ${defaultConfig.cacheEnabled !== false ? "Enabled" : "Disabled"}`);
|
|
566
|
+
if (defaultConfig.context?.enabled) {
|
|
567
|
+
console.log(` Context Detection: ${defaultConfig.context.useAI ? "AI-powered" : "Keyword-based"}`);
|
|
568
|
+
}
|
|
569
|
+
if (defaultConfig.useFallback) {
|
|
570
|
+
const fallbackChain = defaultConfig.fallbackOrder?.slice(0, 3).join(" → ") || "auto";
|
|
571
|
+
console.log(` Fallback Chain: ${fallbackChain}`);
|
|
572
|
+
}
|
|
573
|
+
console.log("\nYour configuration is ready to use!");
|
|
574
|
+
console.log("Run 'localize translate' to start translating\n");
|
|
575
|
+
}
|
|
576
|
+
finally {
|
|
577
|
+
// Restore original values
|
|
578
|
+
defaultConfig.debug = originalDebug;
|
|
579
|
+
defaultConfig.verbose = originalVerbose;
|
|
580
|
+
}
|
|
581
|
+
process.exit(0);
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
console.error("\nConfiguration Validation Failed:\n");
|
|
585
|
+
console.error(error.message);
|
|
586
|
+
console.error("\nFix the errors above and try again\n");
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
program
|
|
591
|
+
.command("review")
|
|
592
|
+
.description("Interactively review low-confidence translations")
|
|
593
|
+
.option("--export <format>", "Export review queue (json|csv)")
|
|
594
|
+
.action(async (options) => {
|
|
595
|
+
try {
|
|
596
|
+
const ReviewCommand = (await import("../src/commands/review.js")).default;
|
|
597
|
+
const review = new ReviewCommand(defaultConfig);
|
|
598
|
+
if (options.export) {
|
|
599
|
+
review.exportReviewQueue(options.export);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
await review.startReview();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
console.error(`\nError: ${error.message}`);
|
|
607
|
+
if (error.stack && process.env.DEBUG) {
|
|
608
|
+
console.error(error.stack);
|
|
609
|
+
}
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
program.action(async () => {
|
|
614
|
+
try {
|
|
615
|
+
await runCommand(program.opts(), {}, "translate");
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.error(`\nError: ${error.message}`);
|
|
619
|
+
if (error.stack && process.env.DEBUG) {
|
|
620
|
+
console.error(error.stack);
|
|
621
|
+
}
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
program.parse(process.argv);
|
|
626
|
+
return defaultConfig;
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Validate that necessary environment variables are set.
|
|
630
|
+
* @returns {Array<string>} - List of available providers.
|
|
631
|
+
*/
|
|
632
|
+
const validateEnvironment = () => {
|
|
633
|
+
try {
|
|
634
|
+
// Check that ProviderFactory can validate providers
|
|
635
|
+
const availableProviders = ProviderFactory.validateProviders();
|
|
636
|
+
console.log(`\nAvailable API providers: ${availableProviders.join(", ")}`);
|
|
637
|
+
return availableProviders;
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
console.error("\nError: " + error.message);
|
|
641
|
+
console.error("Please set at least one of the following environment variables:");
|
|
642
|
+
const possibleProviders = [
|
|
643
|
+
"DASHSCOPE_API_KEY",
|
|
644
|
+
"OPENAI_API_KEY",
|
|
645
|
+
"DEEPSEEK_API_KEY",
|
|
646
|
+
"GEMINI_API_KEY",
|
|
647
|
+
"XAI_API_KEY",
|
|
648
|
+
];
|
|
649
|
+
possibleProviders.forEach((key) => console.error(` - ${key}`));
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* Display performance optimizations and tips.
|
|
655
|
+
* @param {any} options - Configuration options.
|
|
656
|
+
*/
|
|
657
|
+
const displayPerformanceTips = async (options) => {
|
|
658
|
+
if (!options.debug)
|
|
659
|
+
return;
|
|
660
|
+
try {
|
|
661
|
+
const cpuCount = os.cpus().length;
|
|
662
|
+
const cpuModel = os.cpus()[0]?.model || "Unknown CPU";
|
|
663
|
+
const memoryGB = Math.floor(os.totalmem() / (1024 * 1024 * 1024));
|
|
664
|
+
const freememGB = Math.floor(os.freemem() / (1024 * 1024 * 1024));
|
|
665
|
+
const orchestrator = new Orchestrator(options);
|
|
666
|
+
const cacheStats = orchestrator.getCacheStats();
|
|
667
|
+
console.log("\nPerformance Information:");
|
|
668
|
+
console.log(` - CPU: ${cpuModel} (${cpuCount} cores)`);
|
|
669
|
+
console.log(` - Memory: ${freememGB}GB free of ${memoryGB}GB total`);
|
|
670
|
+
console.log(` - Concurrency: ${options.concurrencyLimit} parallel operations`);
|
|
671
|
+
console.log(` - Cache: ${options.cacheEnabled ? "Enabled" : "Disabled"} (${cacheStats.size} items cached)`);
|
|
672
|
+
// Provide tips
|
|
673
|
+
console.log("\nPerformance Tips:");
|
|
674
|
+
if (cpuCount > options.concurrencyLimit * 2) {
|
|
675
|
+
console.log(` - Increase concurrency with --concurrency ${Math.min(cpuCount, 10)} to better utilize your CPU`);
|
|
676
|
+
}
|
|
677
|
+
if (!options.cacheEnabled) {
|
|
678
|
+
console.log(` - Enable caching to improve repeated runs speed`);
|
|
679
|
+
}
|
|
680
|
+
if (memoryGB < 4) {
|
|
681
|
+
console.log(` - Your system has limited memory (${memoryGB}GB). Consider reducing concurrency if you experience issues.`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
console.log("Warning: Could not calculate performance tips", error);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
try {
|
|
689
|
+
await loadEnvironmentVariables();
|
|
690
|
+
let defaultConfig;
|
|
691
|
+
try {
|
|
692
|
+
const { config, configFile } = await loadConfig();
|
|
693
|
+
defaultConfig = config;
|
|
694
|
+
if (configFile) {
|
|
695
|
+
console.log(`Loaded config from: ${path.relative(process.cwd(), configFile)}`);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
console.log("No config file found, using defaults");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
console.warn(`Warning: Could not load config: ${err.message}`);
|
|
703
|
+
// Fallback defaults if logic fails totally (shouldn't with c12 defaults)
|
|
704
|
+
defaultConfig = {
|
|
705
|
+
source: "en",
|
|
706
|
+
targets: [],
|
|
707
|
+
concurrencyLimit: 5,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
await configureCLI(defaultConfig);
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
console.error(`\nError: ${error.message}`);
|
|
714
|
+
if (error.stack && process.env.DEBUG) {
|
|
715
|
+
console.error(error.stack);
|
|
716
|
+
}
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|