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.
Files changed (131) hide show
  1. package/README.md +23 -25
  2. package/dist/cli/helpers.d.ts +34 -2
  3. package/dist/cli/helpers.js +96 -37
  4. package/dist/cli.js +40 -39
  5. package/dist/commands/review.d.ts +33 -8
  6. package/dist/commands/review.js +89 -22
  7. package/dist/commands/translator.d.ts +4 -2
  8. package/dist/commands/translator.js +1 -1
  9. package/dist/config/index.d.ts +53 -66
  10. package/dist/config/index.js +2 -1
  11. package/dist/config/setup.d.ts +3 -2
  12. package/dist/config/setup.js +5 -3
  13. package/dist/core/adapters/android-adapter.d.ts +3 -2
  14. package/dist/core/adapters/android-adapter.js +12 -13
  15. package/dist/core/adapters/base-adapter.d.ts +13 -2
  16. package/dist/core/adapters/ios-adapter.d.ts +3 -2
  17. package/dist/core/adapters/json-adapter.d.ts +4 -3
  18. package/dist/core/adapters/json-adapter.js +1 -3
  19. package/dist/core/adapters/po-adapter.d.ts +3 -2
  20. package/dist/core/adapters/po-adapter.js +4 -5
  21. package/dist/core/adapters/properties-adapter.d.ts +3 -2
  22. package/dist/core/adapters/properties-adapter.js +2 -4
  23. package/dist/core/adapters/yaml-adapter.d.ts +4 -3
  24. package/dist/core/adapters/yaml-adapter.js +1 -3
  25. package/dist/core/context-processor.d.ts +21 -6
  26. package/dist/core/context-processor.js +2 -7
  27. package/dist/core/detection/framework-detector.js +1 -1
  28. package/dist/core/detection/structure-detector.js +1 -1
  29. package/dist/core/fallback-provider.d.ts +3 -4
  30. package/dist/core/fallback-provider.js +15 -21
  31. package/dist/core/orchestrator.d.ts +98 -26
  32. package/dist/core/orchestrator.js +56 -116
  33. package/dist/core/pipeline/Pipeline.d.ts +2 -2
  34. package/dist/core/pipeline/Pipeline.js +1 -3
  35. package/dist/core/pipeline/context.d.ts +69 -13
  36. package/dist/core/pipeline/context.js +1 -7
  37. package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
  38. package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
  39. package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
  40. package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
  41. package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
  42. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
  43. package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
  44. package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
  45. package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
  46. package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
  47. package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
  48. package/dist/core/pipeline/steps/TranslationStep.js +21 -23
  49. package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
  50. package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
  51. package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
  52. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
  53. package/dist/core/provider-factory.d.ts +15 -9
  54. package/dist/core/provider-factory.js +42 -32
  55. package/dist/providers/anthropic.d.ts +5 -4
  56. package/dist/providers/anthropic.js +3 -2
  57. package/dist/providers/api-types.d.ts +74 -0
  58. package/dist/providers/api-types.js +5 -0
  59. package/dist/providers/base-provider.d.ts +27 -31
  60. package/dist/providers/base-provider.js +22 -46
  61. package/dist/providers/dashscope.js +0 -2
  62. package/dist/providers/deepseek.js +0 -1
  63. package/dist/providers/gemini.js +0 -1
  64. package/dist/providers/openai.d.ts +3 -3
  65. package/dist/providers/openai.js +0 -1
  66. package/dist/providers/xai.js +0 -1
  67. package/dist/services/embedding-provider.js +4 -2
  68. package/dist/services/language-processor.d.ts +5 -3
  69. package/dist/services/language-processor.js +38 -43
  70. package/dist/services/source-analyzer.d.ts +24 -0
  71. package/dist/services/source-analyzer.js +161 -68
  72. package/dist/services/translation-service.d.ts +31 -13
  73. package/dist/services/translation-service.js +116 -71
  74. package/dist/services/vector-store.d.ts +15 -2
  75. package/dist/services/vector-store.js +14 -10
  76. package/dist/types/index.d.ts +31 -33
  77. package/dist/utils/ai-context-analyzer.d.ts +12 -3
  78. package/dist/utils/ai-context-analyzer.js +18 -10
  79. package/dist/utils/confidence-scorer.d.ts +16 -19
  80. package/dist/utils/confidence-scorer.js +36 -125
  81. package/dist/utils/error-helper.d.ts +27 -6
  82. package/dist/utils/error-helper.js +10 -9
  83. package/dist/utils/file-manager.d.ts +8 -7
  84. package/dist/utils/file-manager.js +22 -23
  85. package/dist/utils/glossary-manager.d.ts +22 -3
  86. package/dist/utils/glossary-manager.js +47 -28
  87. package/dist/utils/graceful-shutdown.d.ts +7 -1
  88. package/dist/utils/graceful-shutdown.js +1 -2
  89. package/dist/utils/input-validator.d.ts +53 -66
  90. package/dist/utils/input-validator.js +100 -151
  91. package/dist/utils/locale-paths.d.ts +7 -0
  92. package/dist/utils/locale-paths.js +16 -0
  93. package/dist/utils/logger.d.ts +8 -8
  94. package/dist/utils/logger.js +7 -15
  95. package/dist/utils/object-transformer.d.ts +20 -5
  96. package/dist/utils/object-transformer.js +20 -28
  97. package/dist/utils/progress-tracker.js +2 -3
  98. package/dist/utils/prompt-templates.d.ts +45 -5
  99. package/dist/utils/prompt-templates.js +61 -5
  100. package/dist/utils/quality/base-checker.d.ts +40 -18
  101. package/dist/utils/quality/base-checker.js +1 -2
  102. package/dist/utils/quality/code-block-checker.d.ts +13 -5
  103. package/dist/utils/quality/index.d.ts +2 -1
  104. package/dist/utils/quality/index.js +8 -27
  105. package/dist/utils/quality/length-checker.d.ts +35 -5
  106. package/dist/utils/quality/length-checker.js +2 -3
  107. package/dist/utils/quality/markdown-checker.d.ts +14 -7
  108. package/dist/utils/quality/special-characters-checker.d.ts +18 -1
  109. package/dist/utils/quality/special-characters-checker.js +0 -1
  110. package/dist/utils/quality/style-guide-checker.d.ts +1 -1
  111. package/dist/utils/quality/style-guide-checker.js +0 -2
  112. package/dist/utils/quality/tone-verifier.js +1 -1
  113. package/dist/utils/rate-limiter.d.ts +17 -1
  114. package/dist/utils/rate-limiter.js +12 -11
  115. package/dist/utils/retry-helper.d.ts +21 -3
  116. package/dist/utils/retry-helper.js +11 -3
  117. package/dist/utils/review-queue.d.ts +45 -0
  118. package/dist/utils/review-queue.js +53 -0
  119. package/dist/utils/state-manager.d.ts +12 -3
  120. package/dist/utils/state-manager.js +32 -7
  121. package/dist/utils/statistics-manager.d.ts +11 -1
  122. package/dist/utils/statistics-manager.js +8 -0
  123. package/dist/utils/ui-manager.d.ts +2 -1
  124. package/dist/utils/ui-manager.js +15 -1
  125. package/package.json +22 -17
  126. package/dist/commands/interactive-review.d.ts +0 -9
  127. package/dist/commands/interactive-review.js +0 -109
  128. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
  129. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
  130. package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
  131. 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
