locai 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +928 -0
  2. package/dist/cli.js +718 -0
  3. package/dist/commands/review.js +357 -0
  4. package/dist/commands/translator.js +29 -0
  5. package/dist/config/index.js +52 -0
  6. package/dist/core/adapters/base-adapter.js +1 -0
  7. package/dist/core/adapters/factory.js +44 -0
  8. package/dist/core/adapters/json-adapter.js +20 -0
  9. package/dist/core/adapters/po-adapter.js +50 -0
  10. package/dist/core/adapters/properties-adapter.js +31 -0
  11. package/dist/core/adapters/yaml-adapter.js +24 -0
  12. package/dist/core/context-processor.js +247 -0
  13. package/dist/core/detection/framework-detector.js +56 -0
  14. package/dist/core/detection/structure-detector.js +30 -0
  15. package/dist/core/fallback-provider.js +442 -0
  16. package/dist/core/orchestrator.js +438 -0
  17. package/dist/core/pipeline/Pipeline.js +48 -0
  18. package/dist/core/pipeline/PipelineStep.js +13 -0
  19. package/dist/core/pipeline/context.js +21 -0
  20. package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
  21. package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
  22. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
  23. package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
  24. package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
  25. package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
  26. package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
  27. package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
  28. package/dist/core/pipeline/steps/TranslationStep.js +65 -0
  29. package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
  30. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
  31. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
  32. package/dist/core/provider-factory.js +139 -0
  33. package/dist/providers/base-provider.js +230 -0
  34. package/dist/providers/dashscope.js +128 -0
  35. package/dist/providers/deepseek.js +111 -0
  36. package/dist/providers/gemini.js +135 -0
  37. package/dist/providers/openai.js +203 -0
  38. package/dist/providers/xai.js +111 -0
  39. package/dist/services/embedding-provider.js +50 -0
  40. package/dist/services/translation-service.js +609 -0
  41. package/dist/services/vector-store.js +79 -0
  42. package/dist/utils/ai-context-analyzer.js +460 -0
  43. package/dist/utils/confidence-scorer.js +277 -0
  44. package/dist/utils/error-helper.js +386 -0
  45. package/dist/utils/file-manager.js +357 -0
  46. package/dist/utils/glossary-manager.js +194 -0
  47. package/dist/utils/graceful-shutdown.js +118 -0
  48. package/dist/utils/input-validator.js +663 -0
  49. package/dist/utils/logger.js +374 -0
  50. package/dist/utils/object-transformer.js +177 -0
  51. package/dist/utils/progress-tracker.js +277 -0
  52. package/dist/utils/prompt-templates.js +415 -0
  53. package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
  54. package/dist/utils/quality/base-checker.js +73 -0
  55. package/dist/utils/quality/code-block-checker.js +214 -0
  56. package/dist/utils/quality/html-tag-checker.js +64 -0
  57. package/dist/utils/quality/index.js +220 -0
  58. package/dist/utils/quality/length-checker.js +140 -0
  59. package/dist/utils/quality/markdown-checker.js +220 -0
  60. package/dist/utils/quality/placeholder-checker.js +142 -0
  61. package/dist/utils/quality/punctuation-checker.js +50 -0
  62. package/dist/utils/quality/quote-balance-checker.js +143 -0
  63. package/dist/utils/quality/special-characters-checker.js +229 -0
  64. package/dist/utils/quality/style-guide-checker.js +192 -0
  65. package/dist/utils/quality/text-sanitizer.js +120 -0
  66. package/dist/utils/quality/tone-verifier.js +91 -0
  67. package/dist/utils/rate-limiter.js +398 -0
  68. package/dist/utils/retry-helper.js +166 -0
  69. package/dist/utils/state-manager.js +162 -0
  70. package/dist/utils/statistics-manager.js +51 -0
  71. package/dist/utils/ui-manager.js +98 -0
  72. package/package.json +80 -0
