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,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Special Characters Preservation Checker.
|
|
3
|
+
* Ensures special characters, symbols, and formatting are preserved.
|
|
4
|
+
*/
|
|
5
|
+
class SpecialCharactersChecker {
|
|
6
|
+
specialCharacterSets;
|
|
7
|
+
criticalCharacters;
|
|
8
|
+
constructor() {
|
|
9
|
+
// Define categories of special characters to preserve
|
|
10
|
+
this.specialCharacterSets = {
|
|
11
|
+
// Currency symbols
|
|
12
|
+
currency: /[$€£¥₹₽¢]/g,
|
|
13
|
+
// Math symbols
|
|
14
|
+
math: /[+\-×÷=≠<>≤≥±∞√∑∏]/g,
|
|
15
|
+
// Arrows
|
|
16
|
+
arrows: /[←→↑↓↔↕⇐⇒⇑⇓⇔]/g,
|
|
17
|
+
// Bullets and markers
|
|
18
|
+
bullets: /[•·●○◆◇■□▪▫]/g,
|
|
19
|
+
// Quotes and apostrophes
|
|
20
|
+
quotes: /[""''«»‹›]/g,
|
|
21
|
+
// Dashes and hyphens
|
|
22
|
+
dashes: /[–—−]/g,
|
|
23
|
+
// Special punctuation
|
|
24
|
+
punctuation: /[¿¡‽…]/g,
|
|
25
|
+
// Trademark and copyright
|
|
26
|
+
legal: /[©®™℠]/g,
|
|
27
|
+
// Fractions
|
|
28
|
+
fractions: /[¼½¾⅓⅔⅛⅜⅝⅞]/g,
|
|
29
|
+
// Degree and temperature
|
|
30
|
+
measurements: /[°℃℉]/g,
|
|
31
|
+
};
|
|
32
|
+
// Characters that should never be translated
|
|
33
|
+
this.criticalCharacters = [
|
|
34
|
+
...Array.from("$€£¥₹₽¢"),
|
|
35
|
+
...Array.from("©®™℠"),
|
|
36
|
+
...Array.from("°℃℉"),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if special characters are preserved.
|
|
41
|
+
*/
|
|
42
|
+
checkSpecialCharacters(sourceText, translatedText) {
|
|
43
|
+
const issues = [];
|
|
44
|
+
// Check each category of special characters
|
|
45
|
+
for (const [category, pattern] of Object.entries(this.specialCharacterSets)) {
|
|
46
|
+
const sourceMatches = sourceText.match(pattern) || [];
|
|
47
|
+
const translatedMatches = translatedText.match(pattern) || [];
|
|
48
|
+
if (sourceMatches.length !== translatedMatches.length) {
|
|
49
|
+
issues.push({
|
|
50
|
+
type: "special-characters",
|
|
51
|
+
category,
|
|
52
|
+
severity: this.criticalCharacters.some((char) => sourceMatches.includes(char))
|
|
53
|
+
? "error"
|
|
54
|
+
: "warning",
|
|
55
|
+
message: `${category} character count mismatch: source has ${sourceMatches.length}, translation has ${translatedMatches.length}`,
|
|
56
|
+
details: {
|
|
57
|
+
sourceChars: sourceMatches,
|
|
58
|
+
translatedChars: translatedMatches,
|
|
59
|
+
missing: sourceMatches.filter((char) => !translatedMatches.includes(char)),
|
|
60
|
+
extra: translatedMatches.filter((char) => !sourceMatches.includes(char)),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Check for character substitutions (e.g., " to ")
|
|
65
|
+
const sourceUniqueChars = new Set(sourceMatches);
|
|
66
|
+
const translatedUniqueChars = new Set(translatedMatches);
|
|
67
|
+
for (const char of sourceUniqueChars) {
|
|
68
|
+
if (!translatedUniqueChars.has(char)) {
|
|
69
|
+
issues.push({
|
|
70
|
+
type: "special-character-substitution",
|
|
71
|
+
category,
|
|
72
|
+
severity: "warning",
|
|
73
|
+
message: `Special character "${char}" was replaced or removed`,
|
|
74
|
+
details: {
|
|
75
|
+
originalChar: char,
|
|
76
|
+
charCode: char.charCodeAt(0),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Check for emoji preservation
|
|
83
|
+
const emojiIssues = this.checkEmojiPreservation(sourceText, translatedText);
|
|
84
|
+
issues.push(...emojiIssues);
|
|
85
|
+
return issues;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check emoji preservation.
|
|
89
|
+
*/
|
|
90
|
+
checkEmojiPreservation(sourceText, translatedText) {
|
|
91
|
+
const issues = [];
|
|
92
|
+
// Emoji regex pattern
|
|
93
|
+
const emojiPattern = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
|
|
94
|
+
const sourceEmojis = sourceText.match(emojiPattern) || [];
|
|
95
|
+
const translatedEmojis = translatedText.match(emojiPattern) || [];
|
|
96
|
+
if (sourceEmojis.length !== translatedEmojis.length) {
|
|
97
|
+
issues.push({
|
|
98
|
+
type: "emoji-preservation",
|
|
99
|
+
severity: "warning",
|
|
100
|
+
message: `Emoji count mismatch: source has ${sourceEmojis.length}, translation has ${translatedEmojis.length}`,
|
|
101
|
+
details: {
|
|
102
|
+
sourceEmojis,
|
|
103
|
+
translatedEmojis,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return issues;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Fix special character issues.
|
|
111
|
+
*/
|
|
112
|
+
fixSpecialCharacters(sourceText, translatedText) {
|
|
113
|
+
let fixedText = translatedText;
|
|
114
|
+
const foundIssues = [];
|
|
115
|
+
const appliedFixes = [];
|
|
116
|
+
// Extract special characters from source
|
|
117
|
+
const sourceSpecialChars = this.extractSpecialCharacters(sourceText);
|
|
118
|
+
// Check what's missing in translation
|
|
119
|
+
const translatedSpecialChars = this.extractSpecialCharacters(translatedText);
|
|
120
|
+
// Find missing critical characters
|
|
121
|
+
for (const [category, chars] of Object.entries(sourceSpecialChars)) {
|
|
122
|
+
const missingChars = chars.filter((char) => !translatedSpecialChars[category]?.includes(char));
|
|
123
|
+
if (missingChars.length > 0) {
|
|
124
|
+
foundIssues.push({
|
|
125
|
+
type: "missing-special-chars",
|
|
126
|
+
category,
|
|
127
|
+
severity: "warning", // Added severity
|
|
128
|
+
message: `Missing ${missingChars.length} ${category} characters`,
|
|
129
|
+
});
|
|
130
|
+
// Try to restore critical characters
|
|
131
|
+
if (this.criticalCharacters.some((critical) => missingChars.includes(critical))) {
|
|
132
|
+
const result = this.restoreCriticalCharacters(sourceText, fixedText, missingChars);
|
|
133
|
+
fixedText = result.text;
|
|
134
|
+
appliedFixes.push(...result.appliedFixes);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Fix common character substitutions
|
|
139
|
+
const substitutionResult = this.fixCommonSubstitutions(sourceText, fixedText);
|
|
140
|
+
fixedText = substitutionResult.text;
|
|
141
|
+
foundIssues.push(...substitutionResult.foundIssues);
|
|
142
|
+
appliedFixes.push(...substitutionResult.appliedFixes);
|
|
143
|
+
return { text: fixedText, foundIssues, appliedFixes };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Extract special characters by category.
|
|
147
|
+
*/
|
|
148
|
+
extractSpecialCharacters(text) {
|
|
149
|
+
const extracted = {};
|
|
150
|
+
for (const [category, pattern] of Object.entries(this.specialCharacterSets)) {
|
|
151
|
+
const matches = text.match(pattern) || [];
|
|
152
|
+
extracted[category] = matches;
|
|
153
|
+
}
|
|
154
|
+
return extracted;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Restore critical characters that were lost.
|
|
158
|
+
*/
|
|
159
|
+
restoreCriticalCharacters(sourceText, translatedText, missingChars) {
|
|
160
|
+
const appliedFixes = [];
|
|
161
|
+
const fixedText = translatedText;
|
|
162
|
+
// This is a simplified restoration
|
|
163
|
+
// In practice, we'd need context to know where to place the characters
|
|
164
|
+
for (const char of missingChars) {
|
|
165
|
+
if (this.criticalCharacters.includes(char)) {
|
|
166
|
+
// Find where this character appears in source
|
|
167
|
+
const sourceIndex = sourceText.indexOf(char);
|
|
168
|
+
if (sourceIndex !== -1) {
|
|
169
|
+
appliedFixes.push({
|
|
170
|
+
type: "critical-char-restore",
|
|
171
|
+
message: `Noted missing critical character: "${char}"`,
|
|
172
|
+
});
|
|
173
|
+
// Note: Full restoration would require AI to determine placement
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { text: fixedText, appliedFixes };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Fix common character substitutions.
|
|
181
|
+
*/
|
|
182
|
+
fixCommonSubstitutions(sourceText, translatedText) {
|
|
183
|
+
const foundIssues = [];
|
|
184
|
+
const appliedFixes = [];
|
|
185
|
+
let fixedText = translatedText;
|
|
186
|
+
// Common problematic substitutions
|
|
187
|
+
const substitutions = [
|
|
188
|
+
// Smart quotes to regular quotes (and vice versa)
|
|
189
|
+
{ wrong: '"', correct: '"', check: sourceText.includes('"') },
|
|
190
|
+
{ wrong: '"', correct: '"', check: sourceText.includes('"') },
|
|
191
|
+
{ wrong: "'", correct: "'", check: sourceText.includes("'") },
|
|
192
|
+
{ wrong: "'", correct: "'", check: sourceText.includes("'") },
|
|
193
|
+
// Dashes
|
|
194
|
+
{ wrong: "-", correct: "–", check: sourceText.includes("–") },
|
|
195
|
+
{ wrong: "-", correct: "—", check: sourceText.includes("—") },
|
|
196
|
+
// Ellipsis
|
|
197
|
+
{ wrong: "...", correct: "…", check: sourceText.includes("…") },
|
|
198
|
+
];
|
|
199
|
+
for (const { wrong, correct, check } of substitutions) {
|
|
200
|
+
if (check && fixedText.includes(wrong)) {
|
|
201
|
+
const count = (fixedText.match(new RegExp(wrong, "g")) || []).length;
|
|
202
|
+
foundIssues.push({
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
type: "character-substitution",
|
|
205
|
+
severity: "warning", // Added severity
|
|
206
|
+
message: `Found ${count} instances of "${wrong}" that should be "${correct}"`,
|
|
207
|
+
});
|
|
208
|
+
fixedText = fixedText.replace(new RegExp(wrong, "g"), correct);
|
|
209
|
+
appliedFixes.push({
|
|
210
|
+
type: "character-substitution",
|
|
211
|
+
message: `Replaced "${wrong}" with "${correct}" (${count} instances)`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { text: fixedText, foundIssues, appliedFixes };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Check if text has special characters.
|
|
219
|
+
*/
|
|
220
|
+
hasSpecialCharacters(text) {
|
|
221
|
+
for (const pattern of Object.values(this.specialCharacterSets)) {
|
|
222
|
+
if (pattern.test(text)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export default SpecialCharactersChecker;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Guide Checker.
|
|
3
|
+
* Enforces style guide conventions like Oxford comma and sentence case.
|
|
4
|
+
*/
|
|
5
|
+
class StyleGuideChecker {
|
|
6
|
+
styleGuide;
|
|
7
|
+
constructor(styleGuide = {}) {
|
|
8
|
+
this.styleGuide = {
|
|
9
|
+
formality: styleGuide?.formality || "neutral",
|
|
10
|
+
toneOfVoice: styleGuide?.toneOfVoice || "neutral",
|
|
11
|
+
toneProvider: styleGuide?.toneProvider || "openai",
|
|
12
|
+
enforceTone: styleGuide?.enforceTone || false, // Use the configured value directly
|
|
13
|
+
analysisOptions: styleGuide?.analysisOptions || {},
|
|
14
|
+
conventions: {
|
|
15
|
+
useOxfordComma: styleGuide?.conventions?.useOxfordComma ?? true,
|
|
16
|
+
useSentenceCase: styleGuide?.conventions?.useSentenceCase ?? true,
|
|
17
|
+
...styleGuide?.conventions,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if text follows style guide conventions.
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
25
|
+
checkStyleGuide(_sourceText, translatedText) {
|
|
26
|
+
const issues = [];
|
|
27
|
+
if (this.styleGuide.conventions.useOxfordComma) {
|
|
28
|
+
const oxfordIssues = this.checkOxfordComma(translatedText);
|
|
29
|
+
issues.push(...oxfordIssues);
|
|
30
|
+
}
|
|
31
|
+
if (this.styleGuide.conventions.useSentenceCase) {
|
|
32
|
+
const sentenceCaseIssues = this.checkSentenceCase(translatedText);
|
|
33
|
+
issues.push(...sentenceCaseIssues);
|
|
34
|
+
}
|
|
35
|
+
return issues;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check for Oxford comma usage in lists.
|
|
39
|
+
*/
|
|
40
|
+
checkOxfordComma(text) {
|
|
41
|
+
const issues = [];
|
|
42
|
+
// Pattern: word, word and word (missing Oxford comma)
|
|
43
|
+
// Should be: word, word, and word
|
|
44
|
+
const listPattern = /(\w+),\s+(\w+)\s+(?:and|or)\s+(\w+)/gi;
|
|
45
|
+
const matches = text.matchAll(listPattern);
|
|
46
|
+
for (const match of matches) {
|
|
47
|
+
const fullMatch = match[0];
|
|
48
|
+
// Check if there's already a comma before 'and/or'
|
|
49
|
+
if (!fullMatch.match(/,\s+(?:and|or)/)) {
|
|
50
|
+
issues.push({
|
|
51
|
+
type: "oxford-comma",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `Missing Oxford comma in list: "${fullMatch}"`,
|
|
54
|
+
suggestion: fullMatch.replace(/(\w+)\s+(and|or)/, "$1, $2"),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return issues;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check sentence case for headings/titles.
|
|
62
|
+
*/
|
|
63
|
+
checkSentenceCase(text) {
|
|
64
|
+
const issues = [];
|
|
65
|
+
// Check for potential headings (short lines with multiple capital letters)
|
|
66
|
+
const lines = text.split("\n");
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
// Skip empty lines and very long lines (likely not headings)
|
|
70
|
+
if (!trimmed || trimmed.length > 100)
|
|
71
|
+
continue;
|
|
72
|
+
// Check if line looks like a title (multiple consecutive capitals)
|
|
73
|
+
const allCapsPattern = /^[A-Z\s]+$/;
|
|
74
|
+
if (allCapsPattern.test(trimmed) && trimmed.length > 5) {
|
|
75
|
+
issues.push({
|
|
76
|
+
type: "sentence-case",
|
|
77
|
+
severity: "info",
|
|
78
|
+
message: `All caps heading detected: "${trimmed}"`,
|
|
79
|
+
suggestion: this.convertToSentenceCase(trimmed),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return issues;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fix style guide issues.
|
|
87
|
+
*/
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
89
|
+
fixStyleGuide(sourceText, translatedText) {
|
|
90
|
+
let fixedText = translatedText;
|
|
91
|
+
const foundIssues = [];
|
|
92
|
+
const appliedFixes = [];
|
|
93
|
+
if (this.styleGuide.conventions.useOxfordComma) {
|
|
94
|
+
const result = this.fixOxfordComma(fixedText);
|
|
95
|
+
fixedText = result.text;
|
|
96
|
+
foundIssues.push(...result.foundIssues);
|
|
97
|
+
appliedFixes.push(...result.appliedFixes);
|
|
98
|
+
}
|
|
99
|
+
if (this.styleGuide.conventions.useSentenceCase) {
|
|
100
|
+
const result = this.fixSentenceCase(fixedText);
|
|
101
|
+
fixedText = result.text;
|
|
102
|
+
foundIssues.push(...result.foundIssues);
|
|
103
|
+
appliedFixes.push(...result.appliedFixes);
|
|
104
|
+
}
|
|
105
|
+
return { text: fixedText, foundIssues, appliedFixes };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Fix missing Oxford commas.
|
|
109
|
+
*/
|
|
110
|
+
fixOxfordComma(text) {
|
|
111
|
+
const foundIssues = [];
|
|
112
|
+
const appliedFixes = [];
|
|
113
|
+
let fixedText = text;
|
|
114
|
+
// Pattern: word, word and word -> word, word, and word
|
|
115
|
+
const listPattern = /(\w+),\s+(\w+)\s+(and|or)\s+(\w+)/gi;
|
|
116
|
+
fixedText = fixedText.replace(listPattern, (match, word1, word2, conjunction, word3) => {
|
|
117
|
+
// Check if comma already exists before conjunction
|
|
118
|
+
if (!match.includes(`, ${conjunction}`)) {
|
|
119
|
+
foundIssues.push({
|
|
120
|
+
type: "oxford-comma",
|
|
121
|
+
severity: "warning", // Added severity
|
|
122
|
+
message: `Missing Oxford comma: "${match}"`,
|
|
123
|
+
});
|
|
124
|
+
appliedFixes.push({
|
|
125
|
+
type: "oxford-comma",
|
|
126
|
+
message: `Added Oxford comma before "${conjunction}"`,
|
|
127
|
+
});
|
|
128
|
+
return `${word1}, ${word2}, ${conjunction} ${word3}`;
|
|
129
|
+
}
|
|
130
|
+
return match;
|
|
131
|
+
});
|
|
132
|
+
return { text: fixedText, foundIssues, appliedFixes };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Fix sentence case issues.
|
|
136
|
+
*/
|
|
137
|
+
fixSentenceCase(text) {
|
|
138
|
+
const foundIssues = [];
|
|
139
|
+
const appliedFixes = [];
|
|
140
|
+
const lines = text.split("\n");
|
|
141
|
+
const fixedLines = [];
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const trimmed = line.trim();
|
|
144
|
+
// Check for all caps headings
|
|
145
|
+
const allCapsPattern = /^[A-Z\s]+$/;
|
|
146
|
+
if (allCapsPattern.test(trimmed) && trimmed.length > 5 && trimmed.length < 100) {
|
|
147
|
+
foundIssues.push({
|
|
148
|
+
type: "sentence-case",
|
|
149
|
+
severity: "info", // Added severity
|
|
150
|
+
message: `All caps heading: "${trimmed}"`,
|
|
151
|
+
});
|
|
152
|
+
const sentenceCase = this.convertToSentenceCase(trimmed);
|
|
153
|
+
fixedLines.push(line.replace(trimmed, sentenceCase));
|
|
154
|
+
appliedFixes.push({
|
|
155
|
+
type: "sentence-case",
|
|
156
|
+
message: `Converted to sentence case: "${trimmed}" → "${sentenceCase}"`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
fixedLines.push(line);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
text: fixedLines.join("\n"),
|
|
165
|
+
foundIssues,
|
|
166
|
+
appliedFixes,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Convert text to sentence case.
|
|
171
|
+
*/
|
|
172
|
+
convertToSentenceCase(text) {
|
|
173
|
+
// Capitalize first letter, lowercase the rest
|
|
174
|
+
// But preserve acronyms (2-3 letter all-caps words)
|
|
175
|
+
const words = text.split(/\s+/);
|
|
176
|
+
return words
|
|
177
|
+
.map((word, index) => {
|
|
178
|
+
// Keep short all-caps words (likely acronyms)
|
|
179
|
+
if (word.length <= 3 && word === word.toUpperCase()) {
|
|
180
|
+
return word;
|
|
181
|
+
}
|
|
182
|
+
// First word gets capital
|
|
183
|
+
if (index === 0) {
|
|
184
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
185
|
+
}
|
|
186
|
+
// Rest are lowercase (unless acronyms)
|
|
187
|
+
return word.toLowerCase();
|
|
188
|
+
})
|
|
189
|
+
.join(" ");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export default StyleGuideChecker;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Sanitizer.
|
|
3
|
+
* Cleans up translated text by removing AI artifacts, think tags, and unwanted formatting.
|
|
4
|
+
*/
|
|
5
|
+
class TextSanitizer {
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize text by applying a sequence of cleaning rules.
|
|
8
|
+
*/
|
|
9
|
+
sanitize(text) {
|
|
10
|
+
if (!text)
|
|
11
|
+
return text;
|
|
12
|
+
const rules = [
|
|
13
|
+
this.removeThinkTags,
|
|
14
|
+
this.removeMarkdownFormatting,
|
|
15
|
+
this.removeQuotes,
|
|
16
|
+
this.removeBulletPoints,
|
|
17
|
+
this.removeExplanations,
|
|
18
|
+
this.removeAIArtifacts,
|
|
19
|
+
this.removeAllArtifacts,
|
|
20
|
+
this.normalizeWhitespace,
|
|
21
|
+
this.trimSpecialChars,
|
|
22
|
+
];
|
|
23
|
+
return this.applyRules(text, rules);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Apply a list of rules to text.
|
|
27
|
+
*/
|
|
28
|
+
applyRules(text, rules) {
|
|
29
|
+
return rules.reduce((processedText, rule) => rule(processedText), text);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Remove <think> tags and their content.
|
|
33
|
+
*/
|
|
34
|
+
removeThinkTags = (text) => {
|
|
35
|
+
// More robust think tag removal
|
|
36
|
+
return text
|
|
37
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
38
|
+
.replace(/<think[\s\S]*?<\/think>/gi, "")
|
|
39
|
+
.trim();
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Remove bold/italic markdown formatting.
|
|
43
|
+
*/
|
|
44
|
+
removeMarkdownFormatting = (text) => {
|
|
45
|
+
return text.replace(/\*\*.*?:\*\*/g, "");
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Remove leading/trailing quotes.
|
|
49
|
+
*/
|
|
50
|
+
removeQuotes = (text) => {
|
|
51
|
+
return text.replace(/^['"]|['"]$/g, "");
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Remove bullet points at the start.
|
|
55
|
+
*/
|
|
56
|
+
removeBulletPoints = (text) => {
|
|
57
|
+
return text.replace(/^\s*[-•]\s*/g, "");
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Remove explanatory text and metadata.
|
|
61
|
+
*/
|
|
62
|
+
removeExplanations = (text) => {
|
|
63
|
+
// Remove any text between common explanation markers
|
|
64
|
+
return text
|
|
65
|
+
.replace(/<think>[\s\S]*?<\/think>/g, "")
|
|
66
|
+
.replace(/\[.*?\]/g, "")
|
|
67
|
+
.replace(/\(.*?\)/g, "")
|
|
68
|
+
.replace(/^(Translation:|Translated text:|Result:|Output:)/gi, "");
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Remove common AI conversational artifacts.
|
|
72
|
+
*/
|
|
73
|
+
removeAIArtifacts = (text) => {
|
|
74
|
+
// Remove common AI model output patterns
|
|
75
|
+
return text
|
|
76
|
+
.replace(/^(Here's the translation:|The translation is:|I would translate this as:)/gi, "")
|
|
77
|
+
.replace(/^[A-Za-z]+ translation: /g, "")
|
|
78
|
+
.replace(/\b(Note|Remember|Important):.+$/gi, "");
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Trim special characters from ends.
|
|
82
|
+
*/
|
|
83
|
+
trimSpecialChars = (text) => {
|
|
84
|
+
// Remove special characters and extra whitespace
|
|
85
|
+
return text.replace(/^['"*_~`]+|['"*_~`]+$/g, "").replace(/^\s+|\s+$/g, "");
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Normalize whitespace and remove duplicate lines.
|
|
89
|
+
*/
|
|
90
|
+
normalizeWhitespace = (text) => {
|
|
91
|
+
// Remove duplicate lines and normalize whitespace
|
|
92
|
+
const lines = text
|
|
93
|
+
.split("\n")
|
|
94
|
+
.map((line) => line.trim())
|
|
95
|
+
.filter((line) => line); // Remove empty lines
|
|
96
|
+
// Remove duplicate consecutive lines
|
|
97
|
+
const uniqueLines = lines.filter((line, index, arr) => line !== arr[index - 1]);
|
|
98
|
+
return uniqueLines.join("\n");
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Aggressive cleanup of all artifacts.
|
|
102
|
+
*/
|
|
103
|
+
removeAllArtifacts = (text) => {
|
|
104
|
+
const cleaned = text
|
|
105
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
106
|
+
.replace(/^[A-Za-z]+ translation:[\s\S]*?\n/gim, "")
|
|
107
|
+
.replace(/^(Here's|This is|The) (the )?translation:?\s*/gim, "")
|
|
108
|
+
.replace(/^Translation result:?\s*/gim, "")
|
|
109
|
+
.replace(/^\s*[-•]\s*/gm, "")
|
|
110
|
+
.replace(/^['"]|['"]$/g, "")
|
|
111
|
+
.replace(/^\s+|\s+$/gm, "");
|
|
112
|
+
// Remove duplicate lines
|
|
113
|
+
const lines = cleaned
|
|
114
|
+
.split("\n")
|
|
115
|
+
.filter((line) => line.trim())
|
|
116
|
+
.filter((line, index, arr) => line !== arr[index - 1]);
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export default TextSanitizer;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import ProviderFactory from "../../core/provider-factory.js";
|
|
2
|
+
export class ToneVerifier {
|
|
3
|
+
provider;
|
|
4
|
+
initialized = false;
|
|
5
|
+
constructor() {
|
|
6
|
+
// Provider will be initialized lazily to avoid startup overhead
|
|
7
|
+
// or circular dependency issues if ProviderFactory needs full env loaded
|
|
8
|
+
}
|
|
9
|
+
async initProvider(providerName) {
|
|
10
|
+
if (this.initialized && this.provider)
|
|
11
|
+
return;
|
|
12
|
+
// We use a lighter model if available, or just the default configured provider.
|
|
13
|
+
try {
|
|
14
|
+
// Try to get a specific "judge" provider if configured, otherwise default
|
|
15
|
+
this.provider = ProviderFactory.getProvider(providerName, true);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
// If specific provider fails, might fall back to any available
|
|
19
|
+
const available = ProviderFactory.getAvailableProviders();
|
|
20
|
+
if (available.length > 0) {
|
|
21
|
+
this.provider = ProviderFactory.getProvider(available[0], true);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw new Error("No AI providers available for ToneVerifier");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
this.initialized = true;
|
|
28
|
+
}
|
|
29
|
+
async verify(sourceText, translatedText, sourceLang, targetLang, targetTone, providerName = "openai", options = {}) {
|
|
30
|
+
if (!targetTone) {
|
|
31
|
+
return { passed: true, score: 10, reasoning: "No target tone specified." };
|
|
32
|
+
}
|
|
33
|
+
await this.initProvider(providerName);
|
|
34
|
+
const systemInstruction = `Role: You are a strict linguistic tone auditor.
|
|
35
|
+
Task: Analyze the tone of the provided translation against the Target Tone.
|
|
36
|
+
|
|
37
|
+
Instructions:
|
|
38
|
+
1. Analyze if the translation matches the Target Tone in the context of the Source text.
|
|
39
|
+
2. Ignore minor grammatical errors; focus on TONE and PERSONA (e.g., Formal, Casual, Authoritative, Friendly).
|
|
40
|
+
3. Rate the match on a scale of 0-10 (10 = Perfect match, 0 = Complete mismatch/Robot/Wrong tone).
|
|
41
|
+
4. If score is below 7, set passed to false.
|
|
42
|
+
|
|
43
|
+
Output JSON format ONLY:
|
|
44
|
+
{
|
|
45
|
+
"passed": boolean,
|
|
46
|
+
"score": number, // 0-10
|
|
47
|
+
"reasoning": "brief explanation",
|
|
48
|
+
"suggestions": "optional suggestion for improvement"
|
|
49
|
+
}`;
|
|
50
|
+
const userMessage = `
|
|
51
|
+
Target Tone: "${targetTone}"
|
|
52
|
+
Source (${sourceLang}): "${sourceText}"
|
|
53
|
+
Translation (${targetLang}): "${translatedText}"
|
|
54
|
+
`;
|
|
55
|
+
try {
|
|
56
|
+
let response;
|
|
57
|
+
if (this.provider.chat) {
|
|
58
|
+
response = await this.provider.chat([
|
|
59
|
+
{ role: "system", content: systemInstruction },
|
|
60
|
+
{ role: "user", content: userMessage },
|
|
61
|
+
], options);
|
|
62
|
+
}
|
|
63
|
+
else if (this.provider.analyze) {
|
|
64
|
+
// Fallback to analyze if chat not available (unlikely for OpenAI)
|
|
65
|
+
response = await this.provider.analyze(systemInstruction + "\n" + userMessage, options);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Fallback to using translate as a generic prompt runner
|
|
69
|
+
response = await this.provider.translate(systemInstruction + "\n" + userMessage, "System", "JSON");
|
|
70
|
+
}
|
|
71
|
+
// Clean up response if it contains markdown code blocks
|
|
72
|
+
const cleanResponse = response.replace(/```json\n?|\n?```/g, "").trim();
|
|
73
|
+
const result = JSON.parse(cleanResponse);
|
|
74
|
+
return {
|
|
75
|
+
passed: result.passed,
|
|
76
|
+
score: result.score,
|
|
77
|
+
reasoning: result.reasoning || "No reasoning provided",
|
|
78
|
+
suggestions: result.suggestions,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.warn("Tone verification failed:", error);
|
|
83
|
+
// Fail open - if verification fails, we don't block the translation
|
|
84
|
+
return {
|
|
85
|
+
passed: true,
|
|
86
|
+
score: 0,
|
|
87
|
+
reasoning: `Verification failed: ${error.message}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|