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,7 +2,9 @@
2
2
  * Base class for translation providers
3
3
  */
4
4
  import ErrorHelper from "../utils/error-helper.js";
5
- import ConfidenceScorer from "../utils/confidence-scorer.js";
5
+ function isApiErrorLike(error) {
6
+ return typeof error === "object" && error !== null;
7
+ }
6
8
  class BaseProvider {
7
9
  name;
8
10
  config;
@@ -24,10 +26,10 @@ class BaseProvider {
24
26
  async translate(text, sourceLang, targetLang, _options = {}) {
25
27
  throw new Error(`translate method must be implemented by ${this.name} provider`);
26
28
  }
27
- async analyze(prompt, options = {}) {
29
+ async analyze(_prompt, _options = {}) {
28
30
  throw new Error(`analyze method must be implemented by ${this.name} provider if supported`);
29
31
  }
30
- async chat(messages, options = {}) {
32
+ async chat(_messages, _options = {}) {
31
33
  throw new Error(`chat method must be implemented by ${this.name} provider if supported`);
32
34
  }
33
35
  getApiKey() {
@@ -56,19 +58,20 @@ class BaseProvider {
56
58
  }
57
59
  return true;
58
60
  }
59
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
60
61
  handleApiError(error, providerName) {
61
- if (error.response) {
62
- const status = error.response.status;
63
- const apiMessage = error.response.data?.error?.message || error.response.data?.message || null;
62
+ const apiErrorLike = isApiErrorLike(error) ? error : {};
63
+ if (apiErrorLike.response) {
64
+ const { response } = apiErrorLike;
65
+ const status = response.status;
66
+ const apiMessage = response.data?.error?.message || response.data?.message || null;
64
67
  // Rate limit error
65
68
  if (status === 429) {
66
- const retryAfter = error.response.headers?.["retry-after"] || 60;
67
- const limit = error.response.headers?.["x-ratelimit-limit"];
68
- const remaining = error.response.headers?.["x-ratelimit-remaining"];
69
+ const retryAfter = response.headers?.["retry-after"] || 60;
70
+ const limit = response.headers?.["x-ratelimit-limit"];
71
+ const remaining = response.headers?.["x-ratelimit-remaining"];
69
72
  throw ErrorHelper.rateLimitError(providerName, {
70
73
  limit,
71
- current: limit ? limit - remaining : null,
74
+ current: limit ? Number(limit) - Number(remaining) : null,
72
75
  retryAfter,
73
76
  statusCode: status,
74
77
  });
@@ -91,19 +94,18 @@ class BaseProvider {
91
94
  throw apiError;
92
95
  }
93
96
  // Network errors
94
- if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
97
+ if (apiErrorLike.code === "ECONNREFUSED" || apiErrorLike.code === "ENOTFOUND") {
95
98
  throw ErrorHelper.networkError(providerName);
96
99
  }
97
100
  // Timeout errors
98
- if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
99
- throw ErrorHelper.timeoutError(providerName, error.config?.timeout);
101
+ if (apiErrorLike.code === "ECONNABORTED" || apiErrorLike.code === "ETIMEDOUT") {
102
+ throw ErrorHelper.timeoutError(providerName, apiErrorLike.config?.timeout);
100
103
  }
101
104
  // Unknown errors - preserve original for debugging
102
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
- const unknownError = new Error(`${providerName}: ${error.message}`);
104
- unknownError.code = "ERR_UNKNOWN";
105
- unknownError.originalError = error;
106
- throw unknownError;
105
+ throw Object.assign(new Error(`${providerName}: ${apiErrorLike.message}`), {
106
+ code: "ERR_UNKNOWN",
107
+ originalError: error,
108
+ });
107
109
  }
108
110
  generatePrompt(text, sourceLang, targetLang, options = {}) {
109
111
  const context = options.detectedContext;
@@ -145,7 +147,6 @@ class BaseProvider {
145
147
  endpoint: this.getEndpoint(),
146
148
  };
147
149
  }
148
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
149
150
  extractTranslation(response, providerName) {
150
151
  // Check for error response first
151
152
  if (response.error) {
@@ -171,7 +172,7 @@ class BaseProvider {
171
172
  else if (response.text) {
172
173
  translation = response.text.trim();
173
174
  }
174
- else if (response.content) {
175
+ else if (typeof response.content === "string" && response.content) {
175
176
  translation = response.content.trim();
176
177
  }
177
178
  else if (response.data &&
@@ -191,31 +192,6 @@ class BaseProvider {
191
192
  }
192
193
  return translation;
193
194
  }
194
- /**
195
- * Extract translation with confidence score
196
- */
197
- extractTranslationWithConfidence(
198
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
199
- response, providerName, sourceText, sourceLang, targetLang, category = "general") {
200
- const translation = this.extractTranslation(response, providerName);
201
- // Extract AI confidence from response
202
- const aiConfidence = ConfidenceScorer.extractAIConfidence(response, providerName);
203
- // Calculate comprehensive confidence score
204
- const confidenceResult = ConfidenceScorer.calculateConfidence({
205
- aiConfidence,
206
- sourceText,
207
- translation,
208
- sourceLang,
209
- targetLang,
210
- provider: providerName,
211
- category,
212
- });
213
- return {
214
- translation,
215
- confidence: confidenceResult,
216
- rawResponse: response,
217
- };
218
- }
219
195
  sanitizeTranslation(translation) {
220
196
  if (!translation || typeof translation !== "string") {
221
197
  throw new Error("Invalid translation format");
@@ -22,7 +22,6 @@ class DashScopeProvider extends BaseProvider {
22
22
  },
23
23
  timeout: config.timeoutMs || 60000,
24
24
  maxRedirects: 0,
25
- validateStatus: (status) => status < 500,
26
25
  });
27
26
  // Generation client for analysis
28
27
  this.generationClient = axios.create({
@@ -33,7 +32,6 @@ class DashScopeProvider extends BaseProvider {
33
32
  },
34
33
  timeout: config.timeoutMs || 60000,
35
34
  maxRedirects: 0,
36
- validateStatus: (status) => status < 500,
37
35
  });
38
36
  }
39
37
  getApiKey() {
@@ -21,7 +21,6 @@ class DeepSeekProvider extends BaseProvider {
21
21
  },
22
22
  timeout: config.timeoutMs || 60000,
23
23
  maxRedirects: 0,
24
- validateStatus: (status) => status < 500,
25
24
  });
26
25
  }
27
26
  getApiKey() {
@@ -20,7 +20,6 @@ class GeminiProvider extends BaseProvider {
20
20
  },
21
21
  timeout: config.timeoutMs || 60000,
22
22
  maxRedirects: 0,
23
- validateStatus: (status) => status < 500,
24
23
  });
25
24
  }
26
25
  getApiKey() {
@@ -1,4 +1,4 @@
1
- import BaseProvider, { ProviderConfig, TranslateOptions } from "./base-provider.js";
1
+ import BaseProvider, { ChatMessage, ProviderConfig, TranslateOptions } from "./base-provider.js";
2
2
  /**
3
3
  * Provider implementation for OpenAI (GPT) models.
4
4
  */
@@ -16,13 +16,13 @@ declare class OpenAIProvider extends BaseProvider {
16
16
  */
17
17
  translate(text: string, sourceLang: string, targetLang: string, options?: TranslateOptions): Promise<string>;
18
18
  analyze(prompt: string, options?: ProviderConfig): Promise<string>;
19
- chat(messages: any[], options?: ProviderConfig): Promise<string>;
19
+ chat(messages: ChatMessage[], options?: ProviderConfig): Promise<string>;
20
20
  embed(text: string, options?: ProviderConfig): Promise<number[]>;
21
21
  embedBatch(texts: string[], options?: ProviderConfig): Promise<number[][]>;
22
22
  }
23
23
  declare function translate(text: string, sourceLang: string, targetLang: string, options?: TranslateOptions): Promise<string>;
24
24
  declare function analyze(prompt: string, options?: ProviderConfig): Promise<string>;
25
- declare function chat(messages: any[], options?: ProviderConfig): Promise<string>;
25
+ declare function chat(messages: ChatMessage[], options?: ProviderConfig): Promise<string>;
26
26
  declare function embed(text: string, options?: ProviderConfig): Promise<number[]>;
27
27
  declare function embedBatch(texts: string[], options?: ProviderConfig): Promise<number[][]>;
28
28
  export { translate, analyze, chat, embed, embedBatch, OpenAIProvider };
@@ -21,7 +21,6 @@ class OpenAIProvider extends BaseProvider {
21
21
  },
22
22
  timeout: config.timeoutMs || 60000,
23
23
  maxRedirects: 0,
24
- validateStatus: (status) => status < 500,
25
24
  });
26
25
  }
27
26
  getApiKey() {
@@ -21,7 +21,6 @@ class XAIProvider extends BaseProvider {
21
21
  },
22
22
  timeout: config.timeoutMs || 60000,
23
23
  maxRedirects: 0,
24
- validateStatus: (status) => status < 500,
25
24
  });
26
25
  }
27
26
  getApiKey() {
@@ -26,7 +26,8 @@ export class EmbeddingProvider {
26
26
  return [];
27
27
  }
28
28
  catch (error) {
29
- console.error(`Failed to generate embedding: ${error.message}`);
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ console.error(`Failed to generate embedding: ${message}`);
30
31
  return [];
31
32
  }
32
33
  }
@@ -43,7 +44,8 @@ export class EmbeddingProvider {
43
44
  return [];
44
45
  }
45
46
  catch (error) {
46
- console.error(`Failed to generate batch embeddings: ${error.message}`);
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ console.error(`Failed to generate batch embeddings: ${message}`);
47
49
  return [];
48
50
  }
49
51
  }
@@ -1,18 +1,20 @@
1
+ import { FlatTranslations } from "../utils/object-transformer.js";
1
2
  import Orchestrator from "../core/orchestrator.js";
3
+ import { StateComparison } from "../utils/state-manager.js";
2
4
  import { TranslationOptions, GlobalStats, LanguageProcessResult } from "../types/index.js";
3
5
  export declare class LanguageProcessor {
4
6
  /**
5
7
  * Process a single language translation.
6
8
  * @param {string} targetLang - Target language code.
7
9
  * @param {string} sourceFile - Path to source file.
8
- * @param {Record<string, any>} flattenedSource - Flattened source content.
10
+ * @param {FlatTranslations} flattenedSource - Flattened source content.
9
11
  * @param {Orchestrator} orchestrator - Orchestrator instance.
10
12
  * @param {TranslationOptions} options - Translation options.
11
13
  * @param {GlobalStats} globalStats - Global statistics object.
12
- * @param {any} comparison - State comparison result.
14
+ * @param {Pick<StateComparison, "newKeys" | "modifiedKeys">} comparison - State comparison result.
13
15
  * @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
14
16
  */
15
- static processLanguage(targetLang: string, sourceFile: string, flattenedSource: Record<string, any>, orchestrator: Orchestrator, options: TranslationOptions, globalStats: GlobalStats, comparison: any): Promise<LanguageProcessResult>;
17
+ static processLanguage(targetLang: string, sourceFile: string, flattenedSource: FlatTranslations, orchestrator: Orchestrator, options: TranslationOptions, globalStats: GlobalStats, comparison: Pick<StateComparison, "newKeys" | "modifiedKeys">): Promise<LanguageProcessResult>;
16
18
  /**
17
19
  * Check if text only contains placeholders and whitespace.
18
20
  * @param {string} text - Text to check.
@@ -2,6 +2,7 @@ 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 InputValidator from "../utils/input-validator.js";
5
+ import { targetLocaleFile } from "../utils/locale-paths.js";
5
6
  import uiManager from "../utils/ui-manager.js";
6
7
  import statisticsManager from "../utils/statistics-manager.js";
7
8
  export class LanguageProcessor {
@@ -9,11 +10,11 @@ export class LanguageProcessor {
9
10
  * Process a single language translation.
10
11
  * @param {string} targetLang - Target language code.
11
12
  * @param {string} sourceFile - Path to source file.
12
- * @param {Record<string, any>} flattenedSource - Flattened source content.
13
+ * @param {FlatTranslations} flattenedSource - Flattened source content.
13
14
  * @param {Orchestrator} orchestrator - Orchestrator instance.
14
15
  * @param {TranslationOptions} options - Translation options.
15
16
  * @param {GlobalStats} globalStats - Global statistics object.
16
- * @param {any} comparison - State comparison result.
17
+ * @param {Pick<StateComparison, "newKeys" | "modifiedKeys">} comparison - State comparison result.
17
18
  * @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
18
19
  */
19
20
  static async processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison) {
@@ -42,25 +43,9 @@ export class LanguageProcessor {
42
43
  let finalStatus = null;
43
44
  let savedMessage = undefined;
44
45
  statisticsManager.initLanguageStats(safeTargetLang);
45
- const sourceDir = path.dirname(sourceFile);
46
- // Detect structure based on source file name
47
- const isNestedStructure = path.basename(sourceFile) === "translation.json";
48
- let targetPath;
49
- if (isNestedStructure) {
50
- // Nested: locales/tr/translation.json
51
- // We assume sourceDir ends with the language code (e.g. .../en)
52
- // So we need to go up one level and then into target lang
53
- const localesRoot = path.dirname(sourceDir);
54
- const targetDir = path.join(localesRoot, safeTargetLang);
55
- // Ensure target directory exists for nested structure
56
- await FileManager.ensureDir(targetDir);
57
- targetPath = path.join(targetDir, "translation.json");
58
- }
59
- else {
60
- // Flat: locales/tr.json
61
- const safeTargetFilename = `${safeTargetLang}.json`;
62
- targetPath = InputValidator.createSafeFilePath(sourceDir, safeTargetFilename);
63
- }
46
+ // Same layout as the source: locales/tr/translation.json or locales/tr.json
47
+ const targetPath = targetLocaleFile(sourceFile, safeTargetLang);
48
+ await FileManager.ensureDir(path.dirname(targetPath));
64
49
  let targetContent = {};
65
50
  try {
66
51
  targetContent = await FileManager.readJSON(targetPath);
@@ -70,12 +55,13 @@ export class LanguageProcessor {
70
55
  }
71
56
  }
72
57
  catch (err) {
73
- if (err.code === "ENOENT") {
58
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
74
59
  await uiManager.log(`\ud83c\udd95 Creating new translation file for ${safeTargetLang}`);
75
60
  }
76
61
  else {
77
62
  if (options.debug) {
78
- console.warn(`Error reading ${targetPath}: ${err.message}, using empty object`);
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ console.warn(`Error reading ${targetPath}: ${message}, using empty object`);
79
65
  }
80
66
  }
81
67
  targetContent = {};
@@ -88,20 +74,24 @@ export class LanguageProcessor {
88
74
  }
89
75
  }
90
76
  catch (err) {
91
- console.warn(`Error flattening target content: ${err.message}, using empty object`);
77
+ const message = err instanceof Error ? err.message : String(err);
78
+ console.warn(`Error flattening target content: ${message}, using empty object`);
92
79
  flattenedTarget = {};
93
80
  }
94
81
  const missingKeys = [];
95
82
  let hasPlaceholderOnlyChanges = false;
96
- for (const [key, sourceText] of Object.entries(flattenedSource)) {
83
+ for (const [key, sourceValue] of Object.entries(flattenedSource)) {
84
+ let sourceText;
97
85
  try {
98
86
  InputValidator.validateKey(key, "translation key");
99
- InputValidator.validateText(sourceText, "source text");
87
+ sourceText = InputValidator.validateText(sourceValue, "source text");
100
88
  }
101
89
  catch (keyError) {
102
- await uiManager.log(`\u26a0\ufe0f Skipping invalid key/text: ${keyError.message}`);
90
+ const message = keyError instanceof Error ? keyError.message : String(keyError);
91
+ await uiManager.log(`\u26a0\ufe0f Skipping invalid key/text: ${message}`);
103
92
  globalStats.languages[safeTargetLang].failed++;
104
93
  globalStats.failed++;
94
+ globalStats.failedKeys.push(key);
105
95
  continue;
106
96
  }
107
97
  globalStats.languages[safeTargetLang].processed++;
@@ -153,12 +143,10 @@ export class LanguageProcessor {
153
143
  }
154
144
  }
155
145
  catch (err) {
156
- console.error(`Error processing translations for ${safeTargetLang}: ${err.message}`);
146
+ const message = err instanceof Error ? err.message : String(err);
147
+ console.error(`Error processing translations for ${safeTargetLang}: ${message}`);
148
+ // Counted below: every missing key without a valid result has failed.
157
149
  results = [];
158
- if (globalStats.languages[safeTargetLang]) {
159
- globalStats.languages[safeTargetLang].failed += missingKeys.length;
160
- globalStats.failed += missingKeys.length;
161
- }
162
150
  }
163
151
  }
164
152
  else {
@@ -173,19 +161,20 @@ export class LanguageProcessor {
173
161
  }
174
162
  }
175
163
  catch (err) {
176
- console.warn(`Error getting orchestrator status: ${err.message}`);
164
+ const message = err instanceof Error ? err.message : String(err);
165
+ console.warn(`Error getting orchestrator status: ${message}`);
177
166
  }
178
167
  let validResults = [];
179
168
  try {
180
169
  validResults = results.filter((result) => result && result.success === true);
181
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
170
  validResults = validResults.filter((result) => result.key &&
183
171
  typeof result.key === "string" &&
184
172
  result.translated &&
185
173
  typeof result.translated === "string");
186
174
  }
187
175
  catch (err) {
188
- console.warn(`Error filtering results: ${err.message}`);
176
+ const message = err instanceof Error ? err.message : String(err);
177
+ console.warn(`Error filtering results: ${message}`);
189
178
  validResults = [];
190
179
  }
191
180
  if (validResults.length > 0) {
@@ -195,16 +184,21 @@ export class LanguageProcessor {
195
184
  }
196
185
  });
197
186
  }
187
+ // Every missing key without a valid result failed, whether or not anything is written.
188
+ const translatedKeys = new Set(validResults.map((result) => result.key));
189
+ const failedKeys = missingKeys
190
+ .map((item) => item.key)
191
+ .filter((key) => !translatedKeys.has(key));
192
+ globalStats.total += validResults.length + failedKeys.length;
193
+ globalStats.success += validResults.length;
194
+ globalStats.failed += failedKeys.length;
195
+ globalStats.failedKeys.push(...failedKeys);
196
+ globalStats.languages[safeTargetLang].added += validResults.length;
197
+ globalStats.languages[safeTargetLang].failed += failedKeys.length;
198
198
  const hasChangesToSave = validResults.length > 0 || hasPlaceholderOnlyChanges;
199
199
  if (hasChangesToSave) {
200
200
  const unflattened = ObjectTransformer.unflatten(flattenedTarget);
201
201
  await FileManager.writeJSON(targetPath, unflattened);
202
- globalStats.total += validResults.length;
203
- globalStats.success += validResults.length;
204
- globalStats.failed += results.length - validResults.length;
205
- globalStats.languages[safeTargetLang].added += validResults.length;
206
- globalStats.languages[safeTargetLang].failed +=
207
- results.length - validResults.length;
208
202
  validResults.forEach((result) => {
209
203
  if (result.context) {
210
204
  const category = result.context.category || "general";
@@ -227,9 +221,10 @@ export class LanguageProcessor {
227
221
  return { status: finalStatus, savedMessage };
228
222
  }
229
223
  catch (error) {
230
- const safeError = error.message.includes("outside working directory")
224
+ const message = error instanceof Error ? error.message : String(error);
225
+ const safeError = message.includes("outside working directory")
231
226
  ? "Invalid file path detected"
232
- : error.message;
227
+ : message;
233
228
  await uiManager.log(`\n\u274c Error processing ${targetLang}: ${safeError}`);
234
229
  if (globalStats.languages[targetLang]) {
235
230
  globalStats.languages[targetLang].error = safeError;
@@ -10,5 +10,29 @@ export declare class SourceCodeAnalyzer {
10
10
  private processFile;
11
11
  private processCallExpression;
12
12
  private extractContext;
13
+ /**
14
+ * The component the call is in: the nearest enclosing function, class or function-valued
15
+ * variable whose name starts with an uppercase letter, otherwise the nearest one of any name.
16
+ * A variable counts only when the call is inside a function in its initializer
17
+ * (`const Dialog = () => ...`), so `const label = t("key")` does not name a component.
18
+ */
19
+ private findComponent;
20
+ /**
21
+ * The JSX element the text belongs to: the one whose attribute holds the call
22
+ * (`<Button title={t("key")} />`) or whose child it is (`<Button>{t("key")}</Button>`).
23
+ * A call inside a nested function (`onClick={() => alert(t("key"))}`) belongs to none.
24
+ */
25
+ private findJsxElement;
26
+ /**
27
+ * Comments about the call, nearest first: comments right above the call's expression, JSX
28
+ * attribute, JSX element or statement, `{/* ... *\/}` comments right before its JSX element,
29
+ * and the comment above its component. Tooling directives are left out.
30
+ */
31
+ private getComments;
32
+ /**
33
+ * The code around the call with comments removed: the largest of the call, its enclosing
34
+ * expressions and its statement that fits in MAX_SNIPPET_LENGTH characters.
35
+ */
36
+ private getSnippet;
13
37
  getContext(key: string): CodeContext | undefined;
14
38
  }