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
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import rateLimiter from "../utils/rate-limiter.js";
|
|
2
|
+
import pLimit from "p-limit";
|
|
3
|
+
import ProgressTracker from "../utils/progress-tracker.js";
|
|
4
|
+
import QualityChecker from "../utils/quality/index.js";
|
|
5
|
+
import ContextProcessor from "./context-processor.js";
|
|
6
|
+
import { LRUCache } from "lru-cache";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import gracefulShutdown from "../utils/graceful-shutdown.js";
|
|
9
|
+
import GlossaryManager from "../utils/glossary-manager.js";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { log } from "../utils/logger.js";
|
|
13
|
+
// Pipeline imports
|
|
14
|
+
import Pipeline from "./pipeline/Pipeline.js";
|
|
15
|
+
import { createTranslationContext, } from "./pipeline/context.js";
|
|
16
|
+
import InputValidationStep from "./pipeline/steps/InputValidationStep.js";
|
|
17
|
+
import CacheReadStep from "./pipeline/steps/CacheReadStep.js";
|
|
18
|
+
import GlossaryPreStep from "./pipeline/steps/GlossaryPreStep.js";
|
|
19
|
+
import TranslationStep from "./pipeline/steps/TranslationStep.js";
|
|
20
|
+
import GlossaryPostStep from "./pipeline/steps/GlossaryPostStep.js";
|
|
21
|
+
import QualityCheckStep from "./pipeline/steps/QualityCheckStep.js";
|
|
22
|
+
import ToneCheckStep from "./pipeline/steps/ToneCheckStep.js";
|
|
23
|
+
import ConfidenceCheckStep from "./pipeline/steps/ConfidenceCheckStep.js";
|
|
24
|
+
import CacheWriteStep from "./pipeline/steps/CacheWriteStep.js";
|
|
25
|
+
// Vector Memory Imports
|
|
26
|
+
import { VectorStore } from "../services/vector-store.js";
|
|
27
|
+
import { EmbeddingProvider } from "../services/embedding-provider.js";
|
|
28
|
+
import { VectorCacheReadStep } from "./pipeline/steps/VectorCacheReadStep.js";
|
|
29
|
+
import { VectorCacheWriteStep } from "./pipeline/steps/VectorCacheWriteStep.js";
|
|
30
|
+
class Orchestrator {
|
|
31
|
+
options;
|
|
32
|
+
contextProcessor;
|
|
33
|
+
progress;
|
|
34
|
+
qualityChecker;
|
|
35
|
+
advanced;
|
|
36
|
+
glossaryManager;
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
translationCache;
|
|
39
|
+
cacheStats;
|
|
40
|
+
// Vector Memory
|
|
41
|
+
vectorStore;
|
|
42
|
+
embeddingProvider;
|
|
43
|
+
vectorStats;
|
|
44
|
+
confidenceSettings;
|
|
45
|
+
concurrencyLimit;
|
|
46
|
+
pipeline;
|
|
47
|
+
_shutdownCallback;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.contextProcessor = new ContextProcessor(options.context || {});
|
|
51
|
+
// Allow progress tracker to be configured via options
|
|
52
|
+
this.progress = new ProgressTracker(options.progressOptions || {});
|
|
53
|
+
this.qualityChecker = new QualityChecker({
|
|
54
|
+
styleGuide: options.styleGuide,
|
|
55
|
+
context: options.context,
|
|
56
|
+
lengthControl: options.lengthControl,
|
|
57
|
+
...(options.qualityChecks?.rules || {}),
|
|
58
|
+
enabled: options.qualityChecks?.enabled,
|
|
59
|
+
});
|
|
60
|
+
this.advanced = {
|
|
61
|
+
timeoutMs: options.advanced?.timeoutMs || 30000,
|
|
62
|
+
maxKeyLength: options.advanced?.maxKeyLength || 10000,
|
|
63
|
+
maxBatchSize: options.advanced?.maxBatchSize || 50,
|
|
64
|
+
autoOptimize: options.advanced?.autoOptimize !== false,
|
|
65
|
+
debug: options.advanced?.debug || process.env.DEBUG === "true" || false,
|
|
66
|
+
};
|
|
67
|
+
// Initialize glossary manager
|
|
68
|
+
this.glossaryManager = new GlossaryManager(options.glossary || {});
|
|
69
|
+
if (this.glossaryManager.enabled && this.advanced.debug) {
|
|
70
|
+
const stats = this.glossaryManager.getStats();
|
|
71
|
+
log(`Glossary enabled: ${stats.totalTerms} terms loaded`, true);
|
|
72
|
+
}
|
|
73
|
+
if (options.rateLimiter) {
|
|
74
|
+
rateLimiter.updateConfig({
|
|
75
|
+
queueStrategy: options.rateLimiter.queueStrategy,
|
|
76
|
+
queueTimeout: options.rateLimiter.queueTimeout,
|
|
77
|
+
adaptiveThrottling: options.rateLimiter.adaptiveThrottling,
|
|
78
|
+
providerLimits: options.rateLimiter.providerLimits,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Cache Initialization
|
|
82
|
+
this.translationCache = new LRUCache({
|
|
83
|
+
max: options.cacheSize || 1000,
|
|
84
|
+
ttl: options.cacheTTL || 1000 * 60 * 60 * 24,
|
|
85
|
+
updateAgeOnGet: options.updateAgeOnGet !== false,
|
|
86
|
+
allowStale: options.allowStaleCache !== false,
|
|
87
|
+
// Simplified fetchMethod for stale-while-revalidate pattern
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
fetchMethod: async (key, staleValue, { context }) => {
|
|
90
|
+
if (staleValue && options.staleWhileRevalidate !== false) {
|
|
91
|
+
// Background refresh using a mini-pipeline or direct logic
|
|
92
|
+
this._refreshCacheEntry(key, context).catch((err) => {
|
|
93
|
+
console.warn(`Cache refresh failed for key ${key}: ${err.message}`);
|
|
94
|
+
});
|
|
95
|
+
return staleValue;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
this.cacheStats = {
|
|
101
|
+
hits: 0,
|
|
102
|
+
misses: 0,
|
|
103
|
+
staleHits: 0,
|
|
104
|
+
stored: 0,
|
|
105
|
+
refreshes: 0,
|
|
106
|
+
};
|
|
107
|
+
// Confidence scoring settings
|
|
108
|
+
this.confidenceSettings = {
|
|
109
|
+
enabled: options.minConfidence !== undefined ||
|
|
110
|
+
options.saveReviewQueue ||
|
|
111
|
+
options.confidenceScoring?.enabled ||
|
|
112
|
+
false,
|
|
113
|
+
autoApproveThreshold: options.confidenceScoring?.autoApproveThreshold || 0.9,
|
|
114
|
+
reviewThreshold: options.confidenceScoring?.reviewThreshold || 0.7,
|
|
115
|
+
rejectThreshold: options.confidenceScoring?.rejectThreshold || 0.5,
|
|
116
|
+
saveReviewQueue: options.saveReviewQueue || options.confidenceScoring?.saveReviewQueue || false,
|
|
117
|
+
reviewQueue: [],
|
|
118
|
+
autoApprovedCount: 0,
|
|
119
|
+
rejectedCount: 0,
|
|
120
|
+
};
|
|
121
|
+
// Use minConfidence from options if present as specific threshold logic override or mapping
|
|
122
|
+
if (options.minConfidence) {
|
|
123
|
+
// This logic was slightly fuzzy in JS, preserved here:
|
|
124
|
+
// If minConfidence is set, it might override others or act as a floor.
|
|
125
|
+
// The original code used it.
|
|
126
|
+
}
|
|
127
|
+
this.concurrencyLimit = options.concurrencyLimit || 5;
|
|
128
|
+
if (this.advanced.autoOptimize) {
|
|
129
|
+
this._applyAutoOptimizations();
|
|
130
|
+
}
|
|
131
|
+
// Vector Memory Initialization
|
|
132
|
+
this.vectorStore = new VectorStore(options.vectorMemory || {
|
|
133
|
+
enabled: false,
|
|
134
|
+
vectorDbPath: "./.localize-cache/vector-memory",
|
|
135
|
+
});
|
|
136
|
+
this.embeddingProvider = new EmbeddingProvider({
|
|
137
|
+
provider: options.vectorMemory?.embeddingProvider,
|
|
138
|
+
model: options.vectorMemory?.embeddingModel,
|
|
139
|
+
});
|
|
140
|
+
this.vectorStats = {
|
|
141
|
+
hits: 0,
|
|
142
|
+
contextUsed: 0,
|
|
143
|
+
misses: 0,
|
|
144
|
+
};
|
|
145
|
+
// Initialize vector store (non-blocking)
|
|
146
|
+
if (options.vectorMemory?.enabled) {
|
|
147
|
+
this.vectorStore.initialize().catch((err) => {
|
|
148
|
+
if (this.advanced.debug) {
|
|
149
|
+
console.warn("Vector store initialization failed:", err.message);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Initialize Pipeline
|
|
154
|
+
this.pipeline = new Pipeline();
|
|
155
|
+
this._buildPipeline();
|
|
156
|
+
this._shutdownCallback = async () => {
|
|
157
|
+
if (this.translationCache && this.translationCache.size > 0) {
|
|
158
|
+
console.log(`Flushing ${this.translationCache.size} cache entries...`);
|
|
159
|
+
this.translationCache.clear();
|
|
160
|
+
}
|
|
161
|
+
this.resetCacheStats();
|
|
162
|
+
};
|
|
163
|
+
gracefulShutdown.registerCallback(this._shutdownCallback);
|
|
164
|
+
if (this.advanced.debug) {
|
|
165
|
+
log("Orchestrator initialized with options:", true);
|
|
166
|
+
// ...
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
_buildPipeline() {
|
|
170
|
+
// 1. Validation
|
|
171
|
+
this.pipeline.use(new InputValidationStep(this.advanced.maxKeyLength));
|
|
172
|
+
// 2. Cache Read
|
|
173
|
+
if (this.translationCache) {
|
|
174
|
+
this.pipeline.use(new CacheReadStep(this.translationCache, this.cacheStats, {
|
|
175
|
+
enabled: this.options.cacheEnabled,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
// 2.5 Vector Cache Read
|
|
179
|
+
if (this.options.vectorMemory?.enabled) {
|
|
180
|
+
this.pipeline.use(new VectorCacheReadStep(this.vectorStore, this.embeddingProvider, this.vectorStats, this.options.vectorMemory));
|
|
181
|
+
}
|
|
182
|
+
// 3. Glossary Pre-processing
|
|
183
|
+
this.pipeline.use(new GlossaryPreStep(this.glossaryManager));
|
|
184
|
+
// 4. Translation
|
|
185
|
+
this.pipeline.use(new TranslationStep(this.options, this.confidenceSettings));
|
|
186
|
+
// 4.5 Vector Cache Write (save new translations)
|
|
187
|
+
if (this.options.vectorMemory?.enabled) {
|
|
188
|
+
this.pipeline.use(new VectorCacheWriteStep(this.vectorStore, this.embeddingProvider, this.options.vectorMemory));
|
|
189
|
+
}
|
|
190
|
+
// 5. Glossary Post-processing
|
|
191
|
+
this.pipeline.use(new GlossaryPostStep(this.glossaryManager));
|
|
192
|
+
// 6. Quality Check & JSON Validation
|
|
193
|
+
this.pipeline.use(new QualityCheckStep(this.qualityChecker, this.advanced.debug));
|
|
194
|
+
// 6.5. Tone Verification (New)
|
|
195
|
+
this.pipeline.use(new ToneCheckStep({
|
|
196
|
+
enabled: this.options.styleGuide?.enforceTone,
|
|
197
|
+
debug: this.advanced.debug,
|
|
198
|
+
}));
|
|
199
|
+
// 7. Confidence Score Calculationk
|
|
200
|
+
this.pipeline.use(new ConfidenceCheckStep(this.confidenceSettings, this.advanced.debug));
|
|
201
|
+
// 8. Cache Write
|
|
202
|
+
if (this.translationCache) {
|
|
203
|
+
this.pipeline.use(new CacheWriteStep(this.translationCache, this.cacheStats, {
|
|
204
|
+
enabled: this.options.cacheEnabled,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async processTranslation(key, text, targetLang,
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
contextData,
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
existingTranslation) {
|
|
213
|
+
const context = createTranslationContext(key, text, this.options.source, targetLang, this.options, contextData, existingTranslation);
|
|
214
|
+
await this.pipeline.execute(context);
|
|
215
|
+
return context.result;
|
|
216
|
+
}
|
|
217
|
+
async processTranslations(items) {
|
|
218
|
+
this.progress.start(items.length, items[0].targetLang);
|
|
219
|
+
const limit = pLimit(this.concurrencyLimit);
|
|
220
|
+
const contextBatchSize = this.advanced.maxBatchSize;
|
|
221
|
+
const chunks = this._chunkArray(items, contextBatchSize);
|
|
222
|
+
if (this.advanced.debug) {
|
|
223
|
+
log(`Processing ${items.length} items with concurrency limit ${this.concurrencyLimit}`, true);
|
|
224
|
+
}
|
|
225
|
+
console.log(""); // Empty line for progress bar
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
227
|
+
const allPromises = [];
|
|
228
|
+
for (const chunk of chunks) {
|
|
229
|
+
const texts = chunk.map((item) => item.text);
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
231
|
+
let contextResults = [];
|
|
232
|
+
try {
|
|
233
|
+
contextResults = await this.contextProcessor.analyzeBatch(texts);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
console.warn(`Context analysis failed for batch: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
239
|
+
const item = chunk[i];
|
|
240
|
+
const contextData = contextResults[i] || (await this.contextProcessor.analyze(item.text));
|
|
241
|
+
const promise = limit(async () => {
|
|
242
|
+
try {
|
|
243
|
+
const result = await this.processTranslation(item.key, item.text, item.targetLang, contextData, item.existingTranslation);
|
|
244
|
+
this.progress.increment(result.success ? "success" : "failed");
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
this.progress.increment("failed");
|
|
249
|
+
return {
|
|
250
|
+
key: item.key,
|
|
251
|
+
translated: item.text,
|
|
252
|
+
error: error.message,
|
|
253
|
+
success: false,
|
|
254
|
+
context: {},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
allPromises.push(promise);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const processedResults = await Promise.all(allPromises);
|
|
262
|
+
return processedResults;
|
|
263
|
+
}
|
|
264
|
+
// Helper methods
|
|
265
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
266
|
+
_chunkArray(array, chunkSize) {
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
268
|
+
const chunks = [];
|
|
269
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
270
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
271
|
+
}
|
|
272
|
+
return chunks;
|
|
273
|
+
}
|
|
274
|
+
_applyAutoOptimizations() {
|
|
275
|
+
try {
|
|
276
|
+
const cpuCount = os.cpus().length;
|
|
277
|
+
const totalMemory = os.totalmem();
|
|
278
|
+
const memoryGB = Math.floor(totalMemory / (1024 * 1024 * 1024));
|
|
279
|
+
if (memoryGB >= 8 && cpuCount >= 4) {
|
|
280
|
+
this.concurrencyLimit = Math.min(20, cpuCount * 2);
|
|
281
|
+
}
|
|
282
|
+
else if (memoryGB >= 4 && cpuCount >= 2) {
|
|
283
|
+
this.concurrencyLimit = Math.min(10, cpuCount * 2);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
this.concurrencyLimit = 4;
|
|
287
|
+
}
|
|
288
|
+
if (this.advanced.debug) {
|
|
289
|
+
log(`Auto-optimized settings - CPU: ${cpuCount}, Memory: ${memoryGB}GB, Concurrency: ${this.concurrencyLimit}`, true);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
console.warn("Failed to auto-optimize settings:", error.message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
clearCache() {
|
|
297
|
+
if (this.translationCache) {
|
|
298
|
+
this.translationCache.clear();
|
|
299
|
+
}
|
|
300
|
+
this.resetCacheStats();
|
|
301
|
+
}
|
|
302
|
+
resetCacheStats() {
|
|
303
|
+
this.cacheStats = {
|
|
304
|
+
hits: 0,
|
|
305
|
+
misses: 0,
|
|
306
|
+
staleHits: 0,
|
|
307
|
+
stored: 0,
|
|
308
|
+
refreshes: 0,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
312
|
+
getCacheStats() {
|
|
313
|
+
if (!this.translationCache)
|
|
314
|
+
return {};
|
|
315
|
+
return {
|
|
316
|
+
...this.cacheStats,
|
|
317
|
+
size: this.translationCache.size,
|
|
318
|
+
capacity: this.translationCache.max,
|
|
319
|
+
hitRate: this.cacheStats.hits / (this.cacheStats.hits + this.cacheStats.misses) || 0,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
// Used by background revalidation.
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
|
+
async _refreshCacheEntry(key, context) {
|
|
325
|
+
if (!context || !context.text || !context.targetLang) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
// Use the full pipeline for refresh, but skip cache read to force re-translation
|
|
330
|
+
// pass specific options merged with global options
|
|
331
|
+
const refreshOptions = {
|
|
332
|
+
...this.options,
|
|
333
|
+
skipCache: true,
|
|
334
|
+
staleWhileRevalidate: false, // Prevent infinite recursion matching this condition
|
|
335
|
+
};
|
|
336
|
+
const ctx = createTranslationContext(key, context.text, this.options.source, context.targetLang, refreshOptions, context.contextData);
|
|
337
|
+
await this.pipeline.execute(ctx);
|
|
338
|
+
// Stats are handled within steps (TranslationStep adds confidence, CacheWriteStep updates cache & stats)
|
|
339
|
+
this.cacheStats.refreshes++;
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
console.warn(`Failed to refresh cache entry: ${error.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
346
|
+
getStatus() {
|
|
347
|
+
return {
|
|
348
|
+
cache: this.getCacheStats(),
|
|
349
|
+
rateLimiter: rateLimiter.getStatus(),
|
|
350
|
+
concurrency: this.concurrencyLimit,
|
|
351
|
+
advanced: this.advanced,
|
|
352
|
+
confidence: this.confidenceSettings.enabled
|
|
353
|
+
? {
|
|
354
|
+
minThreshold: this.options.minConfidence,
|
|
355
|
+
autoApproveThreshold: this.confidenceSettings.autoApproveThreshold,
|
|
356
|
+
reviewThreshold: this.confidenceSettings.reviewThreshold,
|
|
357
|
+
rejectThreshold: this.confidenceSettings.rejectThreshold,
|
|
358
|
+
reviewQueueSize: this.confidenceSettings.reviewQueue.length,
|
|
359
|
+
autoApprovedCount: this.confidenceSettings.autoApprovedCount,
|
|
360
|
+
rejectedCount: this.confidenceSettings.rejectedCount,
|
|
361
|
+
}
|
|
362
|
+
: null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
saveReviewQueue() {
|
|
366
|
+
if (!this.confidenceSettings.saveReviewQueue ||
|
|
367
|
+
this.confidenceSettings.reviewQueue.length === 0) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const cacheDir = path.join(process.cwd(), ".localize-cache");
|
|
372
|
+
if (!fs.existsSync(cacheDir)) {
|
|
373
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
374
|
+
}
|
|
375
|
+
const reviewFile = path.join(cacheDir, "review-queue.json");
|
|
376
|
+
const data = {
|
|
377
|
+
timestamp: new Date().toISOString(),
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
|
+
minConfidence: this.options.minConfidence,
|
|
380
|
+
items: this.confidenceSettings.reviewQueue,
|
|
381
|
+
stats: {
|
|
382
|
+
total: this.confidenceSettings.reviewQueue.length,
|
|
383
|
+
byLanguage: this._groupByLanguage(this.confidenceSettings.reviewQueue),
|
|
384
|
+
byConfidenceLevel: this._groupByConfidenceLevel(this.confidenceSettings.reviewQueue),
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
fs.writeFileSync(reviewFile, JSON.stringify(data, null, 2));
|
|
388
|
+
console.log(`\nReview queue saved: ${this.confidenceSettings.reviewQueue.length} items need review`);
|
|
389
|
+
console.log(` File: ${reviewFile}`);
|
|
390
|
+
console.log(` Run 'localize review' to start interactive review\n`);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.warn(`Warning: Failed to save review queue: ${error.message}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
397
|
+
_groupByLanguage(items) {
|
|
398
|
+
const grouped = {};
|
|
399
|
+
items.forEach((item) => {
|
|
400
|
+
if (!grouped[item.language]) {
|
|
401
|
+
grouped[item.language] = 0;
|
|
402
|
+
}
|
|
403
|
+
grouped[item.language]++;
|
|
404
|
+
});
|
|
405
|
+
return grouped;
|
|
406
|
+
}
|
|
407
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
408
|
+
_groupByConfidenceLevel(items) {
|
|
409
|
+
const grouped = { high: 0, medium: 0, low: 0, very_low: 0 };
|
|
410
|
+
items.forEach((item) => {
|
|
411
|
+
if (grouped[item.confidence.level] !== undefined) {
|
|
412
|
+
grouped[item.confidence.level]++;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return grouped;
|
|
416
|
+
}
|
|
417
|
+
destroy() {
|
|
418
|
+
if (this.translationCache) {
|
|
419
|
+
this.translationCache.clear();
|
|
420
|
+
}
|
|
421
|
+
this.resetCacheStats();
|
|
422
|
+
if (this._shutdownCallback) {
|
|
423
|
+
gracefulShutdown.unregisterCallback(this._shutdownCallback);
|
|
424
|
+
this._shutdownCallback = null;
|
|
425
|
+
}
|
|
426
|
+
this.translationCache = null;
|
|
427
|
+
// @ts-expect-error allowing null for cleanup
|
|
428
|
+
this.contextProcessor = null;
|
|
429
|
+
// @ts-expect-error allowing null for cleanup
|
|
430
|
+
this.progress = null;
|
|
431
|
+
this.qualityChecker = null;
|
|
432
|
+
// @ts-expect-error allowing null for cleanup
|
|
433
|
+
this.glossaryManager = null;
|
|
434
|
+
// @ts-expect-error allowing null for cleanup
|
|
435
|
+
this.pipeline = null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
export default Orchestrator;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Pipeline class implementing the middleware pattern.
|
|
3
|
+
* Allows composing a sequence of steps that can process a context object.
|
|
4
|
+
*/
|
|
5
|
+
class Pipeline {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
steps = []; // allow loose typing initially for step inputs, but ideally PipelineStep
|
|
8
|
+
constructor() {
|
|
9
|
+
this.steps = [];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Add a step to the pipeline.
|
|
13
|
+
* @param step - A step instance with an execute(context, next) method.
|
|
14
|
+
* @returns - Returns self for chaining.
|
|
15
|
+
*/
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
use(step) {
|
|
18
|
+
if (!step || typeof step.execute !== "function") {
|
|
19
|
+
throw new Error("Pipeline step must implement an execute(context, next) method.");
|
|
20
|
+
}
|
|
21
|
+
this.steps.push(step);
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Execute the pipeline with the given context.
|
|
26
|
+
* @param context - The context object to be processed.
|
|
27
|
+
* @returns - The processed context.
|
|
28
|
+
*/
|
|
29
|
+
async execute(context) {
|
|
30
|
+
if (!context) {
|
|
31
|
+
throw new Error("Pipeline execution requires a context object.");
|
|
32
|
+
}
|
|
33
|
+
const runner = async (index) => {
|
|
34
|
+
if (index >= this.steps.length) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const step = this.steps[index];
|
|
38
|
+
// Allow steps to call next() to proceed to the next step
|
|
39
|
+
const next = async () => {
|
|
40
|
+
await runner(index + 1);
|
|
41
|
+
};
|
|
42
|
+
await step.execute(context, next);
|
|
43
|
+
};
|
|
44
|
+
await runner(0);
|
|
45
|
+
return context;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export default Pipeline;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for pipeline steps.
|
|
3
|
+
* All steps should extend this or implement the execute method.
|
|
4
|
+
*/
|
|
5
|
+
class PipelineStep {
|
|
6
|
+
/**
|
|
7
|
+
* Name of the step for debugging/logging purposes.
|
|
8
|
+
*/
|
|
9
|
+
get name() {
|
|
10
|
+
return this.constructor.name;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export default PipelineStep;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const createTranslationContext = (key, sourceText, sourceLang, targetLang,
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
|
+
options,
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
meta = {},
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
existingTranslation = null) => ({
|
|
8
|
+
key,
|
|
9
|
+
sourceText,
|
|
10
|
+
sourceLang,
|
|
11
|
+
targetLang,
|
|
12
|
+
options,
|
|
13
|
+
meta,
|
|
14
|
+
existingTranslation,
|
|
15
|
+
result: {
|
|
16
|
+
key,
|
|
17
|
+
translated: null,
|
|
18
|
+
success: false,
|
|
19
|
+
context: meta,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import PipelineStep from "../PipelineStep.js";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
class CacheReadStep extends PipelineStep {
|
|
4
|
+
cache;
|
|
5
|
+
stats;
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
options;
|
|
8
|
+
constructor(cache, cacheStats, options = {}) {
|
|
9
|
+
super();
|
|
10
|
+
this.cache = cache;
|
|
11
|
+
this.stats = cacheStats;
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
async execute(context, next) {
|
|
15
|
+
if (this.options.enabled === false || context.options?.skipCache) {
|
|
16
|
+
await next();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const cacheKey = this._generateCacheKey(context.sourceText, context.targetLang, context.meta?.category);
|
|
20
|
+
if (this.cache && this.cache.has(cacheKey)) {
|
|
21
|
+
const cachedResult = this.cache.get(cacheKey);
|
|
22
|
+
if (cachedResult && typeof cachedResult === "object") {
|
|
23
|
+
this.stats.hits++;
|
|
24
|
+
// Check for stale hit
|
|
25
|
+
try {
|
|
26
|
+
const ttl = this.cache.getRemainingTTL?.(cacheKey);
|
|
27
|
+
if (typeof ttl === "number" && ttl <= 0) {
|
|
28
|
+
this.stats.staleHits++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (_error) {
|
|
32
|
+
// Ignore cache errors
|
|
33
|
+
}
|
|
34
|
+
// Hydrate context result from cache
|
|
35
|
+
context.result = {
|
|
36
|
+
...cachedResult,
|
|
37
|
+
key: context.key,
|
|
38
|
+
fromCache: true,
|
|
39
|
+
};
|
|
40
|
+
// Short-circuit: Do NOT call next()
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.cache.delete(cacheKey); // Invalid entry
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.stats.misses++;
|
|
48
|
+
// Store cache key in context for Write step to use
|
|
49
|
+
context.cacheKey = cacheKey;
|
|
50
|
+
await next();
|
|
51
|
+
}
|
|
52
|
+
_generateCacheKey(text, targetLang, category = "unknown") {
|
|
53
|
+
let keyContent;
|
|
54
|
+
if (text.length <= 100) {
|
|
55
|
+
keyContent = `${targetLang}:${category}:${text.toLowerCase()}`;
|
|
56
|
+
}
|
|
57
|
+
else if (text.length <= 500) {
|
|
58
|
+
const start = text.substring(0, 40).toLowerCase();
|
|
59
|
+
const middlePos = Math.floor(text.length / 2);
|
|
60
|
+
const middle = text.substring(middlePos - 20, middlePos + 20).toLowerCase();
|
|
61
|
+
const end = text.substring(text.length - 40).toLowerCase();
|
|
62
|
+
keyContent = `${targetLang}:${category}:${start}|MID:${middle}|${end}`;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const quarter = Math.floor(text.length / 4);
|
|
66
|
+
const start = text.substring(0, 30).toLowerCase();
|
|
67
|
+
const q1 = text.substring(quarter, quarter + 25).toLowerCase();
|
|
68
|
+
const q2 = text.substring(quarter * 2, quarter * 2 + 25).toLowerCase();
|
|
69
|
+
const q3 = text.substring(quarter * 3, quarter * 3 + 25).toLowerCase();
|
|
70
|
+
const end = text.substring(text.length - 30).toLowerCase();
|
|
71
|
+
keyContent = `${targetLang}:${category}:LEN:${text.length}|${start}|Q1:${q1}|Q2:${q2}|Q3:${q3}|${end}`;
|
|
72
|
+
}
|
|
73
|
+
return crypto
|
|
74
|
+
.createHash("sha256")
|
|
75
|
+
.update(keyContent, "utf8")
|
|
76
|
+
.digest("hex")
|
|
77
|
+
.substring(0, 32);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export default CacheReadStep;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import PipelineStep from "../PipelineStep.js";
|
|
2
|
+
class CacheWriteStep extends PipelineStep {
|
|
3
|
+
cache;
|
|
4
|
+
stats;
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
options;
|
|
7
|
+
constructor(cache, cacheStats, options = {}) {
|
|
8
|
+
super();
|
|
9
|
+
this.cache = cache;
|
|
10
|
+
this.stats = cacheStats;
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
async execute(context, next) {
|
|
14
|
+
// Only cache if successful and not from cache
|
|
15
|
+
if (this.options.enabled !== false &&
|
|
16
|
+
context.result &&
|
|
17
|
+
context.result.success &&
|
|
18
|
+
!context.result.fromCache &&
|
|
19
|
+
context.cacheKey) {
|
|
20
|
+
this.cache.set(context.cacheKey, context.result, {
|
|
21
|
+
context: {
|
|
22
|
+
text: context.sourceText,
|
|
23
|
+
targetLang: context.targetLang,
|
|
24
|
+
contextData: context.meta,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
this.stats.stored++;
|
|
28
|
+
}
|
|
29
|
+
await next();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export default CacheWriteStep;
|