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,140 @@
1
+ /**
2
+ * Length Validation Checker.
3
+ * Checks if translated text length is within acceptable bounds relative to source text.
4
+ */
5
+ class LengthChecker {
6
+ defaultConfig;
7
+ constructor() {
8
+ this.defaultConfig = {
9
+ mode: "strict",
10
+ maxDeviation: 0.1,
11
+ minDeviation: -0.1,
12
+ };
13
+ }
14
+ /**
15
+ * Check if translated text length is acceptable.
16
+ */
17
+ checkLength(source, translated, options = {}) {
18
+ try {
19
+ const sourceLength = this.calculateLength(source);
20
+ const translatedLength = this.calculateLength(translated);
21
+ const ratio = sourceLength > 0 ? translatedLength / sourceLength : 1;
22
+ const config = this.getConfig(options);
23
+ const { minRatio, maxRatio } = this.calculateAllowedRange(config);
24
+ if (ratio < minRatio || ratio > maxRatio) {
25
+ return [
26
+ {
27
+ type: "length",
28
+ severity: this.determineSeverity(ratio, minRatio, maxRatio),
29
+ message: this.generateErrorMessage(ratio, config),
30
+ details: {
31
+ mode: config.mode,
32
+ context: config.context,
33
+ targetLang: config.targetLang,
34
+ sourceLength,
35
+ translatedLength,
36
+ ratio: parseFloat(ratio.toFixed(3)),
37
+ allowedRange: {
38
+ min: parseFloat(minRatio.toFixed(2)),
39
+ max: parseFloat(maxRatio.toFixed(2)),
40
+ },
41
+ deviation: parseFloat((ratio - 1).toFixed(3)),
42
+ },
43
+ },
44
+ ];
45
+ }
46
+ return [];
47
+ }
48
+ catch (error) {
49
+ console.error("Length check failed:", error);
50
+ return [
51
+ {
52
+ type: "length",
53
+ severity: "error",
54
+ message: "Length validation failed due to technical error",
55
+ details: { error: error.message },
56
+ },
57
+ ];
58
+ }
59
+ }
60
+ /**
61
+ * Calculate length of text ignoring whitespace.
62
+ */
63
+ calculateLength(text) {
64
+ if (!text)
65
+ return 0;
66
+ return text.replace(/\s+/g, "").length;
67
+ }
68
+ /**
69
+ * Get configuration for length check.
70
+ */
71
+ getConfig(options) {
72
+ const lengthControl = options.lengthControl || {};
73
+ const mode = lengthControl.mode || this.defaultConfig.mode;
74
+ const targetLang = options.targetLang || "en";
75
+ const context = options.detectedContext?.category || "general";
76
+ if (mode === "smart") {
77
+ return this.getSmartModeConfig(lengthControl, targetLang, context);
78
+ }
79
+ return this.getStandardModeConfig(lengthControl, mode, targetLang, context);
80
+ }
81
+ /**
82
+ * Get configuration for smart mode.
83
+ */
84
+ getSmartModeConfig(lengthControl, targetLang, context) {
85
+ const smartRules = lengthControl.rules?.smart || {};
86
+ const langRules = smartRules.byLanguage?.[targetLang] || {};
87
+ const contextRules = smartRules.byContext?.[context] || {};
88
+ const defaultValue = smartRules.default || 0.15;
89
+ return {
90
+ mode: "smart",
91
+ targetLang,
92
+ context,
93
+ maxDeviation: Math.min(langRules.max ?? defaultValue, contextRules.max ?? defaultValue),
94
+ minDeviation: Math.max(langRules.min ?? -defaultValue, contextRules.min ?? -defaultValue),
95
+ };
96
+ }
97
+ /**
98
+ * Get configuration for standard mode.
99
+ */
100
+ getStandardModeConfig(lengthControl, mode, targetLang, context) {
101
+ const deviation = lengthControl.rules?.[mode] || this.defaultConfig.maxDeviation;
102
+ return {
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ mode: mode,
105
+ targetLang,
106
+ context,
107
+ maxDeviation: deviation,
108
+ minDeviation: -deviation,
109
+ };
110
+ }
111
+ /**
112
+ * Calculate allowed length ratio range.
113
+ */
114
+ calculateAllowedRange(config) {
115
+ return {
116
+ minRatio: 1 + config.minDeviation,
117
+ maxRatio: 1 + config.maxDeviation,
118
+ };
119
+ }
120
+ /**
121
+ * Determine severity of length mismatch.
122
+ */
123
+ determineSeverity(ratio, minRatio, maxRatio) {
124
+ const deviation = Math.abs(ratio - 1);
125
+ if (deviation > Math.max(Math.abs(minRatio - 1), Math.abs(maxRatio - 1)) * 1.5) {
126
+ return "critical";
127
+ }
128
+ return "warning";
129
+ }
130
+ /**
131
+ * Generate error message for length mismatch.
132
+ */
133
+ generateErrorMessage(ratio, config) {
134
+ const percentage = Math.abs((ratio - 1) * 100).toFixed(1);
135
+ const direction = ratio > 1 ? "longer" : "shorter";
136
+ return (`Translation is ${percentage}% ${direction} than source text ` +
137
+ `[${(config.targetLang || "").toUpperCase()}, ${config.context}]`);
138
+ }
139
+ }
140
+ export default LengthChecker;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Markdown Preservation Checker.
3
+ * Ensures markdown formatting is preserved during translation.
4
+ */
5
+ class MarkdownChecker {
6
+ markdownPatterns;
7
+ constructor() {
8
+ this.markdownPatterns = {
9
+ // Headers
10
+ headers: /^#{1,6}\s+/gm,
11
+ // Bold
12
+ bold: /\*\*(.+?)\*\*/g,
13
+ // Italic
14
+ italic: /\*(.+?)\*|_(.+?)_/g,
15
+ // Links
16
+ links: /\[([^\]]+)\]\(([^)]+)\)/g,
17
+ // Images
18
+ images: /!\[([^\]]*)\]\(([^)]+)\)/g,
19
+ // Code inline
20
+ inlineCode: /`([^`]+)`/g,
21
+ // Lists
22
+ unorderedLists: /^[*+-]\s+/gm,
23
+ orderedLists: /^\d+\.\s+/gm,
24
+ // Blockquotes
25
+ blockquotes: /^>\s+/gm,
26
+ // Horizontal rules
27
+ horizontalRules: /^(---|\*\*\*|___)$/gm,
28
+ };
29
+ }
30
+ /**
31
+ * Check if markdown is preserved between source and translation.
32
+ */
33
+ checkMarkdownPreservation(sourceText, translatedText) {
34
+ const issues = [];
35
+ // Check each markdown pattern
36
+ for (const [patternName, pattern] of Object.entries(this.markdownPatterns)) {
37
+ const sourceMatches = (sourceText.match(pattern) || []).length;
38
+ const translatedMatches = (translatedText.match(pattern) || []).length;
39
+ if (sourceMatches !== translatedMatches) {
40
+ issues.push({
41
+ type: "markdown-preservation",
42
+ severity: "error",
43
+ pattern: patternName,
44
+ message: `Markdown ${patternName} count mismatch: source has ${sourceMatches}, translation has ${translatedMatches}`,
45
+ details: {
46
+ sourceCount: sourceMatches,
47
+ translatedCount: translatedMatches,
48
+ },
49
+ });
50
+ }
51
+ }
52
+ // Check for broken markdown links
53
+ const brokenLinks = this.checkBrokenMarkdownLinks(translatedText);
54
+ issues.push(...brokenLinks);
55
+ return issues;
56
+ }
57
+ /**
58
+ * Check for broken markdown links.
59
+ */
60
+ checkBrokenMarkdownLinks(text) {
61
+ const issues = [];
62
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
63
+ const matches = text.matchAll(linkPattern);
64
+ for (const match of matches) {
65
+ const [fullMatch, linkText, url] = match;
66
+ // Check for common issues
67
+ if (!linkText.trim()) {
68
+ issues.push({
69
+ type: "markdown-link",
70
+ severity: "warning",
71
+ message: `Empty link text in: "${fullMatch}"`,
72
+ });
73
+ }
74
+ if (!url.trim()) {
75
+ issues.push({
76
+ type: "markdown-link",
77
+ severity: "error",
78
+ message: `Empty URL in: "${fullMatch}"`,
79
+ });
80
+ }
81
+ // Check for spaces in URL (common translation error)
82
+ if (url.includes(" ")) {
83
+ issues.push({
84
+ type: "markdown-link",
85
+ severity: "warning",
86
+ message: `URL contains spaces: "${url}"`,
87
+ });
88
+ }
89
+ }
90
+ return issues;
91
+ }
92
+ /**
93
+ * Fix markdown preservation issues.
94
+ */
95
+ fixMarkdownPreservation(sourceText, translatedText) {
96
+ let fixedText = translatedText;
97
+ const foundIssues = [];
98
+ const appliedFixes = [];
99
+ // Extract markdown elements from source
100
+ const sourceMarkdown = this.extractMarkdownElements(sourceText);
101
+ // Try to restore missing markdown
102
+ for (const [type, elements] of Object.entries(sourceMarkdown)) {
103
+ if (elements.length > 0) {
104
+ const result = this.restoreMarkdownElements(fixedText, elements, type);
105
+ fixedText = result.text;
106
+ foundIssues.push(...result.foundIssues);
107
+ // fixes are not really applied in restoreMarkdownElements for now
108
+ }
109
+ }
110
+ // Fix broken links
111
+ const linkResult = this.fixBrokenLinks(fixedText);
112
+ fixedText = linkResult.text;
113
+ foundIssues.push(...linkResult.foundIssues);
114
+ appliedFixes.push(...linkResult.appliedFixes);
115
+ return { text: fixedText, foundIssues, appliedFixes };
116
+ }
117
+ /**
118
+ * Extract markdown elements from text.
119
+ */
120
+ extractMarkdownElements(text) {
121
+ const elements = {
122
+ headers: [],
123
+ bold: [],
124
+ italic: [],
125
+ links: [],
126
+ images: [],
127
+ inlineCode: [],
128
+ lists: [],
129
+ };
130
+ // Extract headers
131
+ const headerMatches = text.matchAll(this.markdownPatterns.headers);
132
+ for (const match of headerMatches) {
133
+ elements.headers.push(match[0]);
134
+ }
135
+ // Extract bold text
136
+ const boldMatches = text.matchAll(this.markdownPatterns.bold);
137
+ for (const match of boldMatches) {
138
+ elements.bold.push(match[0]);
139
+ }
140
+ // Extract links
141
+ const linkMatches = text.matchAll(this.markdownPatterns.links);
142
+ for (const match of linkMatches) {
143
+ elements.links.push({
144
+ full: match[0],
145
+ text: match[1],
146
+ url: match[2],
147
+ });
148
+ }
149
+ // Extract inline code
150
+ const codeMatches = text.matchAll(this.markdownPatterns.inlineCode);
151
+ for (const match of codeMatches) {
152
+ elements.inlineCode.push(match[0]);
153
+ }
154
+ return elements;
155
+ }
156
+ /**
157
+ * Restore markdown elements that were lost in translation.
158
+ */
159
+ restoreMarkdownElements(text, elements, type) {
160
+ const foundIssues = [];
161
+ const appliedFixes = [];
162
+ const fixedText = text;
163
+ // For now, we'll just report issues
164
+ // Full restoration would require more context about where to place elements
165
+ if (elements.length > 0) {
166
+ const currentCount = (fixedText.match(this.markdownPatterns[type]) || []).length;
167
+ if (currentCount < elements.length) {
168
+ foundIssues.push({
169
+ type: "markdown-missing",
170
+ severity: "warning", // Added severity
171
+ message: `Missing ${elements.length - currentCount} ${type} elements`,
172
+ });
173
+ // Note: Full restoration is complex and context-dependent
174
+ }
175
+ }
176
+ return { text: fixedText, foundIssues, appliedFixes };
177
+ }
178
+ /**
179
+ * Fix broken markdown links.
180
+ */
181
+ fixBrokenLinks(text) {
182
+ const foundIssues = [];
183
+ const appliedFixes = [];
184
+ let fixedText = text;
185
+ // Fix spaces in URLs
186
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
187
+ fixedText = fixedText.replace(linkPattern, (match, linkText, url) => {
188
+ let fixedUrl = url;
189
+ let modified = false;
190
+ // Remove spaces from URL
191
+ if (url.includes(" ")) {
192
+ fixedUrl = url.replace(/\s+/g, "-");
193
+ modified = true;
194
+ foundIssues.push({
195
+ type: "markdown-link",
196
+ severity: "warning", // Added severity
197
+ message: `URL had spaces: "${url}"`,
198
+ });
199
+ appliedFixes.push({
200
+ type: "markdown-link",
201
+ message: `Fixed URL spaces: "${url}" → "${fixedUrl}"`,
202
+ });
203
+ }
204
+ return modified ? `[${linkText}](${fixedUrl})` : match;
205
+ });
206
+ return { text: fixedText, foundIssues, appliedFixes };
207
+ }
208
+ /**
209
+ * Check if text contains markdown.
210
+ */
211
+ hasMarkdown(text) {
212
+ for (const pattern of Object.values(this.markdownPatterns)) {
213
+ if (pattern.test(text)) {
214
+ return true;
215
+ }
216
+ }
217
+ return false;
218
+ }
219
+ }
220
+ export default MarkdownChecker;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Placeholder Consistency Checker.
3
+ * Ensures placeholders like {name}, ${value}, %s are preserved correctly.
4
+ */
5
+ class PlaceholderChecker {
6
+ /**
7
+ * Check for placeholder mismatches or corruptions.
8
+ */
9
+ checkPlaceholders(source, translated) {
10
+ const placeholderRegex = /\{[^}]+\}|\$\{[^}]+\}|%[sd]/g;
11
+ const sourcePlaceholders = source.match(placeholderRegex) || [];
12
+ const translatedPlaceholders = translated.match(placeholderRegex) || [];
13
+ const issues = [];
14
+ // Check for missing or extra placeholders
15
+ if (sourcePlaceholders.length !== translatedPlaceholders.length) {
16
+ issues.push({
17
+ type: "placeholder",
18
+ message: "Placeholder count mismatch",
19
+ source: sourcePlaceholders,
20
+ translated: translatedPlaceholders,
21
+ });
22
+ }
23
+ // Check for corrupted placeholders (placeholders that were translated/modified)
24
+ for (const sourcePlaceholder of sourcePlaceholders) {
25
+ if (!translated.includes(sourcePlaceholder)) {
26
+ // Look for potential corrupted versions
27
+ const corruptedPattern = this.findCorruptedPlaceholder(translated, sourcePlaceholder);
28
+ if (corruptedPattern) {
29
+ issues.push({
30
+ type: "placeholder",
31
+ message: `Corrupted placeholder detected: expected '${sourcePlaceholder}' but found '${corruptedPattern}'`,
32
+ source: sourcePlaceholder,
33
+ corrupted: corruptedPattern,
34
+ });
35
+ }
36
+ else {
37
+ issues.push({
38
+ type: "placeholder",
39
+ message: `Missing placeholder: ${sourcePlaceholder}`,
40
+ source: sourcePlaceholder,
41
+ });
42
+ }
43
+ }
44
+ }
45
+ return issues;
46
+ }
47
+ /**
48
+ * Fix corrupted or missing placeholders in translated text.
49
+ */
50
+ fixPlaceholders(source, translated) {
51
+ const placeholderRegex = /\{[^}]+\}|\$\{[^}]+\}|%[sd]/g;
52
+ const sourcePlaceholders = source.match(placeholderRegex) || [];
53
+ let fixedText = translated;
54
+ const foundIssues = [];
55
+ const appliedFixes = [];
56
+ // First, fix corrupted placeholders
57
+ for (const sourcePlaceholder of sourcePlaceholders) {
58
+ if (!fixedText.includes(sourcePlaceholder)) {
59
+ // Try to find and fix corrupted versions
60
+ const corruptedPattern = this.findCorruptedPlaceholder(fixedText, sourcePlaceholder);
61
+ if (corruptedPattern) {
62
+ foundIssues.push({
63
+ type: "placeholder",
64
+ message: `Corrupted placeholder: ${corruptedPattern} → ${sourcePlaceholder}`,
65
+ });
66
+ // Replace the corrupted version with the correct one
67
+ fixedText = fixedText.replace(corruptedPattern, sourcePlaceholder);
68
+ appliedFixes.push({
69
+ type: "placeholder",
70
+ message: `Fixed corrupted placeholder: ${corruptedPattern} → ${sourcePlaceholder}`,
71
+ });
72
+ }
73
+ else {
74
+ // Placeholder is completely missing, try to add it
75
+ foundIssues.push({
76
+ type: "placeholder",
77
+ message: `Missing placeholder: ${sourcePlaceholder}`,
78
+ });
79
+ const possiblePosition = this.findBestPlaceholderPosition(fixedText, source, sourcePlaceholder);
80
+ if (possiblePosition !== -1) {
81
+ fixedText = this.insertPlaceholder(fixedText, sourcePlaceholder, possiblePosition);
82
+ appliedFixes.push({
83
+ type: "placeholder",
84
+ message: `Added missing placeholder: ${sourcePlaceholder}`,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return { text: fixedText, foundIssues, appliedFixes };
91
+ }
92
+ /**
93
+ * Find a corrupted version of a placeholder in translated text.
94
+ */
95
+ findCorruptedPlaceholder(translated, sourcePlaceholder) {
96
+ // Extract the placeholder name (e.g., "message" from "{message}")
97
+ const placeholderName = sourcePlaceholder.slice(1, -1); // Remove { and }
98
+ // Look for common corruption patterns
99
+ const corruptionPatterns = [
100
+ // Pattern: {mesaj}{message} or {mensaje}{message} (translated + original)
101
+ new RegExp(`\\{[^}]*\\}\\{${placeholderName}\\}`, "g"),
102
+ // Pattern: {mesaj} or {mensaje} (just translated)
103
+ new RegExp(`\\{[^}]*${placeholderName.slice(0, -2)}[^}]*\\}`, "g"),
104
+ // Pattern: Bạn{message} or text{message} (text prefixed to placeholder)
105
+ new RegExp(`[^\\s]{2,}\\{${placeholderName}\\}`, "g"),
106
+ // Pattern: {message}được or {message}text (text suffixed to placeholder)
107
+ new RegExp(`\\{${placeholderName}\\}[^\\s]{2,}`, "g"),
108
+ // Pattern: Any placeholder with similar length but different content
109
+ new RegExp(`\\{[^}]{${Math.max(1, placeholderName.length - 2)},${placeholderName.length + 3}}\\}`, "g"),
110
+ ];
111
+ for (const pattern of corruptionPatterns) {
112
+ const matches = translated.match(pattern);
113
+ if (matches && matches.length > 0) {
114
+ // Return the first match that's not the original placeholder
115
+ for (const match of matches) {
116
+ if (match !== sourcePlaceholder) {
117
+ return match;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ /**
125
+ * Find best position to insert a missing placeholder.
126
+ */
127
+ findBestPlaceholderPosition(translated, source, placeholder) {
128
+ const sourcePosition = source.indexOf(placeholder);
129
+ const sourceWords = source.slice(0, sourcePosition).split(" ").length;
130
+ const translatedWords = translated.split(" ");
131
+ return sourceWords >= translatedWords.length
132
+ ? translated.length
133
+ : translatedWords.slice(0, sourceWords).join(" ").length;
134
+ }
135
+ /**
136
+ * Insert placeholder into text at specified position.
137
+ */
138
+ insertPlaceholder(text, placeholder, position) {
139
+ return text.slice(0, position) + placeholder + text.slice(position);
140
+ }
141
+ }
142
+ export default PlaceholderChecker;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Punctuation Consistency Checker.
3
+ * Ensures end punctuation marks are preserved in translation.
4
+ */
5
+ class PunctuationChecker {
6
+ /**
7
+ * Check for punctuation mismatches.
8
+ */
9
+ checkPunctuation(source, translated) {
10
+ const issues = [];
11
+ const endPunctuation = /[.!?]$/;
12
+ if (source.match(endPunctuation) && !translated.match(endPunctuation)) {
13
+ issues.push({
14
+ type: "punctuation",
15
+ message: "Missing punctuation mark",
16
+ });
17
+ }
18
+ return issues;
19
+ }
20
+ /**
21
+ * Fix missing punctuation marks.
22
+ */
23
+ fixPunctuation(source, translated) {
24
+ const endPunctuation = /[.!?]$/;
25
+ let fixedText = translated;
26
+ const foundIssues = [];
27
+ const appliedFixes = [];
28
+ const sourceEndsWithPunctuation = source.match(endPunctuation);
29
+ const translatedEndsWithPunctuation = translated.match(endPunctuation);
30
+ if (sourceEndsWithPunctuation && !translatedEndsWithPunctuation) {
31
+ foundIssues.push({
32
+ type: "punctuation",
33
+ message: "Missing punctuation mark",
34
+ });
35
+ fixedText = this.addPunctuation(fixedText, sourceEndsWithPunctuation[0]);
36
+ appliedFixes.push({
37
+ type: "punctuation",
38
+ message: `Added punctuation mark: ${sourceEndsWithPunctuation[0]}`,
39
+ });
40
+ }
41
+ return { text: fixedText, foundIssues, appliedFixes };
42
+ }
43
+ /**
44
+ * Add punctuation to the end of text.
45
+ */
46
+ addPunctuation(text, punctuation) {
47
+ return text.trim() + punctuation;
48
+ }
49
+ }
50
+ export default PunctuationChecker;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Quote Balance Checker.
3
+ * Ensures quotes (single, double, guillemets, etc.) are balanced in translation.
4
+ */
5
+ class QuoteBalanceChecker {
6
+ /**
7
+ * Check for unbalanced quotes.
8
+ */
9
+ checkQuoteBalance(text) {
10
+ const issues = [];
11
+ // Check single quotes
12
+ const singleQuotes = this._countQuotes(text, "'");
13
+ if (singleQuotes.unbalanced) {
14
+ issues.push({
15
+ type: "unbalanced-quotes",
16
+ severity: "error",
17
+ message: `Unbalanced single quotes: ${singleQuotes.count} found`,
18
+ char: "'",
19
+ });
20
+ }
21
+ // Check double quotes
22
+ const doubleQuotes = this._countQuotes(text, '"');
23
+ if (doubleQuotes.unbalanced) {
24
+ issues.push({
25
+ type: "unbalanced-quotes",
26
+ severity: "error",
27
+ message: `Unbalanced double quotes: ${doubleQuotes.count} found`,
28
+ char: '"',
29
+ });
30
+ }
31
+ // Check guillemets (French quotes)
32
+ const guillemetsOpen = (text.match(/«/g) || []).length;
33
+ const guillemetsClose = (text.match(/»/g) || []).length;
34
+ if (guillemetsOpen !== guillemetsClose) {
35
+ issues.push({
36
+ type: "unbalanced-quotes",
37
+ severity: "error",
38
+ message: `Unbalanced guillemets: ${guillemetsOpen} « vs ${guillemetsClose} »`,
39
+ char: "«»",
40
+ });
41
+ }
42
+ // Check German quotes
43
+ const germanQuotesOpen = (text.match(/„/g) || []).length;
44
+ const germanQuotesClose = (text.match(/"/g) || []).length;
45
+ if (germanQuotesOpen !== germanQuotesClose) {
46
+ issues.push({
47
+ type: "unbalanced-quotes",
48
+ severity: "warning",
49
+ message: `Unbalanced German quotes: ${germanQuotesOpen} „ vs ${germanQuotesClose} "`,
50
+ char: '„"', // Escaped quote
51
+ });
52
+ }
53
+ return issues;
54
+ }
55
+ /**
56
+ * Count occurrences of a quote character.
57
+ */
58
+ _countQuotes(text, quoteChar) {
59
+ const count = (text.match(new RegExp(`\\${quoteChar}`, "g")) || []).length;
60
+ return {
61
+ count,
62
+ unbalanced: count % 2 !== 0,
63
+ };
64
+ }
65
+ /**
66
+ * Fix unbalanced quotes.
67
+ */
68
+ fixQuoteBalance(text) {
69
+ let fixedText = text;
70
+ const foundIssues = [];
71
+ const appliedFixes = [];
72
+ // Fix single quotes
73
+ const singleQuoteCheck = this._countQuotes(fixedText, "'");
74
+ if (singleQuoteCheck.unbalanced) {
75
+ foundIssues.push({
76
+ type: "unbalanced-quotes",
77
+ severity: "error", // Added severity
78
+ message: `Found ${singleQuoteCheck.count} single quotes (odd number)`,
79
+ });
80
+ // Try to fix by adding missing closing quote at the end
81
+ if (fixedText.includes("'")) {
82
+ fixedText = fixedText + "'";
83
+ appliedFixes.push({
84
+ type: "quote-balance",
85
+ message: "Added missing closing single quote at the end",
86
+ });
87
+ }
88
+ }
89
+ // Fix double quotes
90
+ const doubleQuoteCheck = this._countQuotes(fixedText, '"');
91
+ if (doubleQuoteCheck.unbalanced) {
92
+ foundIssues.push({
93
+ type: "unbalanced-quotes",
94
+ severity: "error", // Added severity
95
+ message: `Found ${doubleQuoteCheck.count} double quotes (odd number)`,
96
+ });
97
+ // Try to fix by adding missing closing quote at the end
98
+ if (fixedText.includes('"')) {
99
+ fixedText = fixedText + '"';
100
+ appliedFixes.push({
101
+ type: "quote-balance",
102
+ message: "Added missing closing double quote at the end",
103
+ });
104
+ }
105
+ }
106
+ // Fix guillemets
107
+ const guillemetsOpen = (fixedText.match(/«/g) || []).length;
108
+ const guillemetsClose = (fixedText.match(/»/g) || []).length;
109
+ if (guillemetsOpen > guillemetsClose) {
110
+ fixedText = fixedText + "»";
111
+ foundIssues.push({
112
+ type: "unbalanced-quotes",
113
+ severity: "error", // Added severity
114
+ message: "Found unbalanced guillemets",
115
+ });
116
+ appliedFixes.push({
117
+ type: "quote-balance",
118
+ message: "Added missing closing guillemet",
119
+ });
120
+ }
121
+ return { text: fixedText, foundIssues, appliedFixes };
122
+ }
123
+ /**
124
+ * Validate that quotes in source and translation match in pattern.
125
+ */
126
+ validateQuoteConsistency(source, translated) {
127
+ const issues = [];
128
+ // Check if source has quotes but translation doesn't (or vice versa)
129
+ const sourceHasSingleQuotes = source.includes("'");
130
+ const translatedHasSingleQuotes = translated.includes("'");
131
+ const translatedHasDoubleQuotes = translated.includes('"');
132
+ // If source has quotes, translation should maintain similar quoting
133
+ if (sourceHasSingleQuotes && !translatedHasSingleQuotes && !translatedHasDoubleQuotes) {
134
+ issues.push({
135
+ type: "quote-consistency",
136
+ severity: "warning",
137
+ message: "Source has quotes but translation doesn't",
138
+ });
139
+ }
140
+ return issues;
141
+ }
142
+ }
143
+ export default QuoteBalanceChecker;