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,442 @@
1
+ import RetryHelper from "../utils/retry-helper.js";
2
+ import rateLimiter from "../utils/rate-limiter.js";
3
+ class FallbackProvider {
4
+ providers;
5
+ currentIndex;
6
+ providerStats;
7
+ maxRetries;
8
+ reRankInterval;
9
+ operationCount;
10
+ lastErrorTime;
11
+ consecutiveErrors;
12
+ constructor(providers) {
13
+ this.providers = providers;
14
+ this.currentIndex = 0;
15
+ this.providerStats = new Map();
16
+ this.maxRetries = 2;
17
+ this.reRankInterval = 10;
18
+ this.operationCount = 0;
19
+ this.lastErrorTime = null;
20
+ this.consecutiveErrors = 0;
21
+ this.providers.forEach((provider) => {
22
+ const providerName = this._getProviderName(provider);
23
+ this.providerStats.set(providerName, {
24
+ success: 0,
25
+ failure: 0,
26
+ avgResponseTime: 0,
27
+ totalTime: 0,
28
+ lastSuccess: null,
29
+ consecutiveFailures: 0,
30
+ lastError: null,
31
+ disabled: false,
32
+ disabledUntil: null,
33
+ });
34
+ });
35
+ }
36
+ _calculatePriority(text) {
37
+ if (!text)
38
+ return 1;
39
+ if (text.length < 100)
40
+ return 2;
41
+ if (text.length > 800)
42
+ return 0;
43
+ return 1;
44
+ }
45
+ async translate(text, sourceLang, targetLang, options = {}) {
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ const errors = [];
48
+ const startTime = Date.now();
49
+ const startIndex = this.currentIndex;
50
+ let currentAttempt = 0;
51
+ this._checkAndReRankProviders();
52
+ const availableProviders = this.providers.filter((_, index) => !this._isProviderDisabled(this.providers[index]));
53
+ if (availableProviders.length === 0) {
54
+ this._resetDisabledProviders();
55
+ availableProviders.push(...this.providers);
56
+ }
57
+ const totalProviders = availableProviders.length;
58
+ const maxAttempts = totalProviders * (this.maxRetries + 1);
59
+ while (currentAttempt < maxAttempts) {
60
+ const currentProviderIndex = this.currentIndex % totalProviders;
61
+ const providerData = availableProviders[currentProviderIndex];
62
+ const providerName = this._getProviderName(providerData);
63
+ const currentProvider = providerData.implementation;
64
+ this.operationCount++;
65
+ try {
66
+ if (!this.providerStats.has(providerName)) {
67
+ this.providerStats.set(providerName, {
68
+ success: 0,
69
+ failure: 0,
70
+ avgResponseTime: 0,
71
+ totalTime: 0,
72
+ lastSuccess: null,
73
+ consecutiveFailures: 0,
74
+ lastError: null,
75
+ disabled: false,
76
+ disabledUntil: null,
77
+ });
78
+ }
79
+ const providerStartTime = Date.now();
80
+ const result = await rateLimiter.enqueue(providerName.toLowerCase(), () => RetryHelper.withRetry(() => currentProvider.translate(text, sourceLang, targetLang, options), {
81
+ maxRetries: 0,
82
+ context: `Fallback:${providerName}`,
83
+ logContext: {
84
+ source: sourceLang,
85
+ target: targetLang,
86
+ providerIndex: currentProviderIndex,
87
+ attempt: currentAttempt + 1,
88
+ maxAttempts,
89
+ },
90
+ }), this._calculatePriority(text));
91
+ const responseTime = Date.now() - providerStartTime;
92
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
93
+ const stats = this.providerStats.get(providerName);
94
+ stats.success++;
95
+ stats.consecutiveFailures = 0;
96
+ stats.lastSuccess = new Date();
97
+ const prevTotal = stats.avgResponseTime * (stats.success + stats.failure - 1);
98
+ stats.totalTime = prevTotal + responseTime;
99
+ stats.avgResponseTime = stats.totalTime / (stats.success + stats.failure);
100
+ this.consecutiveErrors = 0;
101
+ this.lastErrorTime = null;
102
+ return result;
103
+ }
104
+ catch (error) {
105
+ if (this.providerStats.has(providerName)) {
106
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
107
+ const stats = this.providerStats.get(providerName);
108
+ stats.failure++;
109
+ stats.consecutiveFailures++;
110
+ stats.lastError = {
111
+ time: new Date(),
112
+ message: error.message,
113
+ };
114
+ if (stats.consecutiveFailures >= 5) {
115
+ this._disableProvider(providerData, 2 * 60 * 1000);
116
+ }
117
+ }
118
+ this.consecutiveErrors++;
119
+ this.lastErrorTime = Date.now();
120
+ errors.push({
121
+ provider: providerName,
122
+ error: error.message,
123
+ attempt: currentAttempt + 1,
124
+ });
125
+ const safeProviderName = `Provider_${(currentProviderIndex % totalProviders) + 1}`;
126
+ const safeErrorMessage = error.message.includes("API") || error.message.includes("key")
127
+ ? "Authentication or API error"
128
+ : error.message.substring(0, 100);
129
+ console.warn(`${safeProviderName} failed (attempt ${currentAttempt + 1}/${maxAttempts}): ${safeErrorMessage}`);
130
+ currentAttempt++;
131
+ this.currentIndex++;
132
+ }
133
+ }
134
+ this.currentIndex = startIndex;
135
+ throw new Error(`All providers failed after ${maxAttempts} attempts (${Date.now() - startTime}ms):\n${JSON.stringify(errors, null, 2)}`);
136
+ }
137
+ async analyze(prompt, options = {}) {
138
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
+ const errors = [];
140
+ const startTime = Date.now();
141
+ const savedIndex = this.currentIndex;
142
+ let currentAttempt = 0;
143
+ this._checkAndReRankProviders();
144
+ const availableProviders = this.providers.filter((provider) => !this._isProviderDisabled(provider) &&
145
+ typeof provider.implementation.analyze === "function");
146
+ if (availableProviders.length === 0) {
147
+ this._resetDisabledProviders();
148
+ availableProviders.push(...this.providers.filter((p) => typeof p.implementation.analyze === "function"));
149
+ }
150
+ const totalProviders = availableProviders.length;
151
+ if (totalProviders === 0) {
152
+ throw new Error("No providers support analysis capability");
153
+ }
154
+ const maxAttempts = totalProviders * (this.maxRetries + 1);
155
+ while (currentAttempt < maxAttempts) {
156
+ const currentProviderIndex = currentAttempt % totalProviders;
157
+ const providerData = availableProviders[currentProviderIndex];
158
+ const providerName = this._getProviderName(providerData);
159
+ const currentProvider = providerData.implementation;
160
+ this.operationCount++;
161
+ try {
162
+ if (!this.providerStats.has(providerName)) {
163
+ this.providerStats.set(providerName, {
164
+ success: 0,
165
+ failure: 0,
166
+ avgResponseTime: 0,
167
+ totalTime: 0,
168
+ lastSuccess: null,
169
+ consecutiveFailures: 0,
170
+ lastError: null,
171
+ disabled: false,
172
+ disabledUntil: null,
173
+ });
174
+ }
175
+ const providerStartTime = Date.now();
176
+ const result = await rateLimiter.enqueue(providerName.toLowerCase(), () => RetryHelper.withRetry(() => currentProvider.analyze
177
+ ? currentProvider.analyze(prompt, options)
178
+ : Promise.reject(new Error("Analyze not supported")), {
179
+ maxRetries: 0,
180
+ context: `Fallback:${providerName}`,
181
+ logContext: {
182
+ providerIndex: currentProviderIndex,
183
+ attempt: currentAttempt + 1,
184
+ maxAttempts,
185
+ },
186
+ }), this._calculatePriority(prompt));
187
+ const responseTime = Date.now() - providerStartTime;
188
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
189
+ const stats = this.providerStats.get(providerName);
190
+ stats.success++;
191
+ stats.consecutiveFailures = 0;
192
+ stats.lastSuccess = new Date();
193
+ const prevTotal = stats.avgResponseTime * (stats.success + stats.failure - 1);
194
+ stats.totalTime = prevTotal + responseTime;
195
+ stats.avgResponseTime = stats.totalTime / (stats.success + stats.failure);
196
+ this.consecutiveErrors = 0;
197
+ this.lastErrorTime = null;
198
+ this.currentIndex = savedIndex;
199
+ return result;
200
+ }
201
+ catch (error) {
202
+ if (this.providerStats.has(providerName)) {
203
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204
+ const stats = this.providerStats.get(providerName);
205
+ stats.failure++;
206
+ stats.consecutiveFailures++;
207
+ stats.lastError = {
208
+ time: new Date(),
209
+ message: error.message,
210
+ };
211
+ if (stats.consecutiveFailures >= 5) {
212
+ this._disableProvider(providerData, 2 * 60 * 1000);
213
+ }
214
+ }
215
+ this.consecutiveErrors++;
216
+ this.lastErrorTime = Date.now();
217
+ errors.push({
218
+ provider: providerName,
219
+ error: error.message,
220
+ attempt: currentAttempt + 1,
221
+ });
222
+ const safeProviderName = `Provider_${(currentProviderIndex % totalProviders) + 1}`;
223
+ const safeErrorMessage = error.message.includes("API") || error.message.includes("key")
224
+ ? "Authentication or API error"
225
+ : error.message.substring(0, 100);
226
+ console.warn(`${safeProviderName} failed (attempt ${currentAttempt + 1}/${maxAttempts}): ${safeErrorMessage}`);
227
+ currentAttempt++;
228
+ this.currentIndex++;
229
+ }
230
+ }
231
+ this.currentIndex = savedIndex;
232
+ throw new Error(`All providers failed for analysis after ${maxAttempts} attempts (${Date.now() - startTime}ms):\n${JSON.stringify(errors, null, 2)}`);
233
+ }
234
+ async chat(messages, options = {}) {
235
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
+ const errors = [];
237
+ const startTime = Date.now();
238
+ const savedIndex = this.currentIndex;
239
+ let currentAttempt = 0;
240
+ this._checkAndReRankProviders();
241
+ const availableProviders = this.providers.filter((provider) => !this._isProviderDisabled(provider) &&
242
+ typeof provider.implementation.chat === "function");
243
+ if (availableProviders.length === 0) {
244
+ this._resetDisabledProviders();
245
+ availableProviders.push(...this.providers.filter((p) => typeof p.implementation.chat === "function"));
246
+ }
247
+ const totalProviders = availableProviders.length;
248
+ if (totalProviders === 0) {
249
+ throw new Error("No providers support chat capability");
250
+ }
251
+ const maxAttempts = totalProviders * (this.maxRetries + 1);
252
+ while (currentAttempt < maxAttempts) {
253
+ const currentProviderIndex = currentAttempt % totalProviders;
254
+ const providerData = availableProviders[currentProviderIndex];
255
+ const providerName = this._getProviderName(providerData);
256
+ const currentProvider = providerData.implementation;
257
+ this.operationCount++;
258
+ try {
259
+ if (!this.providerStats.has(providerName)) {
260
+ this.providerStats.set(providerName, {
261
+ success: 0,
262
+ failure: 0,
263
+ avgResponseTime: 0,
264
+ totalTime: 0,
265
+ lastSuccess: null,
266
+ consecutiveFailures: 0,
267
+ lastError: null,
268
+ disabled: false,
269
+ disabledUntil: null,
270
+ });
271
+ }
272
+ const providerStartTime = Date.now();
273
+ const result = await rateLimiter.enqueue(providerName.toLowerCase(), () => RetryHelper.withRetry(() => currentProvider.chat
274
+ ? currentProvider.chat(messages, options)
275
+ : Promise.reject(new Error("Chat not supported")), {
276
+ maxRetries: 0,
277
+ context: `Fallback:${providerName}`,
278
+ logContext: {
279
+ providerIndex: currentProviderIndex,
280
+ attempt: currentAttempt + 1,
281
+ maxAttempts,
282
+ },
283
+ }), 1 // Priority 1 (normal)
284
+ );
285
+ const responseTime = Date.now() - providerStartTime;
286
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287
+ const stats = this.providerStats.get(providerName);
288
+ stats.success++;
289
+ stats.consecutiveFailures = 0;
290
+ stats.lastSuccess = new Date();
291
+ const prevTotal = stats.avgResponseTime * (stats.success + stats.failure - 1);
292
+ stats.totalTime = prevTotal + responseTime;
293
+ stats.avgResponseTime = stats.totalTime / (stats.success + stats.failure);
294
+ this.consecutiveErrors = 0;
295
+ this.lastErrorTime = null;
296
+ this.currentIndex = savedIndex;
297
+ return result;
298
+ }
299
+ catch (error) {
300
+ if (this.providerStats.has(providerName)) {
301
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302
+ const stats = this.providerStats.get(providerName);
303
+ stats.failure++;
304
+ stats.consecutiveFailures++;
305
+ stats.lastError = {
306
+ time: new Date(),
307
+ message: error.message,
308
+ };
309
+ if (stats.consecutiveFailures >= 5) {
310
+ this._disableProvider(providerData, 2 * 60 * 1000);
311
+ }
312
+ }
313
+ this.consecutiveErrors++;
314
+ this.lastErrorTime = Date.now();
315
+ errors.push({
316
+ provider: providerName,
317
+ error: error.message,
318
+ attempt: currentAttempt + 1,
319
+ });
320
+ const safeProviderName = `Provider_${(currentProviderIndex % totalProviders) + 1}`;
321
+ const safeErrorMessage = error.message.includes("API") || error.message.includes("key")
322
+ ? "Authentication or API error"
323
+ : error.message.substring(0, 100);
324
+ console.warn(`${safeProviderName} failed (attempt ${currentAttempt + 1}/${maxAttempts}): ${safeErrorMessage}`);
325
+ currentAttempt++;
326
+ this.currentIndex++;
327
+ }
328
+ }
329
+ this.currentIndex = savedIndex;
330
+ throw new Error(`All providers failed for chat after ${maxAttempts} attempts (${Date.now() - startTime}ms):\n${JSON.stringify(errors, null, 2)}`);
331
+ }
332
+ getStats() {
333
+ const stats = Object.fromEntries(this.providerStats);
334
+ Object.keys(stats).forEach((provider) => {
335
+ const providerStats = stats[provider];
336
+ const total = providerStats.success + providerStats.failure;
337
+ providerStats.successRate = total > 0 ? providerStats.success / total : 0;
338
+ providerStats.totalCalls = total;
339
+ providerStats.avgResponseTimeMs = Math.round(providerStats.avgResponseTime);
340
+ const isDisabled = this._isProviderDisabled(this.providers.find((p) => this._getProviderName(p) === provider));
341
+ providerStats.isDisabled = isDisabled;
342
+ if (isDisabled && providerStats.disabledUntil) {
343
+ providerStats.enablesInMs = Math.max(0, providerStats.disabledUntil - Date.now());
344
+ }
345
+ });
346
+ return stats;
347
+ }
348
+ reset() {
349
+ this.currentIndex = 0;
350
+ this._resetDisabledProviders();
351
+ }
352
+ resetStats() {
353
+ this.providerStats.clear();
354
+ this.providers.forEach((provider) => {
355
+ const providerName = this._getProviderName(provider);
356
+ this.providerStats.set(providerName, {
357
+ success: 0,
358
+ failure: 0,
359
+ avgResponseTime: 0,
360
+ totalTime: 0,
361
+ lastSuccess: null,
362
+ consecutiveFailures: 0,
363
+ lastError: null,
364
+ disabled: false,
365
+ disabledUntil: null,
366
+ });
367
+ });
368
+ this.operationCount = 0;
369
+ this.consecutiveErrors = 0;
370
+ this.lastErrorTime = null;
371
+ }
372
+ _checkAndReRankProviders() {
373
+ if (this.operationCount % this.reRankInterval !== 0) {
374
+ return;
375
+ }
376
+ const providerRanks = this.providers.map((provider, index) => {
377
+ const name = this._getProviderName(provider);
378
+ const stats = this.providerStats.get(name) || {
379
+ success: 0,
380
+ failure: 0,
381
+ avgResponseTime: 0,
382
+ consecutiveFailures: 0,
383
+ };
384
+ const total = stats.success + stats.failure;
385
+ let score = 0;
386
+ if (total > 0) {
387
+ const successRate = stats.success / total;
388
+ const responseTimePenalty = stats.avgResponseTime > 0 ? Math.min(0.3, stats.avgResponseTime / 5000) : 0;
389
+ score = successRate * (1 - responseTimePenalty);
390
+ if (stats.consecutiveFailures > 0) {
391
+ score -= Math.min(0.5, stats.consecutiveFailures * 0.1);
392
+ }
393
+ }
394
+ return { provider, index, score };
395
+ });
396
+ const hasEnoughData = providerRanks.some((p) => (this.providerStats.get(this._getProviderName(p.provider))?.success || 0) > 2);
397
+ if (!hasEnoughData) {
398
+ return;
399
+ }
400
+ providerRanks.sort((a, b) => b.score - a.score);
401
+ this.providers = providerRanks.map((p) => p.provider);
402
+ this.currentIndex = 0;
403
+ }
404
+ _disableProvider(provider, timeoutMs = 2 * 60 * 1000) {
405
+ const providerName = this._getProviderName(provider);
406
+ const stats = this.providerStats.get(providerName);
407
+ if (stats) {
408
+ stats.disabled = true;
409
+ stats.disabledUntil = Date.now() + timeoutMs;
410
+ setTimeout(() => {
411
+ stats.disabled = false;
412
+ stats.disabledUntil = null;
413
+ console.log(`Re-enabled provider: ${providerName}`);
414
+ }, timeoutMs);
415
+ console.log(`Temporarily disabled provider ${providerName} for ${timeoutMs / 1000}s due to failures`);
416
+ }
417
+ }
418
+ _isProviderDisabled(provider) {
419
+ if (!provider)
420
+ return true;
421
+ const providerName = this._getProviderName(provider);
422
+ const stats = this.providerStats.get(providerName);
423
+ if (!stats)
424
+ return false;
425
+ if (stats.disabled && stats.disabledUntil && Date.now() > stats.disabledUntil) {
426
+ stats.disabled = false;
427
+ stats.disabledUntil = null;
428
+ return false;
429
+ }
430
+ return stats.disabled === true;
431
+ }
432
+ _resetDisabledProviders() {
433
+ this.providerStats.forEach((stats) => {
434
+ stats.disabled = false;
435
+ stats.disabledUntil = null;
436
+ });
437
+ }
438
+ _getProviderName(provider) {
439
+ return provider?.name || "Unknown";
440
+ }
441
+ }
442
+ export default FallbackProvider;