locai 2.6.2 → 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 +14 -9
  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
@@ -1,8 +1,8 @@
1
+ import type { FlatTranslations, FlatTranslationValue } from "./object-transformer.js";
1
2
  export interface StateOptions {
2
3
  enabled?: boolean;
3
4
  stateFileName?: string;
4
5
  stateDir?: string;
5
- [key: string]: any;
6
6
  }
7
7
  export interface StateComparison {
8
8
  deletedKeys: string[];
@@ -29,7 +29,7 @@ declare class StateManager {
29
29
  /**
30
30
  * Generate a SHA-256 hash for given text content
31
31
  */
32
- _generateHash(text: string | object): string;
32
+ _generateHash(text: FlatTranslationValue): string;
33
33
  /**
34
34
  * Get the full path to the state file
35
35
  */
@@ -45,7 +45,16 @@ declare class StateManager {
45
45
  /**
46
46
  * Generate state object from source content by creating hashes for each key
47
47
  */
48
- generateStateFromSource(sourceContent: Record<string, string>): Record<string, string>;
48
+ generateStateFromSource(sourceContent: FlatTranslations): Record<string, string>;
49
+ /**
50
+ * The state to save after a run: the current source hashes, except that keys whose
51
+ * translation did not finish keep their previous hash (or stay absent if they are new),
52
+ * so the next run still sees them as changed and translates them again.
53
+ * @param {Record<string, string>} previousState - State loaded at the start of the run.
54
+ * @param {Record<string, string>} currentState - Hashes of the current source.
55
+ * @param {Iterable<string>} unfinishedKeys - Keys that failed or were not processed.
56
+ */
57
+ stateAfterRun(previousState: Record<string, string>, currentState: Record<string, string>, unfinishedKeys: Iterable<string>): Record<string, string>;
49
58
  /**
50
59
  * Compare two states and identify changes
51
60
  */
@@ -1,5 +1,5 @@
1
1
  import crypto from "crypto";
2
- import { FileManager } from "./file-manager.js"; // Note: Changed named import to default import based on file-manager migration
2
+ import { FileManager } from "./file-manager.js";
3
3
  import path from "path";
4
4
  import { readFileSync } from "fs";
5
5
  import { fileURLToPath } from "url";
@@ -49,10 +49,12 @@ class StateManager {
49
49
  }
50
50
  catch (error) {
51
51
  // State file doesn't exist or is corrupted - return empty state
52
- if (error.message.includes("File read error") || error.code === "ENOENT") {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ const code = error instanceof Error && "code" in error ? error.code : undefined;
54
+ if (message.includes("File read error") || code === "ENOENT") {
53
55
  return {};
54
56
  }
55
- throw new Error(`Failed to load state: ${error.message}`);
57
+ throw new Error(`Failed to load state: ${message}`);
56
58
  }
57
59
  }
58
60
  /**
@@ -64,20 +66,22 @@ class StateManager {
64
66
  // Ensure state directory exists
65
67
  const stateDir = path.dirname(stateFilePath);
66
68
  await FileManager.ensureDir(stateDir);
69
+ const packageJson = JSON.parse(readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
67
70
  // Save state with metadata
68
71
  const stateWithMetadata = {
69
72
  ...state,
70
73
  _metadata: {
71
74
  lastUpdated: new Date().toISOString(),
72
75
  version: "1.0.0",
73
- toolVersion: JSON.parse(readFileSync(path.join(__dirname, "../../package.json"), "utf8")).version,
76
+ toolVersion: packageJson.version,
74
77
  },
75
78
  };
76
79
  await FileManager.writeJSON(stateFilePath, stateWithMetadata);
77
80
  return true;
78
81
  }
79
82
  catch (error) {
80
- throw new Error(`Failed to save state: ${error.message}`);
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ throw new Error(`Failed to save state: ${message}`);
81
85
  }
82
86
  }
83
87
  /**
@@ -91,6 +95,26 @@ class StateManager {
91
95
  }
92
96
  return state;
93
97
  }
98
+ /**
99
+ * The state to save after a run: the current source hashes, except that keys whose
100
+ * translation did not finish keep their previous hash (or stay absent if they are new),
101
+ * so the next run still sees them as changed and translates them again.
102
+ * @param {Record<string, string>} previousState - State loaded at the start of the run.
103
+ * @param {Record<string, string>} currentState - Hashes of the current source.
104
+ * @param {Iterable<string>} unfinishedKeys - Keys that failed or were not processed.
105
+ */
106
+ stateAfterRun(previousState, currentState, unfinishedKeys) {
107
+ const state = { ...currentState };
108
+ for (const key of unfinishedKeys) {
109
+ if (key in previousState) {
110
+ state[key] = previousState[key];
111
+ }
112
+ else {
113
+ delete state[key];
114
+ }
115
+ }
116
+ return state;
117
+ }
94
118
  /**
95
119
  * Compare two states and identify changes
96
120
  */
@@ -151,11 +175,12 @@ class StateManager {
151
175
  return true;
152
176
  }
153
177
  catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
154
179
  // File doesn't exist - that's fine
155
- if (error.message.includes("ENOENT")) {
180
+ if (message.includes("ENOENT")) {
156
181
  return true;
157
182
  }
158
- throw new Error(`Failed to cleanup state: ${error.message}`);
183
+ throw new Error(`Failed to cleanup state: ${message}`);
159
184
  }
160
185
  }
161
186
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Statistics manager to track translation progress and metrics
3
3
  */
4
+ import type Orchestrator from "../core/orchestrator.js";
4
5
  export interface GlobalStats {
5
6
  total: number;
6
7
  byCategory: Record<string, number>;
@@ -11,10 +12,12 @@ export interface GlobalStats {
11
12
  totalTime: number;
12
13
  success: number;
13
14
  failed: number;
15
+ /** Keys that failed in at least one language (may repeat across languages). */
16
+ failedKeys: string[];
14
17
  skipped: number;
15
18
  languages: Record<string, LanguageStats>;
16
19
  startTime: string;
17
- orchestrators: any[];
20
+ orchestrators: Orchestrator[];
18
21
  }
19
22
  export interface LanguageStats {
20
23
  processed: number;
@@ -22,7 +25,14 @@ export interface LanguageStats {
22
25
  skipped: number;
23
26
  failed: number;
24
27
  timeMs: number;
28
+ /** Set when the language could not be processed at all. */
29
+ error?: string;
25
30
  }
31
+ /** Whether a run had failures: keys that were not translated, or languages that errored. */
32
+ export declare const runFailures: (stats: Pick<GlobalStats, "failed" | "languages">) => {
33
+ failedTranslations: number;
34
+ failedLanguages: string[];
35
+ };
26
36
  export declare class StatisticsManager {
27
37
  private stats;
28
38
  constructor();
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Statistics manager to track translation progress and metrics
3
3
  */
4
+ /** Whether a run had failures: keys that were not translated, or languages that errored. */
5
+ export const runFailures = (stats) => ({
6
+ failedTranslations: stats.failed,
7
+ failedLanguages: Object.entries(stats.languages)
8
+ .filter(([, language]) => language.error)
9
+ .map(([code]) => code),
10
+ });
4
11
  export class StatisticsManager {
5
12
  stats;
6
13
  constructor() {
@@ -17,6 +24,7 @@ export class StatisticsManager {
17
24
  totalTime: 0,
18
25
  success: 0,
19
26
  failed: 0,
27
+ failedKeys: [],
20
28
  skipped: 0,
21
29
  languages: {},
22
30
  startTime: new Date().toISOString(),
@@ -1,3 +1,4 @@
1
+ import type { LanguageProcessResult } from "../types/index.js";
1
2
  import { GlobalStats } from "./statistics-manager.js";
2
3
  /**
3
4
  * UI Manager for handling CLI output and user interaction
@@ -15,7 +16,7 @@ export declare class UIManager {
15
16
  displayGlobalSummary(stats: GlobalStats & {
16
17
  totalDuration?: number;
17
18
  }, totalLanguages: number): Promise<void>;
18
- logBatchResults(batchResults: any[]): Promise<void>;
19
+ logBatchResults(batchResults: LanguageProcessResult[]): Promise<void>;
19
20
  }
20
21
  declare const _default: UIManager;
21
22
  export default _default;
@@ -1,3 +1,4 @@
1
+ import { runFailures } from "./statistics-manager.js";
1
2
  /**
2
3
  * UI Manager for handling CLI output and user interaction
3
4
  */
@@ -84,7 +85,20 @@ export class UIManager {
84
85
  // Clear completion message - only once
85
86
  await this.log("\n" + "=".repeat(60));
86
87
  const duration = stats.totalDuration || 0;
87
- await this.log(`All operations completed successfully in ${duration.toFixed(1)}s`);
88
+ const { failedTranslations, failedLanguages } = runFailures(stats);
89
+ if (failedTranslations > 0 || failedLanguages.length > 0) {
90
+ const parts = [];
91
+ if (failedTranslations > 0) {
92
+ parts.push(`${failedTranslations} translation(s) failed`);
93
+ }
94
+ if (failedLanguages.length > 0) {
95
+ parts.push(`could not process: ${failedLanguages.join(", ")}`);
96
+ }
97
+ await this.log(`Finished with errors in ${duration.toFixed(1)}s: ${parts.join("; ")}`);
98
+ }
99
+ else {
100
+ await this.log(`All operations completed successfully in ${duration.toFixed(1)}s`);
101
+ }
88
102
  await this.log("=".repeat(60) + "\n");
89
103
  }
90
104
  async logBatchResults(batchResults) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locai",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/core/orchestrator.js",
6
6
  "types": "dist/core/orchestrator.d.ts",
@@ -13,10 +13,10 @@
13
13
  "localize": "./dist/cli.js"
14
14
  },
15
15
  "scripts": {
16
- "start": "node dist/core/orchestrator.js",
17
- "dev": "tsx src/core/orchestrator.js",
18
- "build": "tsc",
19
- "typecheck": "tsc --noEmit",
16
+ "start": "node dist/cli.js",
17
+ "dev": "tsx src/cli.ts",
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "typecheck": "tsc",
20
20
  "lint": "eslint .",
21
21
  "lint:fix": "eslint . --fix",
22
22
  "format": "prettier --write \"**/*.{js,json,md,jsx,ts,tsx,css}\"",
@@ -32,7 +32,8 @@
32
32
  "translation",
33
33
  "l10n",
34
34
  "internationalization",
35
- "automation",
35
+ "json",
36
+ "i18next",
36
37
  "cli",
37
38
  "llm",
38
39
  "ai",
@@ -43,6 +44,8 @@
43
44
  "gemini",
44
45
  "deepseek",
45
46
  "xai",
47
+ "dashscope",
48
+ "qwen",
46
49
  "vector-database",
47
50
  "semantic-search",
48
51
  "developer-tools",
@@ -54,7 +57,7 @@
54
57
  "url": "https://github.com/ahmetenesdur"
55
58
  },
56
59
  "license": "MIT",
57
- "description": "Global, AI-Powered Translation CLI for every project.",
60
+ "description": "CLI that translates JSON locale files with AI providers (OpenAI, Anthropic, Gemini, DeepSeek, xAI, DashScope).",
58
61
  "repository": {
59
62
  "type": "git",
60
63
  "url": "git+https://github.com/ahmetenesdur/locai.git"
@@ -64,12 +67,13 @@
64
67
  },
65
68
  "homepage": "https://github.com/ahmetenesdur/locai#readme",
66
69
  "engines": {
67
- "node": ">=14.13.0"
70
+ "node": "^20.19.0 || >=22.0.0"
68
71
  },
69
72
  "exports": {
70
73
  ".": "./dist/core/orchestrator.js",
71
74
  "./orchestrator": "./dist/core/orchestrator.js",
72
- "./translator": "./dist/commands/translator.js"
75
+ "./translator": "./dist/commands/translator.js",
76
+ "./config": "./dist/config/index.js"
73
77
  },
74
78
  "dependencies": {
75
79
  "axios": "1.13.6",
@@ -100,6 +104,7 @@
100
104
  "std-mocks": "^2.0.0",
101
105
  "tsx": "^4.21.0",
102
106
  "typescript": "^5.9.3",
107
+ "typescript-eslint": "^8.72.2",
103
108
  "vitest": "^4.1.0"
104
109
  },
105
110
  "files": [
@@ -1,9 +0,0 @@
1
- export declare class InteractiveReviewer {
2
- private queue;
3
- private decisions;
4
- constructor();
5
- loadQueue(): boolean;
6
- start(): Promise<void>;
7
- private handleAction;
8
- private saveDecisions;
9
- }
@@ -1,109 +0,0 @@
1
- import prompts from "prompts";
2
- import fs from "fs";
3
- import path from "path";
4
- import ConfidenceScorer from "../utils/confidence-scorer.js";
5
- export class InteractiveReviewer {
6
- queue;
7
- decisions;
8
- constructor() {
9
- this.queue = [];
10
- this.decisions = {
11
- accepted: [],
12
- edited: [],
13
- rejected: [],
14
- skipped: [],
15
- };
16
- }
17
- loadQueue() {
18
- const reviewFile = path.join(process.cwd(), ".localize-cache", "review-queue.json");
19
- if (!fs.existsSync(reviewFile))
20
- return false;
21
- try {
22
- const data = JSON.parse(fs.readFileSync(reviewFile, "utf8"));
23
- this.queue = data.items || [];
24
- return true;
25
- }
26
- catch (error) {
27
- return false;
28
- }
29
- }
30
- async start() {
31
- if (!this.loadQueue() || this.queue.length === 0) {
32
- console.log("No items to review.");
33
- return;
34
- }
35
- console.clear();
36
- console.log(`\n🔍 Interactive Review Mode (${this.queue.length} items)\n`);
37
- for (let i = 0; i < this.queue.length; i++) {
38
- const item = this.queue[i];
39
- const progress = `[${i + 1}/${this.queue.length}]`;
40
- console.log(`\n${progress} ${item.language.toUpperCase()} | Key: ${item.key}`);
41
- console.log(`Confidence: ${ConfidenceScorer.formatConfidence(item.confidence.score)} (${item.confidence.level})`);
42
- const response = await prompts({
43
- type: "select",
44
- name: "action",
45
- message: `Translation: "${item.translation}"\nSource: "${item.source}"\nAction:`,
46
- choices: [
47
- { title: "✅ Approve", value: "approve" },
48
- { title: "✏️ Edit", value: "edit" },
49
- { title: "⏭️ Skip", value: "skip" },
50
- { title: "🚪 Exit", value: "exit" },
51
- ],
52
- initial: 0,
53
- });
54
- if (response.action === "exit")
55
- break;
56
- await this.handleAction(response.action, item);
57
- }
58
- this.saveDecisions();
59
- }
60
- async handleAction(action, item) {
61
- switch (action) {
62
- case "approve":
63
- this.decisions.accepted.push(item);
64
- break;
65
- case "edit":
66
- const editResponse = await prompts({
67
- type: "text",
68
- name: "newTranslation",
69
- message: "Enter new translation:",
70
- initial: item.translation,
71
- });
72
- if (editResponse.newTranslation) {
73
- item.translation = editResponse.newTranslation;
74
- item.edited = true;
75
- this.decisions.edited.push(item);
76
- }
77
- else {
78
- this.decisions.skipped.push(item);
79
- }
80
- break;
81
- case "skip":
82
- this.decisions.skipped.push(item);
83
- break;
84
- }
85
- }
86
- saveDecisions() {
87
- // Reuse existing ReviewCommand logic or implement save/apply logic here
88
- // For now, let's save to a decision file that ReviewCommand can pick up or apply directly
89
- const cacheDir = path.join(process.cwd(), ".localize-cache");
90
- if (!fs.existsSync(cacheDir))
91
- fs.mkdirSync(cacheDir, { recursive: true });
92
- const decisionsFile = path.join(cacheDir, "review-decisions.json");
93
- const data = {
94
- timestamp: new Date().toISOString(),
95
- decisions: this.decisions,
96
- stats: {
97
- accepted: this.decisions.accepted.length,
98
- edited: this.decisions.edited.length,
99
- rejected: this.decisions.rejected.length,
100
- skipped: this.decisions.skipped.length,
101
- },
102
- };
103
- fs.writeFileSync(decisionsFile, JSON.stringify(data, null, 2));
104
- console.log(`\n✅ Review complete! Decisions saved to ${decisionsFile}`);
105
- console.log(`Run 'locai review --apply' to apply changes to files.`);
106
- // Note: The existing CLI 'review' command applies immediately in its class.
107
- // We might want to trigger apply here or ensuring ReviewCommand is used.
108
- }
109
- }
@@ -1,100 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { ToneCheckStep } from "../ToneCheckStep.js";
3
- // Mock ToneVerifier
4
- vi.mock("../../../../utils/quality/tone-verifier.js", () => {
5
- return {
6
- ToneVerifier: class {
7
- verify = vi.fn();
8
- },
9
- };
10
- });
11
- describe("ToneCheckStep", () => {
12
- let step;
13
- let mockContext;
14
- let mockNext;
15
- let mockVerifier;
16
- beforeEach(() => {
17
- // Reset mocks
18
- vi.clearAllMocks();
19
- step = new ToneCheckStep({ enabled: true });
20
- mockContext = {
21
- translatedText: "Sanal Makine",
22
- sourceText: "Virtual Machine",
23
- sourceLang: "en",
24
- targetLang: "tr",
25
- options: {
26
- styleGuide: {
27
- toneOfVoice: "tech",
28
- },
29
- },
30
- qualityResult: {
31
- issues: [],
32
- },
33
- };
34
- mockNext = vi.fn().mockResolvedValue(undefined);
35
- // Get the mock instance
36
- // @ts-ignore
37
- mockVerifier = step.verifier;
38
- });
39
- it("should skip verification if disabled", async () => {
40
- const disabledStep = new ToneCheckStep({ enabled: false });
41
- await disabledStep.execute(mockContext, mockNext);
42
- expect(mockVerifier.verify).not.toHaveBeenCalled();
43
- expect(mockNext).toHaveBeenCalled();
44
- });
45
- it("should skip verification if no translated text", async () => {
46
- mockContext.translatedText = "";
47
- await step.execute(mockContext, mockNext);
48
- expect(mockVerifier.verify).not.toHaveBeenCalled();
49
- expect(mockNext).toHaveBeenCalled();
50
- });
51
- it("should skip verification if no tone defined", async () => {
52
- mockContext.options.styleGuide.toneOfVoice = undefined;
53
- await step.execute(mockContext, mockNext);
54
- expect(mockVerifier.verify).not.toHaveBeenCalled();
55
- expect(mockNext).toHaveBeenCalled();
56
- });
57
- it("should pass configured provider to verifier", async () => {
58
- mockContext.options.styleGuide.toneProvider = "gemini";
59
- mockVerifier.verify.mockResolvedValue({ passed: true, score: 10 });
60
- await step.execute(mockContext, mockNext);
61
- expect(mockVerifier.verify).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(String), expect.any(String), expect.any(String), "gemini", undefined);
62
- });
63
- it("should pass configured toneProvider and analysis options to verifier", async () => {
64
- mockContext.options.styleGuide.toneProvider = "deepseek";
65
- mockContext.options.styleGuide.analysisOptions = { temperature: 0.7 };
66
- mockVerifier.verify.mockResolvedValue({ passed: true, score: 10 });
67
- await step.execute(mockContext, mockNext);
68
- expect(mockVerifier.verify).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(String), expect.any(String), expect.any(String), "deepseek", { temperature: 0.7 });
69
- });
70
- it("should verify and add warning on failure", async () => {
71
- mockVerifier.verify.mockResolvedValue({
72
- passed: false,
73
- score: 5,
74
- reasoning: "Too simple",
75
- });
76
- await step.execute(mockContext, mockNext);
77
- expect(mockVerifier.verify).toHaveBeenCalledWith("Virtual Machine", "Sanal Makine", "en", "tr", "tech", undefined, // Default provider
78
- undefined // Default options
79
- );
80
- expect(mockContext.qualityResult.issues.length).toBe(1);
81
- expect(mockContext.qualityResult.issues[0].message).toContain("Too simple");
82
- expect(mockNext).toHaveBeenCalled();
83
- });
84
- it("should not add warning on success", async () => {
85
- mockVerifier.verify.mockResolvedValue({
86
- passed: true,
87
- score: 9,
88
- });
89
- await step.execute(mockContext, mockNext);
90
- expect(mockContext.qualityResult.issues.length).toBe(0);
91
- expect(mockNext).toHaveBeenCalled();
92
- });
93
- it("should handle verifier errors gracefully", async () => {
94
- mockVerifier.verify.mockRejectedValue(new Error("Network error"));
95
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
96
- await step.execute(mockContext, mockNext);
97
- expect(mockNext).toHaveBeenCalled();
98
- consoleSpy.mockRestore();
99
- });
100
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,91 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { ToneVerifier } from "../tone-verifier.js";
3
- import ProviderFactory from "../../../core/provider-factory.js";
4
- // Mock ProviderFactory
5
- vi.mock("../../../core/provider-factory.js", () => {
6
- const mockProvider = {
7
- chat: vi.fn(),
8
- analyze: vi.fn(),
9
- translate: vi.fn(),
10
- };
11
- return {
12
- default: {
13
- getProvider: vi.fn(() => mockProvider),
14
- getAvailableProviders: vi.fn(() => ["openai"]),
15
- },
16
- };
17
- });
18
- describe("ToneVerifier", () => {
19
- let verifier;
20
- let mockProvider;
21
- beforeEach(() => {
22
- verifier = new ToneVerifier();
23
- mockProvider = ProviderFactory.getProvider("openai");
24
- vi.clearAllMocks();
25
- });
26
- it("should use requested provider", async () => {
27
- mockProvider.chat.mockResolvedValue(JSON.stringify({ passed: true, score: 10 }));
28
- const result = await verifier.verify("src", "trgt", "en", "es", "formal", "gemini");
29
- expect(ProviderFactory.getProvider).toHaveBeenCalledWith("gemini", true);
30
- });
31
- it("should use openai as default provider", async () => {
32
- mockProvider.chat.mockResolvedValue(JSON.stringify({ passed: true, score: 10 }));
33
- await verifier.verify("src", "trgt", "en", "es", "formal");
34
- expect(ProviderFactory.getProvider).toHaveBeenCalledWith("openai", true);
35
- });
36
- it("should pass options to provider", async () => {
37
- mockProvider.chat.mockResolvedValue(JSON.stringify({ passed: true, score: 10 }));
38
- const options = { temperature: 0.1 };
39
- await verifier.verify("src", "trgt", "en", "es", "formal", "openai", options);
40
- expect(mockProvider.chat).toHaveBeenCalledWith(expect.any(Array), options);
41
- });
42
- it("should initialize provider lazily", async () => {
43
- // Mock a valid response to avoid internal error
44
- mockProvider.chat.mockResolvedValue(JSON.stringify({ passed: true, score: 10 }));
45
- // Create a new instance effectively
46
- const newVerifier = new ToneVerifier();
47
- // initialize is private but called by verify
48
- await newVerifier.verify("src", "trgt", "en", "es", "formal");
49
- expect(ProviderFactory.getProvider).toHaveBeenCalled();
50
- });
51
- it("should return perfect score when no target tone is provided", async () => {
52
- const result = await verifier.verify("src", "trgt", "en", "es", "");
53
- expect(result.passed).toBe(true);
54
- expect(result.score).toBe(10);
55
- });
56
- it("should parse valid JSON response from chat", async () => {
57
- const mockResponse = JSON.stringify({
58
- passed: true,
59
- score: 9,
60
- reasoning: "Good tone",
61
- suggestions: "None",
62
- });
63
- mockProvider.chat.mockResolvedValue(mockResponse);
64
- const result = await verifier.verify("Hello", "Hola", "en", "es", "formal");
65
- expect(result.passed).toBe(true);
66
- expect(result.score).toBe(9);
67
- expect(result.reasoning).toBe("Good tone");
68
- });
69
- it("should clean markdown and parse JSON response", async () => {
70
- const mockResponse = "```json\n" +
71
- JSON.stringify({
72
- passed: false,
73
- score: 4,
74
- reasoning: "Too casual",
75
- }) +
76
- "\n```";
77
- mockProvider.chat.mockResolvedValue(mockResponse);
78
- const result = await verifier.verify("Hello", "Hola", "en", "es", "formal");
79
- expect(result.passed).toBe(false);
80
- expect(result.score).toBe(4);
81
- });
82
- it("should fail open (return passed) when provider throws", async () => {
83
- mockProvider.chat.mockRejectedValue(new Error("API Error"));
84
- const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
85
- const result = await verifier.verify("Hello", "Hola", "en", "es", "formal");
86
- expect(result.passed).toBe(true);
87
- expect(result.score).toBe(0);
88
- expect(result.reasoning).toContain("Verification failed");
89
- consoleSpy.mockRestore();
90
- });
91
- });