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.
- package/README.md +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- 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;
|