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,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
+ }