- gracefulShutdown.registerCallback(async () => {
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
- console.error("Failed to save state during shutdown:", error.message);
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
- await uiManager.log(`\nTranslation error: ${error.message}`);
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ await uiManager.log(`\nTranslation error: ${message}`);
54
67
  globalStats.error = {
55
- message: error.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
- await uiManager.log(`\nInput validation error: ${validationError.message}`);
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 {Record<string, any>} flattenedSource - Flattened source content.
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 {Record<string, any>} flattenedSource - Flattened source content.
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 {any} comparison - State comparison result.
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
- sharedAnalyzer.initialize();
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 {Record<string, any>} flattenedSource - Flattened source content.
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 {any} comparison - State comparison result.
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 {any} currentState - Current state object.
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
- // Save review queue if confidence scoring was enabled
214
- if (options.saveReviewQueue || options.minConfidence !== undefined) {
215
- if (globalStats.orchestrators && globalStats.orchestrators.length > 0) {
216
- // Collect all review queues from all orchestrators
217
- const allReviewItems = [];
218
- globalStats.orchestrators.forEach((orchestrator) => {
219
- if (orchestrator.confidenceSettings &&
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
- await uiManager.log(`Warning: Could not save state: ${error.message}`);
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
- let targetPath;
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
- await uiManager.log(` \u274c Error processing ${targetLang}: ${error.message}`);
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
- let targetPath;
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
- await uiManager.log(`Could not read ${targetLang}: ${err.message}`);
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: err.message,
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
- await uiManager.log(`\nValidation error: ${error.message}`);
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
- await uiManager.log(`Error finding locale files: ${error.message}`);
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?: any;
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[], metadata?: any): Promise<void>;
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
- console.error(`Failed to initialize vector store: ${error.message}`);
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, metadata = {}) {
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
- console.warn(`Failed to add item to vector store: ${error.message}`);
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
- // @ts-ignore - Vectra types might be mismatched in this version
59
- const results = await this.index.queryItems(embedding, limit);
60
- // Filter by target language and map to result format
61
- return results
62
- .filter((result) => result.item.metadata.targetLang === targetLang)
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
- console.warn(`Vector search failed: ${error.message}`);
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ console.warn(`Vector search failed: ${message}`);
76
80
  return [];
77
81
  }
78
82
  }
@@ -1,58 +1,56 @@
1
- import { OrchestratorOptions } from "../core/orchestrator.js";
2
- import { ContextConfig } from "../core/context-processor.js";
3
- export interface TranslationOptions extends Omit<OrchestratorOptions, "context"> {
4
- source: string;
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
- export interface GlobalStats {
24
- total: number;
25
- success: number;
26
- failed: number;
27
- skipped: number;
28
- languages: Record<string, any>;
29
- byCategory: Record<string, number>;
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
- existingTranslation?: string;
43
+ /** Current value of the key in the target file, if any. */
44
+ existingTranslation?: FlatTranslationValue;
50
45
  isModified: boolean;
51
46
  isNew: boolean;
52
- issueDetails?: any;
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
- status: any;
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: string;
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, any>;
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
- [key: string]: any;
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
- console.error("Batch AI analysis failed:", error.message);
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(), () => provider.analyze(batchPrompt, {
116
- ...this.config.analysisOptions,
117
- maxTokens: Math.min(4000, this.config.analysisOptions?.maxTokens || 2000),
118
- timeout: this.config.analysisOptions?.timeout || 10000,
119
- retries: this.config.analysisOptions?.retries || 2,
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
- console.error("Error parsing batch AI analysis result:", error.message);
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
- console.error("Error parsing AI analysis result:", error.message);
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
  }