locai 2.6.1 → 2.7.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 +23 -25
- package/dist/cli/helpers.d.ts +34 -2
- package/dist/cli/helpers.js +96 -37
- package/dist/cli.js +40 -39
- package/dist/commands/review.d.ts +33 -8
- package/dist/commands/review.js +89 -22
- package/dist/commands/translator.d.ts +4 -2
- package/dist/commands/translator.js +1 -1
- package/dist/config/index.d.ts +53 -66
- package/dist/config/index.js +2 -1
- package/dist/config/setup.d.ts +3 -2
- package/dist/config/setup.js +5 -3
- package/dist/core/adapters/android-adapter.d.ts +3 -2
- package/dist/core/adapters/android-adapter.js +12 -13
- package/dist/core/adapters/base-adapter.d.ts +13 -2
- package/dist/core/adapters/ios-adapter.d.ts +3 -2
- package/dist/core/adapters/json-adapter.d.ts +4 -3
- package/dist/core/adapters/json-adapter.js +1 -3
- package/dist/core/adapters/po-adapter.d.ts +3 -2
- package/dist/core/adapters/po-adapter.js +4 -5
- package/dist/core/adapters/properties-adapter.d.ts +3 -2
- package/dist/core/adapters/properties-adapter.js +2 -4
- package/dist/core/adapters/yaml-adapter.d.ts +4 -3
- package/dist/core/adapters/yaml-adapter.js +1 -3
- package/dist/core/context-processor.d.ts +21 -6
- package/dist/core/context-processor.js +2 -7
- package/dist/core/detection/framework-detector.js +1 -1
- package/dist/core/detection/structure-detector.js +1 -1
- package/dist/core/fallback-provider.d.ts +3 -4
- package/dist/core/fallback-provider.js +15 -21
- package/dist/core/orchestrator.d.ts +98 -26
- package/dist/core/orchestrator.js +56 -116
- package/dist/core/pipeline/Pipeline.d.ts +2 -2
- package/dist/core/pipeline/Pipeline.js +1 -3
- package/dist/core/pipeline/context.d.ts +69 -13
- package/dist/core/pipeline/context.js +1 -7
- package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
- package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
- package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
- package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
- package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
- package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
- package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
- package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
- package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
- package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
- package/dist/core/pipeline/steps/TranslationStep.js +21 -23
- package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
- package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
- package/dist/core/provider-factory.d.ts +15 -9
- package/dist/core/provider-factory.js +42 -32
- package/dist/providers/anthropic.d.ts +5 -4
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/api-types.d.ts +74 -0
- package/dist/providers/api-types.js +5 -0
- package/dist/providers/base-provider.d.ts +27 -31
- package/dist/providers/base-provider.js +22 -46
- package/dist/providers/dashscope.js +0 -2
- package/dist/providers/deepseek.js +0 -1
- package/dist/providers/gemini.js +0 -1
- package/dist/providers/openai.d.ts +3 -3
- package/dist/providers/openai.js +0 -1
- package/dist/providers/xai.js +0 -1
- package/dist/services/embedding-provider.js +4 -2
- package/dist/services/language-processor.d.ts +5 -3
- package/dist/services/language-processor.js +38 -43
- package/dist/services/source-analyzer.d.ts +24 -0
- package/dist/services/source-analyzer.js +161 -68
- package/dist/services/translation-service.d.ts +31 -13
- package/dist/services/translation-service.js +116 -71
- package/dist/services/vector-store.d.ts +15 -2
- package/dist/services/vector-store.js +14 -10
- package/dist/types/index.d.ts +31 -33
- package/dist/utils/ai-context-analyzer.d.ts +12 -3
- package/dist/utils/ai-context-analyzer.js +18 -10
- package/dist/utils/confidence-scorer.d.ts +16 -19
- package/dist/utils/confidence-scorer.js +36 -125
- package/dist/utils/error-helper.d.ts +27 -6
- package/dist/utils/error-helper.js +10 -9
- package/dist/utils/file-manager.d.ts +8 -7
- package/dist/utils/file-manager.js +22 -23
- package/dist/utils/glossary-manager.d.ts +22 -3
- package/dist/utils/glossary-manager.js +47 -28
- package/dist/utils/graceful-shutdown.d.ts +7 -1
- package/dist/utils/graceful-shutdown.js +1 -2
- package/dist/utils/input-validator.d.ts +53 -66
- package/dist/utils/input-validator.js +100 -151
- package/dist/utils/locale-paths.d.ts +7 -0
- package/dist/utils/locale-paths.js +16 -0
- package/dist/utils/logger.d.ts +8 -8
- package/dist/utils/logger.js +7 -15
- package/dist/utils/object-transformer.d.ts +20 -5
- package/dist/utils/object-transformer.js +20 -28
- package/dist/utils/progress-tracker.js +2 -3
- package/dist/utils/prompt-templates.d.ts +45 -5
- package/dist/utils/prompt-templates.js +61 -5
- package/dist/utils/quality/base-checker.d.ts +40 -18
- package/dist/utils/quality/base-checker.js +1 -2
- package/dist/utils/quality/code-block-checker.d.ts +13 -5
- package/dist/utils/quality/index.d.ts +2 -1
- package/dist/utils/quality/index.js +8 -27
- package/dist/utils/quality/length-checker.d.ts +35 -5
- package/dist/utils/quality/length-checker.js +2 -3
- package/dist/utils/quality/markdown-checker.d.ts +14 -7
- package/dist/utils/quality/special-characters-checker.d.ts +18 -1
- package/dist/utils/quality/special-characters-checker.js +0 -1
- package/dist/utils/quality/style-guide-checker.d.ts +1 -1
- package/dist/utils/quality/style-guide-checker.js +0 -2
- package/dist/utils/quality/tone-verifier.js +1 -1
- package/dist/utils/rate-limiter.d.ts +17 -1
- package/dist/utils/rate-limiter.js +12 -11
- package/dist/utils/retry-helper.d.ts +21 -3
- package/dist/utils/retry-helper.js +11 -3
- package/dist/utils/review-queue.d.ts +45 -0
- package/dist/utils/review-queue.js +53 -0
- package/dist/utils/state-manager.d.ts +12 -3
- package/dist/utils/state-manager.js +32 -7
- package/dist/utils/statistics-manager.d.ts +11 -1
- package/dist/utils/statistics-manager.js +8 -0
- package/dist/utils/ui-manager.d.ts +2 -1
- package/dist/utils/ui-manager.js +15 -1
- package/package.json +22 -17
- package/dist/commands/interactive-review.d.ts +0 -9
- package/dist/commands/interactive-review.js +0 -109
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
- package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
- package/dist/utils/quality/__tests__/tone-verifier.test.js +0 -91
|
@@ -2,12 +2,16 @@ import path from "path";
|
|
|
2
2
|
import { FileManager } from "../utils/file-manager.js";
|
|
3
3
|
import ObjectTransformer from "../utils/object-transformer.js";
|
|
4
4
|
import Orchestrator from "../core/orchestrator.js";
|
|
5
|
+
import ProviderFactory, { NoProviderError } from "../core/provider-factory.js";
|
|
6
|
+
import ConfidenceScorer from "../utils/confidence-scorer.js";
|
|
7
|
+
import { updateReviewQueue } from "../utils/review-queue.js";
|
|
5
8
|
import QualityChecker from "../utils/quality/index.js";
|
|
6
9
|
import StateManager from "../utils/state-manager.js";
|
|
7
10
|
import InputValidator from "../utils/input-validator.js";
|
|
11
|
+
import { targetLocaleFile } from "../utils/locale-paths.js";
|
|
8
12
|
import gracefulShutdown from "../utils/graceful-shutdown.js";
|
|
9
13
|
import uiManager from "../utils/ui-manager.js";
|
|
10
|
-
import statisticsManager from "../utils/statistics-manager.js";
|
|
14
|
+
import statisticsManager, { runFailures } from "../utils/statistics-manager.js";
|
|
11
15
|
import { LanguageProcessor } from "./language-processor.js";
|
|
12
16
|
import { SourceCodeAnalyzer } from "./source-analyzer.js";
|
|
13
17
|
/**
|
|
@@ -24,6 +28,8 @@ class TranslationService {
|
|
|
24
28
|
* @throws {Error} If validation fails or translation process errors.
|
|
25
29
|
*/
|
|
26
30
|
async translateFile(file, options) {
|
|
31
|
+
// Stop before reading or changing anything when no provider could translate a key.
|
|
32
|
+
ProviderFactory.assertProviderAvailable(options.apiProvider, options.useFallback !== false);
|
|
27
33
|
await uiManager.log(`\nProcessing File: "${path.basename(file)}"`);
|
|
28
34
|
try {
|
|
29
35
|
const resolvedFile = await this.validateTranslationInputs(file, options);
|
|
@@ -32,40 +38,51 @@ class TranslationService {
|
|
|
32
38
|
const flattenedSource = ObjectTransformer.flatten(sourceContent);
|
|
33
39
|
const totalKeys = Object.keys(flattenedSource).length;
|
|
34
40
|
await uiManager.log(`Source file contains ${totalKeys} translation keys`);
|
|
35
|
-
const { stateManager, projectRoot, currentState, comparison } = await this.initializeTranslationState(resolvedFile, flattenedSource, options);
|
|
36
|
-
|
|
41
|
+
const { stateManager, projectRoot, previousState, currentState, comparison } = await this.initializeTranslationState(resolvedFile, flattenedSource, options);
|
|
42
|
+
// Interrupted (Ctrl-C): changed keys may not be translated yet, so keep them changed.
|
|
43
|
+
const saveStateOnShutdown = async () => {
|
|
37
44
|
try {
|
|
38
|
-
await stateManager.saveState(projectRoot, currentState
|
|
45
|
+
await stateManager.saveState(projectRoot, stateManager.stateAfterRun(previousState, currentState, [
|
|
46
|
+
...comparison.newKeys,
|
|
47
|
+
...comparison.modifiedKeys,
|
|
48
|
+
]));
|
|
39
49
|
console.log("State saved during shutdown");
|
|
40
50
|
}
|
|
41
51
|
catch (error) {
|
|
42
|
-
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
console.error("Failed to save state during shutdown:", message);
|
|
43
54
|
}
|
|
44
|
-
}
|
|
55
|
+
};
|
|
56
|
+
gracefulShutdown.registerCallback(saveStateOnShutdown);
|
|
45
57
|
statisticsManager.reset();
|
|
46
58
|
const globalStats = statisticsManager.getStats();
|
|
47
59
|
try {
|
|
48
60
|
await this.processAllLanguages(resolvedFile, flattenedSource, options, globalStats, comparison);
|
|
49
|
-
await this.finalizeTranslation(stateManager, projectRoot, currentState, globalStats, startTime, options);
|
|
61
|
+
await this.finalizeTranslation(stateManager, projectRoot, previousState, currentState, comparison, globalStats, startTime, options);
|
|
50
62
|
return globalStats;
|
|
51
63
|
}
|
|
52
64
|
catch (error) {
|
|
53
|
-
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
await uiManager.log(`\nTranslation error: ${message}`);
|
|
54
67
|
globalStats.error = {
|
|
55
|
-
message
|
|
68
|
+
message,
|
|
56
69
|
time: new Date().toISOString(),
|
|
57
|
-
stack: process.env.DEBUG ? error.stack : undefined,
|
|
70
|
+
stack: process.env.DEBUG && error instanceof Error ? error.stack : undefined,
|
|
58
71
|
};
|
|
59
72
|
throw error;
|
|
60
73
|
}
|
|
61
74
|
finally {
|
|
75
|
+
gracefulShutdown.unregisterCallback(saveStateOnShutdown);
|
|
62
76
|
if (process.env.DEBUG) {
|
|
63
77
|
await uiManager.log("\nCache statistics:");
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
catch (validationError) {
|
|
68
|
-
|
|
82
|
+
const message = validationError instanceof Error
|
|
83
|
+
? validationError.message
|
|
84
|
+
: String(validationError);
|
|
85
|
+
await uiManager.log(`\nInput validation error: ${message}`);
|
|
69
86
|
throw validationError;
|
|
70
87
|
}
|
|
71
88
|
}
|
|
@@ -102,7 +119,7 @@ class TranslationService {
|
|
|
102
119
|
/**
|
|
103
120
|
* Initialize translation state and sync analysis.
|
|
104
121
|
* @param {string} resolvedFile - Path to resolved source file.
|
|
105
|
-
* @param {
|
|
122
|
+
* @param {FlatTranslations} flattenedSource - Flattened source content.
|
|
106
123
|
* @param {TranslationOptions} options - Translation options.
|
|
107
124
|
* @returns {Promise<Object>} - Initialized state components.
|
|
108
125
|
*/
|
|
@@ -138,15 +155,15 @@ class TranslationService {
|
|
|
138
155
|
else {
|
|
139
156
|
await uiManager.log(`First run - will process all keys`);
|
|
140
157
|
}
|
|
141
|
-
return { stateManager, projectRoot, currentState, comparison };
|
|
158
|
+
return { stateManager, projectRoot, previousState, currentState, comparison };
|
|
142
159
|
}
|
|
143
160
|
/**
|
|
144
161
|
* Process all target languages with concurrency.
|
|
145
162
|
* @param {string} resolvedFile - Path to source file.
|
|
146
|
-
* @param {
|
|
163
|
+
* @param {FlatTranslations} flattenedSource - Flattened source content.
|
|
147
164
|
* @param {TranslationOptions} options - Translation options.
|
|
148
165
|
* @param {GlobalStats} globalStats - Global statistics object to update.
|
|
149
|
-
* @param {
|
|
166
|
+
* @param {StateComparison} comparison - State comparison result.
|
|
150
167
|
*/
|
|
151
168
|
async processAllLanguages(resolvedFile, flattenedSource, options, globalStats, comparison) {
|
|
152
169
|
const languageConcurrency = options.concurrencyLimit || 3;
|
|
@@ -155,7 +172,10 @@ class TranslationService {
|
|
|
155
172
|
// Initialize shared SourceCodeAnalyzer
|
|
156
173
|
console.log("Initializing Locai Engine...");
|
|
157
174
|
const sharedAnalyzer = new SourceCodeAnalyzer();
|
|
158
|
-
|
|
175
|
+
// Scan the source code only for deep context; disabled, the analyzer stays empty
|
|
176
|
+
if (options.deepContext !== false) {
|
|
177
|
+
sharedAnalyzer.initialize();
|
|
178
|
+
}
|
|
159
179
|
// Create shared orchestrators array to collect review queues
|
|
160
180
|
const orchestrators = [];
|
|
161
181
|
for (let i = 0; i < targetLanguages.length; i += languageConcurrency) {
|
|
@@ -186,11 +206,11 @@ class TranslationService {
|
|
|
186
206
|
* Process a single language translation.
|
|
187
207
|
* @param {string} targetLang - Target language code.
|
|
188
208
|
* @param {string} sourceFile - Path to source file.
|
|
189
|
-
* @param {
|
|
209
|
+
* @param {FlatTranslations} flattenedSource - Flattened source content.
|
|
190
210
|
* @param {Orchestrator} orchestrator - Orchestrator instance.
|
|
191
211
|
* @param {TranslationOptions} options - Translation options.
|
|
192
212
|
* @param {GlobalStats} globalStats - Global statistics object.
|
|
193
|
-
* @param {
|
|
213
|
+
* @param {StateComparison} comparison - State comparison result.
|
|
194
214
|
* @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
|
|
195
215
|
*/
|
|
196
216
|
async processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison) {
|
|
@@ -200,44 +220,81 @@ class TranslationService {
|
|
|
200
220
|
* Finalize translation process and save state.
|
|
201
221
|
* @param {StateManager} stateManager - State manager instance.
|
|
202
222
|
* @param {string} projectRoot - Project root directory.
|
|
203
|
-
* @param {
|
|
223
|
+
* @param {Record<string, string>} previousState - State loaded at the start of the run.
|
|
224
|
+
* @param {Record<string, string>} currentState - Current state (key → content hash).
|
|
225
|
+
* @param {StateComparison} comparison - New, modified and deleted keys of this run.
|
|
204
226
|
* @param {GlobalStats} globalStats - Global statistics object.
|
|
205
227
|
* @param {number} startTime - Process start timestamp.
|
|
206
228
|
* @param {TranslationOptions} options - Translation options.
|
|
207
229
|
*/
|
|
208
|
-
async finalizeTranslation(stateManager, projectRoot, currentState, globalStats, startTime, options) {
|
|
230
|
+
async finalizeTranslation(stateManager, projectRoot, previousState, currentState, comparison, globalStats, startTime, options) {
|
|
209
231
|
globalStats.endTime = new Date().toISOString();
|
|
210
232
|
globalStats.totalDuration = (Date.now() - startTime) / 1000;
|
|
211
233
|
globalStats.totalTime = globalStats.totalDuration;
|
|
212
234
|
await uiManager.displayGlobalSummary(globalStats, (options.targets || []).length);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
orchestrator.confidenceSettings.reviewQueue) {
|
|
221
|
-
allReviewItems.push(...orchestrator.confidenceSettings.reviewQueue);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
// If we have items to review, save them
|
|
225
|
-
if (allReviewItems.length > 0) {
|
|
226
|
-
// Use first orchestrator to save, but update its review queue
|
|
227
|
-
const orchestrator = globalStats.orchestrators[0];
|
|
228
|
-
orchestrator.confidenceSettings.reviewQueue = allReviewItems;
|
|
229
|
-
orchestrator.saveReviewQueue();
|
|
230
|
-
}
|
|
235
|
+
await this.reportConfidence(globalStats.orchestrators);
|
|
236
|
+
// Keys that failed in any language (or every changed key, if a whole language failed)
|
|
237
|
+
// keep their previous hash, so the next run still treats them as changed.
|
|
238
|
+
const unfinishedKeys = new Set(globalStats.failedKeys);
|
|
239
|
+
if (runFailures(globalStats).failedLanguages.length > 0) {
|
|
240
|
+
for (const key of [...comparison.newKeys, ...comparison.modifiedKeys]) {
|
|
241
|
+
unfinishedKeys.add(key);
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
244
|
try {
|
|
234
|
-
await stateManager.saveState(projectRoot, currentState);
|
|
245
|
+
await stateManager.saveState(projectRoot, stateManager.stateAfterRun(previousState, currentState, unfinishedKeys));
|
|
235
246
|
if (options.debug) {
|
|
236
247
|
await uiManager.log(`State saved for future sync operations`);
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
250
|
catch (error) {
|
|
240
|
-
|
|
251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
+
await uiManager.log(`Warning: Could not save state: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Print the confidence scores of the run and, when the review queue is enabled, add the
|
|
257
|
+
* flagged translations to `.localize-cache/review-queue.json`.
|
|
258
|
+
* @param {Orchestrator[]} orchestrators - The run's orchestrators (they share the settings).
|
|
259
|
+
*/
|
|
260
|
+
async reportConfidence(orchestrators) {
|
|
261
|
+
const settings = orchestrators[0]?.confidenceSettings;
|
|
262
|
+
if (!settings?.enabled) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const scored = orchestrators.flatMap((o) => o.confidenceSettings.scored);
|
|
266
|
+
const flagged = orchestrators.flatMap((o) => o.confidenceSettings.reviewQueue);
|
|
267
|
+
const rejected = flagged.filter((item) => item.rejected).length;
|
|
268
|
+
const autoApproved = orchestrators.reduce((sum, o) => sum + o.confidenceSettings.autoApprovedCount, 0);
|
|
269
|
+
if (scored.length > 0) {
|
|
270
|
+
await uiManager.log(`\nConfidence scores: ${scored.length} translations scored`);
|
|
271
|
+
await uiManager.log(` Auto-approved (score >= ${settings.autoApproveThreshold}): ${autoApproved}`);
|
|
272
|
+
await uiManager.log(` Need review (score < ${settings.reviewThreshold}): ${flagged.length - rejected}`);
|
|
273
|
+
await uiManager.log(` Rejected, not written (score < ${settings.rejectThreshold}): ${rejected}`);
|
|
274
|
+
}
|
|
275
|
+
if (!settings.saveReviewQueue) {
|
|
276
|
+
if (flagged.length > 0) {
|
|
277
|
+
const listed = 20;
|
|
278
|
+
for (const item of flagged.slice(0, listed)) {
|
|
279
|
+
await uiManager.log(` ${item.language} ${item.key}: ${ConfidenceScorer.formatConfidence(item.confidence.score)}${item.rejected ? " (rejected)" : ""}`);
|
|
280
|
+
}
|
|
281
|
+
if (flagged.length > listed) {
|
|
282
|
+
await uiManager.log(` ...and ${flagged.length - listed} more`);
|
|
283
|
+
}
|
|
284
|
+
await uiManager.log(" Save them for 'locai review' with --save-review-queue");
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const { file, size } = updateReviewQueue(scored, flagged);
|
|
290
|
+
await uiManager.log(`Review queue: ${size} items in ${file}`);
|
|
291
|
+
if (size > 0) {
|
|
292
|
+
await uiManager.log(" Run 'locai review' to approve or edit them");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
297
|
+
await uiManager.log(`Warning: Failed to save review queue: ${message}`);
|
|
241
298
|
}
|
|
242
299
|
}
|
|
243
300
|
/**
|
|
@@ -247,25 +304,11 @@ class TranslationService {
|
|
|
247
304
|
* @param {TranslationOptions} options - Translation options.
|
|
248
305
|
*/
|
|
249
306
|
async removeDeletedKeysFromTargets(sourceFile, deletedKeys, options) {
|
|
250
|
-
const sourceDir = path.dirname(sourceFile);
|
|
251
|
-
// Detect structure based on source file name
|
|
252
|
-
const isNestedStructure = path.basename(sourceFile) === "translation.json";
|
|
253
307
|
let totalRemoved = 0;
|
|
254
308
|
let filesProcessed = 0;
|
|
255
309
|
for (const targetLang of options.targets || []) {
|
|
256
310
|
try {
|
|
257
|
-
|
|
258
|
-
if (isNestedStructure) {
|
|
259
|
-
// Nested: locales/tr/translation.json
|
|
260
|
-
const localesRoot = path.dirname(sourceDir);
|
|
261
|
-
const targetDir = path.join(localesRoot, targetLang);
|
|
262
|
-
targetPath = path.join(targetDir, "translation.json");
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// Flat: locales/tr.json
|
|
266
|
-
const safeTargetFilename = `${targetLang}.json`;
|
|
267
|
-
targetPath = InputValidator.createSafeFilePath(sourceDir, safeTargetFilename);
|
|
268
|
-
}
|
|
311
|
+
const targetPath = targetLocaleFile(sourceFile, targetLang);
|
|
269
312
|
// Check if target file exists
|
|
270
313
|
const fileExists = await FileManager.exists(targetPath);
|
|
271
314
|
if (!fileExists) {
|
|
@@ -296,7 +339,8 @@ class TranslationService {
|
|
|
296
339
|
filesProcessed++;
|
|
297
340
|
}
|
|
298
341
|
catch (error) {
|
|
299
|
-
|
|
342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
343
|
+
await uiManager.log(` \u274c Error processing ${targetLang}: ${message}`);
|
|
300
344
|
}
|
|
301
345
|
}
|
|
302
346
|
await uiManager.log(`Cleanup Summary: Removed ${totalRemoved} keys from ${filesProcessed} files\n`);
|
|
@@ -305,6 +349,7 @@ class TranslationService {
|
|
|
305
349
|
* Validate and fix existing translations that have length issues.
|
|
306
350
|
* @param {string} file - Path to source file.
|
|
307
351
|
* @param {TranslationOptions} options - Translation options.
|
|
352
|
+
* @returns {Promise<FixSummary>} - How many length issues were found and fixed.
|
|
308
353
|
* @throws {Error} If validation process fails.
|
|
309
354
|
*/
|
|
310
355
|
async validateAndFixExistingTranslations(file, options) {
|
|
@@ -312,29 +357,21 @@ class TranslationService {
|
|
|
312
357
|
const sourceContent = await FileManager.readJSON(file);
|
|
313
358
|
const flattenedSource = ObjectTransformer.flatten(sourceContent);
|
|
314
359
|
const orchestrator = new Orchestrator(options);
|
|
315
|
-
const sourceDir = path.dirname(file);
|
|
316
|
-
const isNestedStructure = path.basename(file) === "translation.json";
|
|
317
360
|
const languageResults = await Promise.all((options.targets || []).map(async (targetLang) => {
|
|
318
361
|
try {
|
|
319
|
-
|
|
320
|
-
if (isNestedStructure) {
|
|
321
|
-
const localesRoot = path.dirname(sourceDir);
|
|
322
|
-
targetPath = path.join(localesRoot, targetLang, "translation.json");
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
targetPath = path.join(sourceDir, `${targetLang}.json`);
|
|
326
|
-
}
|
|
362
|
+
const targetPath = targetLocaleFile(file, targetLang);
|
|
327
363
|
const targetContent = await FileManager.readJSON(targetPath);
|
|
328
364
|
return { targetLang, targetPath, content: targetContent, success: true };
|
|
329
365
|
}
|
|
330
366
|
catch (err) {
|
|
331
|
-
|
|
367
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
368
|
+
await uiManager.log(`Could not read ${targetLang}: ${message}`);
|
|
332
369
|
return {
|
|
333
370
|
targetLang,
|
|
334
371
|
targetPath: "",
|
|
335
372
|
content: {},
|
|
336
373
|
success: false,
|
|
337
|
-
error:
|
|
374
|
+
error: message,
|
|
338
375
|
};
|
|
339
376
|
}
|
|
340
377
|
}));
|
|
@@ -346,7 +383,6 @@ class TranslationService {
|
|
|
346
383
|
styleGuide: options.styleGuide,
|
|
347
384
|
context: options.context,
|
|
348
385
|
lengthControl: options.lengthControl,
|
|
349
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
350
386
|
...(options.qualityChecks?.rules || {}),
|
|
351
387
|
enabled: true,
|
|
352
388
|
});
|
|
@@ -379,6 +415,8 @@ class TranslationService {
|
|
|
379
415
|
if (invalidItems.length > 0) {
|
|
380
416
|
totalIssues += invalidItems.length;
|
|
381
417
|
await uiManager.log(`Found ${invalidItems.length} length issues in ${targetLang}`);
|
|
418
|
+
// Re-translating needs a provider; stop before the first request if none has a key.
|
|
419
|
+
ProviderFactory.assertProviderAvailable(options.apiProvider, options.useFallback !== false);
|
|
382
420
|
// Batch process fixes to improve performance
|
|
383
421
|
const results = await orchestrator.processTranslations(invalidItems);
|
|
384
422
|
const fixedCount = results.filter((r) => r.success).length;
|
|
@@ -402,9 +440,15 @@ class TranslationService {
|
|
|
402
440
|
else {
|
|
403
441
|
await uiManager.log(`\nNo length issues found in any language`);
|
|
404
442
|
}
|
|
443
|
+
await this.reportConfidence([orchestrator]);
|
|
444
|
+
return { issues: totalIssues, fixed: totalFixed };
|
|
405
445
|
}
|
|
406
446
|
catch (error) {
|
|
407
|
-
|
|
447
|
+
// The CLI prints a missing-provider error once; don't log it here as well.
|
|
448
|
+
if (!(error instanceof NoProviderError)) {
|
|
449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
450
|
+
await uiManager.log(`\nValidation error: ${message}`);
|
|
451
|
+
}
|
|
408
452
|
throw error;
|
|
409
453
|
}
|
|
410
454
|
}
|
|
@@ -416,7 +460,8 @@ class TranslationService {
|
|
|
416
460
|
return await FileManager.findLocaleFiles(localesDir, sourceLang);
|
|
417
461
|
}
|
|
418
462
|
catch (error) {
|
|
419
|
-
|
|
463
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
464
|
+
await uiManager.log(`Error finding locale files: ${message}`);
|
|
420
465
|
return [];
|
|
421
466
|
}
|
|
422
467
|
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
import { MetadataTypes } from "vectra";
|
|
1
2
|
export interface VectorStoreConfig {
|
|
2
3
|
enabled: boolean;
|
|
3
4
|
vectorDbPath: string;
|
|
4
5
|
similarityThreshold?: number;
|
|
5
6
|
exactMatchThreshold?: number;
|
|
6
7
|
}
|
|
8
|
+
/** Extra metadata callers attach to a stored translation (e.g. key, category). */
|
|
9
|
+
export type VectorItemMetadata = Record<string, MetadataTypes>;
|
|
10
|
+
/** Metadata persisted with every vector: the translation pair plus caller metadata. */
|
|
11
|
+
export type TranslationMemoryMetadata = VectorItemMetadata & {
|
|
12
|
+
text: string;
|
|
13
|
+
translation: string;
|
|
14
|
+
sourceLang: string;
|
|
15
|
+
targetLang: string;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
};
|
|
7
18
|
export interface SearchResult {
|
|
8
19
|
item: {
|
|
9
20
|
text: string;
|
|
10
21
|
translation: string;
|
|
11
22
|
sourceLang: string;
|
|
12
23
|
targetLang: string;
|
|
13
|
-
metadata?:
|
|
24
|
+
metadata?: TranslationMemoryMetadata;
|
|
14
25
|
};
|
|
15
26
|
score: number;
|
|
16
27
|
}
|
|
@@ -21,6 +32,8 @@ export declare class VectorStore {
|
|
|
21
32
|
private readonly DIMENSION;
|
|
22
33
|
constructor(config: VectorStoreConfig);
|
|
23
34
|
initialize(): Promise<void>;
|
|
24
|
-
addItem(text: string, translation: string, sourceLang: string, targetLang: string, embedding: number[],
|
|
35
|
+
addItem(text: string, translation: string, sourceLang: string, targetLang: string, embedding: number[],
|
|
36
|
+
/** Undefined values are dropped when vectra serializes the index. */
|
|
37
|
+
metadata?: Partial<VectorItemMetadata>): Promise<void>;
|
|
25
38
|
search(embedding: number[], targetLang: string, limit?: number): Promise<SearchResult[]>;
|
|
26
39
|
}
|
|
@@ -27,11 +27,14 @@ export class VectorStore {
|
|
|
27
27
|
// log(`Vector store initialized at ${dbPath}`, true);
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
30
|
-
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
console.error(`Failed to initialize vector store: ${message}`);
|
|
31
32
|
this.config.enabled = false; // Disable if init fails
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
async addItem(text, translation, sourceLang, targetLang, embedding,
|
|
35
|
+
async addItem(text, translation, sourceLang, targetLang, embedding,
|
|
36
|
+
/** Undefined values are dropped when vectra serializes the index. */
|
|
37
|
+
metadata = {}) {
|
|
35
38
|
if (!this.initialized || !this.index)
|
|
36
39
|
return;
|
|
37
40
|
try {
|
|
@@ -48,19 +51,19 @@ export class VectorStore {
|
|
|
48
51
|
});
|
|
49
52
|
}
|
|
50
53
|
catch (error) {
|
|
51
|
-
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
console.warn(`Failed to add item to vector store: ${message}`);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
async search(embedding, targetLang, limit = 3) {
|
|
55
59
|
if (!this.initialized || !this.index)
|
|
56
60
|
return [];
|
|
57
61
|
try {
|
|
58
|
-
//
|
|
59
|
-
const results = await this.index.queryItems(embedding, limit
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.map((result) => ({
|
|
62
|
+
// Filter by target language before ranking so `limit` counts only matching items.
|
|
63
|
+
const results = await this.index.queryItems(embedding, "", limit, {
|
|
64
|
+
targetLang: { $eq: targetLang },
|
|
65
|
+
});
|
|
66
|
+
return results.map((result) => ({
|
|
64
67
|
item: {
|
|
65
68
|
text: result.item.metadata.text,
|
|
66
69
|
translation: result.item.metadata.translation,
|
|
@@ -72,7 +75,8 @@ export class VectorStore {
|
|
|
72
75
|
}));
|
|
73
76
|
}
|
|
74
77
|
catch (error) {
|
|
75
|
-
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
console.warn(`Vector search failed: ${message}`);
|
|
76
80
|
return [];
|
|
77
81
|
}
|
|
78
82
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,58 +1,56 @@
|
|
|
1
|
-
import { OrchestratorOptions } from "../core/orchestrator.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { OrchestratorOptions } from "../core/orchestrator.js";
|
|
2
|
+
import type { FlatTranslationValue } from "../utils/object-transformer.js";
|
|
3
|
+
import type { ProgressStatus } from "../utils/progress-tracker.js";
|
|
4
|
+
import type { QualityIssue } from "../utils/quality/base-checker.js";
|
|
5
|
+
import type { StateOptions } from "../utils/state-manager.js";
|
|
6
|
+
import type { GlobalStats as CollectedStats, LanguageStats as LanguageCounters } from "../utils/statistics-manager.js";
|
|
7
|
+
/** The `syncOptions` configuration section: keeping target files in sync with the source. */
|
|
8
|
+
export interface SyncOptions {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
removeDeletedKeys?: boolean;
|
|
11
|
+
retranslateModified?: boolean;
|
|
12
|
+
stateTracking?: StateOptions;
|
|
13
|
+
}
|
|
14
|
+
/** Options of a translate or fix run: the orchestrator options plus file and sync settings. */
|
|
15
|
+
export interface TranslationOptions extends OrchestratorOptions {
|
|
5
16
|
targets?: string[];
|
|
6
17
|
localesDir?: string;
|
|
7
18
|
debug?: boolean;
|
|
8
19
|
forceUpdate?: boolean;
|
|
9
|
-
syncOptions?:
|
|
10
|
-
enabled?: boolean;
|
|
11
|
-
removeDeletedKeys?: boolean;
|
|
12
|
-
};
|
|
13
|
-
concurrencyLimit?: number;
|
|
14
|
-
saveReviewQueue?: boolean;
|
|
15
|
-
minConfidence?: number;
|
|
20
|
+
syncOptions?: SyncOptions;
|
|
16
21
|
fileExtensions?: string[];
|
|
17
|
-
progressOptions?: any;
|
|
18
|
-
qualityChecks?: any;
|
|
19
|
-
styleGuide?: any;
|
|
20
|
-
context?: ContextConfig;
|
|
21
|
-
lengthControl?: any;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
details: Record<string, {
|
|
31
|
-
totalConfidence: number;
|
|
32
|
-
samples: number;
|
|
33
|
-
}>;
|
|
23
|
+
/** Per-language counters, plus the error message when the language failed as a whole. */
|
|
24
|
+
export interface LanguageStats extends LanguageCounters {
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Statistics of a translate run, completed by the translation service. */
|
|
28
|
+
export interface GlobalStats extends CollectedStats {
|
|
29
|
+
languages: Record<string, LanguageStats>;
|
|
34
30
|
error?: {
|
|
35
31
|
message: string;
|
|
36
32
|
time: string;
|
|
37
33
|
stack?: string;
|
|
38
34
|
};
|
|
39
|
-
orchestrators: any[];
|
|
40
35
|
endTime?: string;
|
|
41
36
|
totalDuration?: number;
|
|
42
|
-
totalTime: number;
|
|
43
|
-
startTime: string;
|
|
44
37
|
}
|
|
38
|
+
/** A source key that needs translating (or re-translating) into one target language. */
|
|
45
39
|
export interface MissingKey {
|
|
46
40
|
key: string;
|
|
47
41
|
text: string;
|
|
48
42
|
targetLang: string;
|
|
49
|
-
|
|
43
|
+
/** Current value of the key in the target file, if any. */
|
|
44
|
+
existingTranslation?: FlatTranslationValue;
|
|
50
45
|
isModified: boolean;
|
|
51
46
|
isNew: boolean;
|
|
52
|
-
|
|
47
|
+
/** The quality issue that requires the existing translation to be redone. */
|
|
48
|
+
issueDetails?: QualityIssue;
|
|
53
49
|
}
|
|
50
|
+
/** Outcome of translating one target language. */
|
|
54
51
|
export interface LanguageProcessResult {
|
|
55
|
-
|
|
52
|
+
/** Final progress counters; null when the language failed. */
|
|
53
|
+
status: Pick<ProgressStatus, "completed" | "total" | "language"> | null;
|
|
56
54
|
savedMessage?: string;
|
|
57
55
|
error?: string;
|
|
58
56
|
}
|
|
@@ -3,16 +3,22 @@ export interface AnalysisResult {
|
|
|
3
3
|
confidence: number;
|
|
4
4
|
keywords: string[];
|
|
5
5
|
explanation: string;
|
|
6
|
-
prompt
|
|
6
|
+
/** Category prompt; undefined when neither the category nor the fallback defines one */
|
|
7
|
+
prompt?: string;
|
|
7
8
|
method?: string;
|
|
8
9
|
}
|
|
10
|
+
export interface ContextCategory {
|
|
11
|
+
keywords?: string[];
|
|
12
|
+
prompt?: string;
|
|
13
|
+
weight?: number;
|
|
14
|
+
}
|
|
9
15
|
export interface ContextConfig {
|
|
10
16
|
enabled?: boolean;
|
|
11
17
|
useAI?: boolean;
|
|
12
18
|
minTextLength?: number;
|
|
13
19
|
cacheSize?: number;
|
|
14
20
|
cacheTTL?: number;
|
|
15
|
-
categories?: Record<string,
|
|
21
|
+
categories?: Record<string, ContextCategory>;
|
|
16
22
|
detection?: {
|
|
17
23
|
threshold?: number;
|
|
18
24
|
minConfidence?: number;
|
|
@@ -23,11 +29,14 @@ export interface ContextConfig {
|
|
|
23
29
|
prompt?: string;
|
|
24
30
|
};
|
|
25
31
|
analysisOptions?: {
|
|
32
|
+
model?: string;
|
|
33
|
+
temperature?: number;
|
|
26
34
|
maxTokens?: number;
|
|
27
35
|
timeout?: number;
|
|
28
36
|
retries?: number;
|
|
29
37
|
};
|
|
30
|
-
|
|
38
|
+
/** Provider fallback chain used for analysis calls */
|
|
39
|
+
fallbackOrder?: string[];
|
|
31
40
|
}
|
|
32
41
|
export interface Stats {
|
|
33
42
|
totalAnalyzed: number;
|
|
@@ -90,7 +90,8 @@ class AIContextAnalyzer {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
catch (error) {
|
|
93
|
-
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
console.error("Batch AI analysis failed:", message);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
return results;
|
|
@@ -106,18 +107,23 @@ class AIContextAnalyzer {
|
|
|
106
107
|
return [];
|
|
107
108
|
this.stats.totalAnalyzed += texts.length;
|
|
108
109
|
this.stats.aiCalls++; // Only one API call for the batch
|
|
109
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
110
|
const provider = ProviderFactory.getProvider(apiProvider, true, this.config);
|
|
111
111
|
if (!provider) {
|
|
112
112
|
throw new Error("AI provider not available for context analysis");
|
|
113
113
|
}
|
|
114
114
|
const batchPrompt = this._createBatchAnalysisPrompt(texts);
|
|
115
|
-
const result = await rateLimiter.enqueue(apiProvider.toLowerCase(), () =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
const result = await rateLimiter.enqueue(apiProvider.toLowerCase(), () => {
|
|
116
|
+
if (!provider.analyze) {
|
|
117
|
+
throw new TypeError("provider.analyze is not a function");
|
|
118
|
+
}
|
|
119
|
+
const analysisOptions = {
|
|
120
|
+
...this.config.analysisOptions,
|
|
121
|
+
maxTokens: Math.min(4000, this.config.analysisOptions?.maxTokens || 2000),
|
|
122
|
+
timeout: this.config.analysisOptions?.timeout || 10000,
|
|
123
|
+
retries: this.config.analysisOptions?.retries || 2,
|
|
124
|
+
};
|
|
125
|
+
return provider.analyze(batchPrompt, analysisOptions);
|
|
126
|
+
});
|
|
121
127
|
return this._parseBatchAnalysisResult(result, texts.length);
|
|
122
128
|
}
|
|
123
129
|
// Create batch analysis prompt for multiple texts
|
|
@@ -252,7 +258,8 @@ RESPONSE FORMAT:
|
|
|
252
258
|
return results;
|
|
253
259
|
}
|
|
254
260
|
catch (error) {
|
|
255
|
-
|
|
261
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
262
|
+
console.error("Error parsing batch AI analysis result:", message);
|
|
256
263
|
return new Array(expectedCount).fill(null);
|
|
257
264
|
}
|
|
258
265
|
}
|
|
@@ -295,7 +302,8 @@ RESPONSE FORMAT:
|
|
|
295
302
|
};
|
|
296
303
|
}
|
|
297
304
|
catch (error) {
|
|
298
|
-
|
|
305
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
306
|
+
console.error("Error parsing AI analysis result:", message);
|
|
299
307
|
return null;
|
|
300
308
|
}
|
|
301
309
|
}
|