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.
Files changed (72) hide show
  1. package/README.md +928 -0
  2. package/dist/cli.js +718 -0
  3. package/dist/commands/review.js +357 -0
  4. package/dist/commands/translator.js +29 -0
  5. package/dist/config/index.js +52 -0
  6. package/dist/core/adapters/base-adapter.js +1 -0
  7. package/dist/core/adapters/factory.js +44 -0
  8. package/dist/core/adapters/json-adapter.js +20 -0
  9. package/dist/core/adapters/po-adapter.js +50 -0
  10. package/dist/core/adapters/properties-adapter.js +31 -0
  11. package/dist/core/adapters/yaml-adapter.js +24 -0
  12. package/dist/core/context-processor.js +247 -0
  13. package/dist/core/detection/framework-detector.js +56 -0
  14. package/dist/core/detection/structure-detector.js +30 -0
  15. package/dist/core/fallback-provider.js +442 -0
  16. package/dist/core/orchestrator.js +438 -0
  17. package/dist/core/pipeline/Pipeline.js +48 -0
  18. package/dist/core/pipeline/PipelineStep.js +13 -0
  19. package/dist/core/pipeline/context.js +21 -0
  20. package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
  21. package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
  22. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
  23. package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
  24. package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
  25. package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
  26. package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
  27. package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
  28. package/dist/core/pipeline/steps/TranslationStep.js +65 -0
  29. package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
  30. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
  31. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
  32. package/dist/core/provider-factory.js +139 -0
  33. package/dist/providers/base-provider.js +230 -0
  34. package/dist/providers/dashscope.js +128 -0
  35. package/dist/providers/deepseek.js +111 -0
  36. package/dist/providers/gemini.js +135 -0
  37. package/dist/providers/openai.js +203 -0
  38. package/dist/providers/xai.js +111 -0
  39. package/dist/services/embedding-provider.js +50 -0
  40. package/dist/services/translation-service.js +609 -0
  41. package/dist/services/vector-store.js +79 -0
  42. package/dist/utils/ai-context-analyzer.js +460 -0
  43. package/dist/utils/confidence-scorer.js +277 -0
  44. package/dist/utils/error-helper.js +386 -0
  45. package/dist/utils/file-manager.js +357 -0
  46. package/dist/utils/glossary-manager.js +194 -0
  47. package/dist/utils/graceful-shutdown.js +118 -0
  48. package/dist/utils/input-validator.js +663 -0
  49. package/dist/utils/logger.js +374 -0
  50. package/dist/utils/object-transformer.js +177 -0
  51. package/dist/utils/progress-tracker.js +277 -0
  52. package/dist/utils/prompt-templates.js +415 -0
  53. package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
  54. package/dist/utils/quality/base-checker.js +73 -0
  55. package/dist/utils/quality/code-block-checker.js +214 -0
  56. package/dist/utils/quality/html-tag-checker.js +64 -0
  57. package/dist/utils/quality/index.js +220 -0
  58. package/dist/utils/quality/length-checker.js +140 -0
  59. package/dist/utils/quality/markdown-checker.js +220 -0
  60. package/dist/utils/quality/placeholder-checker.js +142 -0
  61. package/dist/utils/quality/punctuation-checker.js +50 -0
  62. package/dist/utils/quality/quote-balance-checker.js +143 -0
  63. package/dist/utils/quality/special-characters-checker.js +229 -0
  64. package/dist/utils/quality/style-guide-checker.js +192 -0
  65. package/dist/utils/quality/text-sanitizer.js +120 -0
  66. package/dist/utils/quality/tone-verifier.js +91 -0
  67. package/dist/utils/rate-limiter.js +398 -0
  68. package/dist/utils/retry-helper.js +166 -0
  69. package/dist/utils/state-manager.js +162 -0
  70. package/dist/utils/statistics-manager.js +51 -0
  71. package/dist/utils/ui-manager.js +98 -0
  72. 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;