@@ -0,0 +1,91 @@
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
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Base Quality Checker.
3
+ * Common infrastructure for all quality checkers.
4
+ */
5
+ class BaseChecker {
6
+ rules;
7
+ styleGuide;
8
+ /**
9
+ * Create a new BaseChecker.
10
+ * @param {CheckerOptions} [options={}] - Configuration options.
11
+ */
12
+ constructor(options = {}) {
13
+ this.rules = {
14
+ placeholderConsistency: true,
15
+ htmlTagsConsistency: true,
16
+ punctuationCheck: true,
17
+ lengthValidation: true,
18
+ sanitizeOutput: true,
19
+ ...options,
20
+ };
21
+ this.styleGuide = {
22
+ formality: options.styleGuide?.formality || "neutral",
23
+ toneOfVoice: options.styleGuide?.toneOfVoice || "professional",
24
+ };
25
+ }
26
+ /**
27
+ * Create an issue object.
28
+ */
29
+ createIssue(type, message, details = {}) {
30
+ return {
31
+ type,
32
+ message,
33
+ timestamp: new Date().toISOString(),
34
+ ...details,
35
+ };
36
+ }
37
+ /**
38
+ * Create a fix object.
39
+ */
40
+ createFix(type, message, details = {}) {
41
+ return {
42
+ type,
43
+ message,
44
+ timestamp: new Date().toISOString(),
45
+ ...details,
46
+ };
47
+ }
48
+ /**
49
+ * Validate translation.
50
+ */
51
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
52
+ validate(_sourceText, _translatedText, _options) {
53
+ const issues = [];
54
+ return {
55
+ isValid: issues.length === 0,
56
+ issues,
57
+ };
58
+ }
59
+ /**
60
+ * Validate and fix translation.
61
+ */
62
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
63
+ validateAndFix(sourceText, translatedText) {
64
+ return {
65
+ originalText: translatedText,
66
+ fixedText: translatedText,
67
+ isModified: false,
68
+ issues: [],
69
+ fixes: [],
70
+ };
71
+ }
72
+ }
73
+ export default BaseChecker;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Code Block Preservation Checker.
3
+ * Ensures code blocks are preserved exactly during translation.
4
+ */
5
+ class CodeBlockChecker {
6
+ codePatterns;
7
+ constructor() {
8
+ this.codePatterns = {
9
+ // Fenced code blocks (```)
10
+ fencedBlocks: /```[\s\S]*?```/g,
11
+ // Inline code (`code`)
12
+ inlineCode: /`([^`]+)`/g,
13
+ // Indented code blocks (4 spaces)
14
+ indentedBlocks: /^( {4}|\t).*$/gm,
15
+ // HTML <code> tags
16
+ htmlCode: /<code>[\s\S]*?<\/code>/gi,
17
+ // HTML <pre> tags
18
+ htmlPre: /<pre>[\s\S]*?<\/pre>/gi,
19
+ };
20
+ }
21
+ /**
22
+ * Check if code blocks are preserved.
23
+ */
24
+ checkCodeBlockPreservation(sourceText, translatedText) {
25
+ const issues = [];
26
+ // Extract code blocks from source
27
+ const sourceCodeBlocks = this.extractCodeBlocks(sourceText);
28
+ const translatedCodeBlocks = this.extractCodeBlocks(translatedText);
29
+ // Check count matches
30
+ if (sourceCodeBlocks.length !== translatedCodeBlocks.length) {
31
+ issues.push({
32
+ type: "code-block-count",
33
+ severity: "error",
34
+ message: `Code block count mismatch: source has ${sourceCodeBlocks.length}, translation has ${translatedCodeBlocks.length}`,
35
+ details: {
36
+ sourceCount: sourceCodeBlocks.length,
37
+ translatedCount: translatedCodeBlocks.length,
38
+ },
39
+ });
40
+ }
41
+ // Check each code block is preserved exactly
42
+ for (let i = 0; i < Math.min(sourceCodeBlocks.length, translatedCodeBlocks.length); i++) {
43
+ const sourceBlock = sourceCodeBlocks[i];
44
+ const translatedBlock = translatedCodeBlocks[i];
45
+ if (sourceBlock.code !== translatedBlock.code) {
46
+ issues.push({
47
+ type: "code-block-modified",
48
+ severity: "critical",
49
+ message: `Code block ${i + 1} was modified during translation`,
50
+ details: {
51
+ index: i,
52
+ sourceCode: sourceBlock.code,
53
+ translatedCode: translatedBlock.code,
54
+ },
55
+ });
56
+ }
57
+ }
58
+ return issues;
59
+ }
60
+ /**
61
+ * Extract all code blocks from text.
62
+ */
63
+ extractCodeBlocks(text) {
64
+ const blocks = [];
65
+ // Extract fenced code blocks
66
+ const fencedMatches = text.matchAll(this.codePatterns.fencedBlocks);
67
+ for (const match of fencedMatches) {
68
+ if (match.index !== undefined) {
69
+ blocks.push({
70
+ type: "fenced",
71
+ full: match[0],
72
+ code: match[0],
73
+ position: match.index,
74
+ });
75
+ }
76
+ }
77
+ // Extract inline code
78
+ const inlineMatches = text.matchAll(this.codePatterns.inlineCode);
79
+ for (const match of inlineMatches) {
80
+ if (match.index !== undefined) {
81
+ blocks.push({
82
+ type: "inline",
83
+ full: match[0],
84
+ code: match[1],
85
+ position: match.index,
86
+ });
87
+ }
88
+ }
89
+ // Extract HTML code tags
90
+ const htmlCodeMatches = text.matchAll(this.codePatterns.htmlCode);
91
+ for (const match of htmlCodeMatches) {
92
+ if (match.index !== undefined) {
93
+ blocks.push({
94
+ type: "html-code",
95
+ full: match[0],
96
+ code: match[0],
97
+ position: match.index,
98
+ });
99
+ }
100
+ }
101
+ // Extract HTML pre tags
102
+ const htmlPreMatches = text.matchAll(this.codePatterns.htmlPre);
103
+ for (const match of htmlPreMatches) {
104
+ if (match.index !== undefined) {
105
+ blocks.push({
106
+ type: "html-pre",
107
+ full: match[0],
108
+ code: match[0],
109
+ position: match.index,
110
+ });
111
+ }
112
+ }
113
+ return blocks.sort((a, b) => a.position - b.position);
114
+ }
115
+ /**
116
+ * Fix code block preservation issues.
117
+ */
118
+ fixCodeBlockPreservation(sourceText, translatedText) {
119
+ const foundIssues = [];
120
+ const appliedFixes = [];
121
+ let fixedText = translatedText;
122
+ // Extract code blocks from both
123
+ const sourceCodeBlocks = this.extractCodeBlocks(sourceText);
124
+ const translatedCodeBlocks = this.extractCodeBlocks(translatedText);
125
+ // If counts don't match, try to restore from source
126
+ if (sourceCodeBlocks.length !== translatedCodeBlocks.length) {
127
+ foundIssues.push({
128
+ type: "code-block-count",
129
+ severity: "error", // Added severity
130
+ message: `Code block count mismatch: ${sourceCodeBlocks.length} vs ${translatedCodeBlocks.length}`,
131
+ });
132
+ // Attempt to restore missing code blocks
133
+ const result = this.restoreCodeBlocks(sourceText, translatedText, sourceCodeBlocks);
134
+ fixedText = result.text;
135
+ appliedFixes.push(...result.appliedFixes);
136
+ }
137
+ // Fix modified code blocks
138
+ for (let i = 0; i < Math.min(sourceCodeBlocks.length, translatedCodeBlocks.length); i++) {
139
+ const sourceBlock = sourceCodeBlocks[i];
140
+ const translatedBlock = translatedCodeBlocks[i];
141
+ if (sourceBlock.code !== translatedBlock.code) {
142
+ foundIssues.push({
143
+ type: "code-block-modified",
144
+ severity: "critical", // Added severity
145
+ message: `Code block ${i + 1} was modified`,
146
+ });
147
+ // Restore original code
148
+ fixedText = fixedText.replace(translatedBlock.full, sourceBlock.full);
149
+ appliedFixes.push({
150
+ type: "code-block-restore",
151
+ message: `Restored original code block ${i + 1}`,
152
+ });
153
+ }
154
+ }
155
+ return { text: fixedText, foundIssues, appliedFixes };
156
+ }
157
+ /**
158
+ * Restore missing code blocks from source.
159
+ */
160
+ restoreCodeBlocks(sourceText, translatedText, sourceCodeBlocks) {
161
+ const appliedFixes = [];
162
+ const fixedText = translatedText;
163
+ // This is a simplified restoration - in reality, we'd need more context
164
+ // to know exactly where to place the code blocks
165
+ // For now, if code blocks are completely missing, append them at the end
166
+ const translatedCodeBlocks = this.extractCodeBlocks(translatedText);
167
+ if (sourceCodeBlocks.length > translatedCodeBlocks.length) {
168
+ const missingCount = sourceCodeBlocks.length - translatedCodeBlocks.length;
169
+ appliedFixes.push({
170
+ type: "code-block-restore",
171
+ message: `Attempted to restore ${missingCount} missing code blocks`,
172
+ });
173
+ // Note: Full restoration would require AI to determine proper placement
174
+ }
175
+ return { text: fixedText, appliedFixes };
176
+ }
177
+ /**
178
+ * Protect code blocks before translation.
179
+ * Replaces code blocks with tokens to prevent translation.
180
+ */
181
+ protectCodeBlocks(text) {
182
+ const codeBlocks = this.extractCodeBlocks(text);
183
+ const tokenMap = new Map();
184
+ let protectedText = text;
185
+ codeBlocks.forEach((block, index) => {
186
+ const token = `__CODE_BLOCK_${index}__`;
187
+ tokenMap.set(token, block.full);
188
+ protectedText = protectedText.replace(block.full, token);
189
+ });
190
+ return { protectedText, tokenMap };
191
+ }
192
+ /**
193
+ * Restore code blocks after translation.
194
+ */
195
+ restoreCodeBlocksFromTokens(text, tokenMap) {
196
+ let restoredText = text;
197
+ for (const [token, codeBlock] of tokenMap.entries()) {
198
+ restoredText = restoredText.replace(token, codeBlock);
199
+ }
200
+ return restoredText;
201
+ }
202
+ /**
203
+ * Check if text contains code blocks.
204
+ */
205
+ hasCodeBlocks(text) {
206
+ for (const pattern of Object.values(this.codePatterns)) {
207
+ if (pattern.test(text)) {
208
+ return true;
209
+ }
210
+ }
211
+ return false;
212
+ }
213
+ }
214
+ export default CodeBlockChecker;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * HTML Tag Consistency Checker.
3
+ * Ensures HTML tags are preserved and correctly nested in translations.
4
+ */
5
+ class HtmlTagChecker {
6
+ /**
7
+ * Check for HTML tag mismatches between source and translation.
8
+ */
9
+ checkHtmlTags(source, translated) {
10
+ const tagRegex = /<[^>]+>/g;
11
+ const sourceTags = source.match(tagRegex) || [];
12
+ const translatedTags = translated.match(tagRegex) || [];
13
+ if (sourceTags.length !== translatedTags.length) {
14
+ return [
15
+ {
16
+ type: "htmlTag",
17
+ message: "HTML tag count mismatch",
18
+ source: sourceTags,
19
+ translated: translatedTags,
20
+ },
21
+ ];
22
+ }
23
+ return [];
24
+ }
25
+ /**
26
+ * Fix missing HTML tags in translated text.
27
+ */
28
+ fixHtmlTags(source, translated) {
29
+ const tagRegex = /<[^>]+>/g;
30
+ const sourceTags = source.match(tagRegex) || [];
31
+ let fixedText = translated;
32
+ const foundIssues = [];
33
+ const appliedFixes = [];
34
+ sourceTags.forEach((tag) => {
35
+ if (!fixedText.includes(tag)) {
36
+ foundIssues.push({
37
+ type: "htmlTag",
38
+ message: `Missing HTML tag: ${tag}`,
39
+ });
40
+ fixedText = this.insertHtmlTag(fixedText, tag);
41
+ appliedFixes.push({
42
+ type: "htmlTag",
43
+ message: `Added HTML tag: ${tag}`,
44
+ });
45
+ }
46
+ });
47
+ return { text: fixedText, foundIssues, appliedFixes };
48
+ }
49
+ /**
50
+ * Insert HTML tag into text.
51
+ */
52
+ insertHtmlTag(text, tag) {
53
+ const isClosingTag = tag.startsWith("</");
54
+ if (isClosingTag) {
55
+ const openingTag = tag.replace("/", "");
56
+ const position = text.indexOf(openingTag) + openingTag.length;
57
+ if (position >= openingTag.length) {
58
+ return text.slice(0, position) + tag + text.slice(position);
59
+ }
60
+ }
61
+ return tag + text;
62
+ }
63
+ }
64
+ export default HtmlTagChecker;
@@ -0,0 +1,220 @@
1
+ import BaseChecker from "./base-checker.js";
2
+ import PlaceholderChecker from "./placeholder-checker.js";
3
+ import HtmlTagChecker from "./html-tag-checker.js";
4
+ import PunctuationChecker from "./punctuation-checker.js";
5
+ import LengthChecker from "./length-checker.js";
6
+ import TextSanitizer from "./text-sanitizer.js";
7
+ import QuoteBalanceChecker from "./quote-balance-checker.js";
8
+ import StyleGuideChecker from "./style-guide-checker.js";
9
+ import MarkdownChecker from "./markdown-checker.js";
10
+ import CodeBlockChecker from "./code-block-checker.js";
11
+ import SpecialCharactersChecker from "./special-characters-checker.js";
12
+ /**
13
+ * Main Quality Checker Class.
14
+ * Orchestrates various quality checks and fixes for translated text.
15
+ */
16
+ class QualityChecker extends BaseChecker {
17
+ placeholderChecker;
18
+ htmlTagChecker;
19
+ punctuationChecker;
20
+ lengthChecker;
21
+ textSanitizer;
22
+ quoteBalanceChecker;
23
+ styleGuideChecker;
24
+ markdownChecker;
25
+ codeBlockChecker;
26
+ specialCharactersChecker;
27
+ context;
28
+ /**
29
+ * Initialize QualityChecker with options.
30
+ */
31
+ constructor(options = {}) {
32
+ super(options);
33
+ this.initializeCheckers();
34
+ this.context = options.context || {};
35
+ }
36
+ /**
37
+ * Initialize all individual checkers.
38
+ */
39
+ initializeCheckers() {
40
+ this.placeholderChecker = new PlaceholderChecker();
41
+ this.htmlTagChecker = new HtmlTagChecker();
42
+ this.punctuationChecker = new PunctuationChecker();
43
+ this.lengthChecker = new LengthChecker();
44
+ this.textSanitizer = new TextSanitizer();
45
+ this.quoteBalanceChecker = new QuoteBalanceChecker();
46
+ this.styleGuideChecker = new StyleGuideChecker(this.styleGuide);
47
+ this.markdownChecker = new MarkdownChecker();
48
+ this.codeBlockChecker = new CodeBlockChecker();
49
+ this.specialCharactersChecker = new SpecialCharactersChecker();
50
+ }
51
+ /**
52
+ * Validate translated text against source text.
53
+ */
54
+ validate(sourceText, translatedText, options = {}) {
55
+ const issues = [];
56
+ if (this.rules.placeholderConsistency) {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ issues.push(...this.placeholderChecker.checkPlaceholders(sourceText, translatedText));
59
+ }
60
+ if (this.rules.htmlTagsConsistency) {
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ issues.push(...this.htmlTagChecker.checkHtmlTags(sourceText, translatedText));
63
+ }
64
+ if (this.rules.punctuationCheck) {
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ issues.push(...this.punctuationChecker.checkPunctuation(sourceText, translatedText));
67
+ }
68
+ // Quote balance validation
69
+ if (this.rules.quoteBalanceCheck !== false) {
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ issues.push(...this.quoteBalanceChecker.checkQuoteBalance(translatedText));
72
+ issues.push(
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ ...this.quoteBalanceChecker.validateQuoteConsistency(sourceText, translatedText));
75
+ }
76
+ if (this.rules.lengthValidation) {
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ issues.push(...this.lengthChecker.checkLength(sourceText, translatedText, options));
79
+ }
80
+ // Style guide validation
81
+ if (this.styleGuide && this.styleGuideChecker) {
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ issues.push(...this.styleGuideChecker.checkStyleGuide(sourceText, translatedText));
84
+ }
85
+ // Markdown preservation
86
+ if (this.rules.markdownPreservation) {
87
+ issues.push(
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ ...this.markdownChecker.checkMarkdownPreservation(sourceText, translatedText));
90
+ }
91
+ // Code block preservation
92
+ if (this.rules.codeBlockPreservation) {
93
+ issues.push(
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ ...this.codeBlockChecker.checkCodeBlockPreservation(sourceText, translatedText));
96
+ }
97
+ // Special characters preservation
98
+ if (this.rules.specialCharacters) {
99
+ issues.push(
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ ...this.specialCharactersChecker.checkSpecialCharacters(sourceText, translatedText));
102
+ }
103
+ if (this.rules.sanitizeOutput) {
104
+ translatedText = this.sanitizeTranslation(translatedText);
105
+ }
106
+ return {
107
+ isValid: issues.length === 0,
108
+ issues,
109
+ source: sourceText,
110
+ translated: translatedText,
111
+ context: this.context,
112
+ };
113
+ }
114
+ /**
115
+ * Validate and attempt to fix translated text.
116
+ */
117
+ validateAndFix(sourceText, translatedText) {
118
+ let fixedText = translatedText;
119
+ const issues = [];
120
+ const fixes = [];
121
+ if (this.rules.sanitizeOutput) {
122
+ fixedText = this.sanitizeTranslation(fixedText);
123
+ }
124
+ if (this.rules.placeholderConsistency) {
125
+ const result = this.placeholderChecker.fixPlaceholders(sourceText, fixedText);
126
+ fixedText = result.text;
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ issues.push(...result.foundIssues);
129
+ fixes.push(...result.appliedFixes);
130
+ }
131
+ if (this.rules.htmlTagsConsistency) {
132
+ const result = this.htmlTagChecker.fixHtmlTags(sourceText, fixedText);
133
+ fixedText = result.text;
134
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
+ issues.push(...result.foundIssues);
136
+ fixes.push(...result.appliedFixes);
137
+ }
138
+ if (this.rules.punctuationCheck) {
139
+ const result = this.punctuationChecker.fixPunctuation(sourceText, fixedText);
140
+ fixedText = result.text;
141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
+ issues.push(...result.foundIssues);
143
+ fixes.push(...result.appliedFixes);
144
+ }
145
+ // Quote balance auto-fix
146
+ if (this.rules.quoteBalanceCheck !== false) {
147
+ const result = this.quoteBalanceChecker.fixQuoteBalance(fixedText);
148
+ fixedText = result.text;
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ issues.push(...result.foundIssues);
151
+ fixes.push(...result.appliedFixes);
152
+ }
153
+ // Style guide fixes
154
+ if (this.styleGuide && this.styleGuideChecker) {
155
+ const result = this.styleGuideChecker.fixStyleGuide(sourceText, fixedText);
156
+ fixedText = result.text;
157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
+ issues.push(...result.foundIssues);
159
+ fixes.push(...result.appliedFixes);
160
+ }
161
+ // Markdown preservation fixes
162
+ if (this.rules.markdownPreservation) {
163
+ const result = this.markdownChecker.fixMarkdownPreservation(sourceText, fixedText);
164
+ fixedText = result.text;
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
+ issues.push(...result.foundIssues);
167
+ fixes.push(...result.appliedFixes);
168
+ }
169
+ // Code block preservation fixes
170
+ if (this.rules.codeBlockPreservation) {
171
+ const result = this.codeBlockChecker.fixCodeBlockPreservation(sourceText, fixedText);
172
+ fixedText = result.text;
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ issues.push(...result.foundIssues);
175
+ fixes.push(...result.appliedFixes);
176
+ }
177
+ // Special characters fixes
178
+ if (this.rules.specialCharacters) {
179
+ const result = this.specialCharactersChecker.fixSpecialCharacters(sourceText, fixedText);
180
+ fixedText = result.text;
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ issues.push(...result.foundIssues);
183
+ fixes.push(...result.appliedFixes);
184
+ }
185
+ return {
186
+ originalText: translatedText,
187
+ fixedText,
188
+ isModified: translatedText !== fixedText,
189
+ issues,
190
+ fixes,
191
+ metadata: {
192
+ sourceLength: sourceText.length,
193
+ originalLength: translatedText.length,
194
+ fixedLength: fixedText.length,
195
+ timestamp: new Date().toISOString(),
196
+ },
197
+ };
198
+ }
199
+ /**
200
+ * Sanitize translated text using multiple passes.
201
+ */
202
+ sanitizeTranslation(text) {
203
+ if (!text)
204
+ return text;
205
+ // First pass sanitization
206
+ let sanitized = this.textSanitizer.sanitize(text);
207
+ // Remove any remaining think tags
208
+ if (sanitized.includes("<think>")) {
209
+ sanitized = this.textSanitizer.removeThinkTags(sanitized);
210
+ }
211
+ // Final cleanup of duplicate lines
212
+ sanitized = sanitized
213
+ .split("\n")
214
+ .map((line) => line.trim())
215
+ .filter((line, index, arr) => line !== arr[index - 1])
216
+ .join("\n");
217
+ return sanitized;
218
+ }
219
+ }
220
+ export default QualityChecker;