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,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress tracking for translation operations
|
|
3
|
+
*/
|
|
4
|
+
class ProgressTracker {
|
|
5
|
+
logToConsole;
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
logFrequency;
|
|
8
|
+
_isUpdating;
|
|
9
|
+
_pendingUpdates;
|
|
10
|
+
_spinnerInterval;
|
|
11
|
+
_spinnerIndex;
|
|
12
|
+
_spinnerFrames;
|
|
13
|
+
total;
|
|
14
|
+
completed;
|
|
15
|
+
success;
|
|
16
|
+
failed;
|
|
17
|
+
startTime;
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
19
|
+
endTime;
|
|
20
|
+
lastUpdateTime;
|
|
21
|
+
recentOperationTimes;
|
|
22
|
+
language;
|
|
23
|
+
isCompleted;
|
|
24
|
+
statistics;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.logToConsole = options.logToConsole !== false;
|
|
27
|
+
this.logFrequency = options.logFrequency || 1;
|
|
28
|
+
this._isUpdating = false;
|
|
29
|
+
this._pendingUpdates = [];
|
|
30
|
+
this._spinnerInterval = null;
|
|
31
|
+
this._spinnerIndex = 0;
|
|
32
|
+
this._spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
33
|
+
// Initialize fields to satisfy TypeScript before calling reset()
|
|
34
|
+
this.total = 0;
|
|
35
|
+
this.completed = 0;
|
|
36
|
+
this.success = 0;
|
|
37
|
+
this.failed = 0;
|
|
38
|
+
this.startTime = null;
|
|
39
|
+
this.endTime = null;
|
|
40
|
+
this.lastUpdateTime = null;
|
|
41
|
+
this.recentOperationTimes = [];
|
|
42
|
+
this.language = null;
|
|
43
|
+
this.isCompleted = false;
|
|
44
|
+
this.statistics = {
|
|
45
|
+
avgTimePerItem: 0,
|
|
46
|
+
totalTime: 0,
|
|
47
|
+
estimatedTimeRemaining: 0,
|
|
48
|
+
itemsPerSecond: 0,
|
|
49
|
+
percentComplete: 0,
|
|
50
|
+
successRate: 0,
|
|
51
|
+
};
|
|
52
|
+
this.reset();
|
|
53
|
+
}
|
|
54
|
+
reset() {
|
|
55
|
+
// Stop any existing spinner
|
|
56
|
+
this._stopSpinner();
|
|
57
|
+
this.total = 0;
|
|
58
|
+
this.completed = 0;
|
|
59
|
+
this.success = 0;
|
|
60
|
+
this.failed = 0;
|
|
61
|
+
this.startTime = null;
|
|
62
|
+
this.endTime = null;
|
|
63
|
+
this.lastUpdateTime = null;
|
|
64
|
+
this.recentOperationTimes = [];
|
|
65
|
+
this.language = null;
|
|
66
|
+
this.isCompleted = false;
|
|
67
|
+
this._isUpdating = false;
|
|
68
|
+
this._pendingUpdates = [];
|
|
69
|
+
this.statistics = {
|
|
70
|
+
avgTimePerItem: 0,
|
|
71
|
+
totalTime: 0,
|
|
72
|
+
estimatedTimeRemaining: 0,
|
|
73
|
+
itemsPerSecond: 0,
|
|
74
|
+
percentComplete: 0,
|
|
75
|
+
successRate: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
start(total, language = null) {
|
|
79
|
+
this.reset();
|
|
80
|
+
this.total = Math.max(0, total);
|
|
81
|
+
this.startTime = Date.now();
|
|
82
|
+
this.lastUpdateTime = Date.now();
|
|
83
|
+
this.language = language;
|
|
84
|
+
this.isCompleted = false;
|
|
85
|
+
if (this.logToConsole && this.total > 0) {
|
|
86
|
+
// Start spinner animation
|
|
87
|
+
this._startSpinner();
|
|
88
|
+
this._renderProgress();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
increment(type = "success") {
|
|
92
|
+
if (!this.startTime) {
|
|
93
|
+
throw new Error("Progress tracker not started");
|
|
94
|
+
}
|
|
95
|
+
if (this.isCompleted) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (this._isUpdating) {
|
|
99
|
+
this._pendingUpdates.push({ type, timestamp: Date.now() });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this._processUpdate(type);
|
|
103
|
+
}
|
|
104
|
+
_processUpdate(type) {
|
|
105
|
+
this._isUpdating = true;
|
|
106
|
+
try {
|
|
107
|
+
if (this.isCompleted) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const previousCompleted = this.completed;
|
|
111
|
+
this.completed = Math.min(this.total, this.completed + 1);
|
|
112
|
+
if (this.completed > previousCompleted) {
|
|
113
|
+
if (type === "success") {
|
|
114
|
+
this.success = Math.min(this.completed, this.success + 1);
|
|
115
|
+
}
|
|
116
|
+
else if (type === "failed") {
|
|
117
|
+
this.failed = Math.min(this.completed, this.failed + 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this._ensureDataConsistency();
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const timeSinceLastUpdate = now - (this.lastUpdateTime || now);
|
|
123
|
+
this.lastUpdateTime = now;
|
|
124
|
+
const MAX_OPERATION_TIMES = 10;
|
|
125
|
+
this.recentOperationTimes.push(timeSinceLastUpdate);
|
|
126
|
+
if (this.recentOperationTimes.length > MAX_OPERATION_TIMES) {
|
|
127
|
+
this.recentOperationTimes.shift(); // Keep only last 10
|
|
128
|
+
}
|
|
129
|
+
this._updateStatistics();
|
|
130
|
+
if (this.logToConsole && this._shouldLog()) {
|
|
131
|
+
this._renderProgress();
|
|
132
|
+
}
|
|
133
|
+
if (this.completed >= this.total && !this.isCompleted) {
|
|
134
|
+
this.endTime = Date.now();
|
|
135
|
+
this.isCompleted = true;
|
|
136
|
+
this._stopSpinner();
|
|
137
|
+
this._renderProgress(true); // Final render
|
|
138
|
+
if (this.logToConsole) {
|
|
139
|
+
this._finalReport();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this._isUpdating = false;
|
|
145
|
+
this._processPendingUpdates();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
_ensureDataConsistency() {
|
|
149
|
+
this.completed = Math.min(this.total, Math.max(0, this.completed));
|
|
150
|
+
this.success = Math.min(this.completed, Math.max(0, this.success));
|
|
151
|
+
this.failed = Math.min(this.completed, Math.max(0, this.failed));
|
|
152
|
+
const totalCounted = this.success + this.failed;
|
|
153
|
+
if (totalCounted > this.completed) {
|
|
154
|
+
if (this.success > this.completed) {
|
|
155
|
+
this.success = this.completed;
|
|
156
|
+
this.failed = 0;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
this.failed = this.completed - this.success;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
_processPendingUpdates() {
|
|
164
|
+
if (!this._pendingUpdates ||
|
|
165
|
+
!Array.isArray(this._pendingUpdates) ||
|
|
166
|
+
this._pendingUpdates.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setImmediate(() => {
|
|
170
|
+
while (this._pendingUpdates && this._pendingUpdates.length > 0 && !this._isUpdating) {
|
|
171
|
+
const updateItem = this._pendingUpdates.shift();
|
|
172
|
+
if (updateItem && typeof updateItem === "object" && updateItem.type) {
|
|
173
|
+
this._processUpdate(updateItem.type);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
_shouldLog() {
|
|
179
|
+
if (!this.logToConsole || this.total === 0)
|
|
180
|
+
return false;
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
_startSpinner() {
|
|
184
|
+
if (this._spinnerInterval)
|
|
185
|
+
return;
|
|
186
|
+
this._spinnerInterval = setInterval(() => {
|
|
187
|
+
if (!this.isCompleted) {
|
|
188
|
+
this._spinnerIndex = (this._spinnerIndex + 1) % this._spinnerFrames.length;
|
|
189
|
+
this._renderProgress();
|
|
190
|
+
}
|
|
191
|
+
}, 80);
|
|
192
|
+
}
|
|
193
|
+
_stopSpinner() {
|
|
194
|
+
if (this._spinnerInterval) {
|
|
195
|
+
clearInterval(this._spinnerInterval);
|
|
196
|
+
this._spinnerInterval = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
_renderProgress(final = false) {
|
|
200
|
+
const percent = this.total > 0 ? (this.completed / this.total) * 100 : 0;
|
|
201
|
+
const langInfo = this.language ? `[${this.language}] ` : "";
|
|
202
|
+
const barWidth = 20;
|
|
203
|
+
const filledWidth = Math.max(0, Math.min(barWidth, Math.round((this.completed / this.total) * barWidth)));
|
|
204
|
+
const emptyWidth = barWidth - filledWidth;
|
|
205
|
+
const bar = "[" + "█".repeat(filledWidth) + "═".repeat(emptyWidth) + "]";
|
|
206
|
+
let etaText = "";
|
|
207
|
+
if (this.completed > 0 && this.completed < this.total) {
|
|
208
|
+
const eta = this.statistics.estimatedTimeRemaining;
|
|
209
|
+
if (eta < 60) {
|
|
210
|
+
etaText = ` | ETA: ${Math.round(eta)}s`;
|
|
211
|
+
}
|
|
212
|
+
else if (eta < 3600) {
|
|
213
|
+
etaText = ` | ETA: ${Math.floor(eta / 60)}m ${Math.round(eta % 60)}s`;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
etaText = ` | ETA: ${Math.floor(eta / 3600)}h ${Math.floor((eta % 3600) / 60)}m`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const spinner = final ? "[OK]" : this._spinnerFrames[this._spinnerIndex];
|
|
220
|
+
const text = `${spinner} ${langInfo}${bar} ${percent.toFixed(1)}% | ${this.completed}/${this.total} items | OK ${this.success} | ERR ${this.failed}${etaText}`;
|
|
221
|
+
process.stderr.write("\r\x1b[K" + text);
|
|
222
|
+
if (final) {
|
|
223
|
+
process.stderr.write("\n");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
_updateStatistics() {
|
|
227
|
+
if (!this.startTime)
|
|
228
|
+
return;
|
|
229
|
+
const elapsed = Date.now() - this.startTime;
|
|
230
|
+
const averageTime = this.completed > 0 ? elapsed / this.completed : 0;
|
|
231
|
+
let recentAverage = 0;
|
|
232
|
+
if (this.recentOperationTimes.length >= 3) {
|
|
233
|
+
recentAverage =
|
|
234
|
+
this.recentOperationTimes.reduce((a, b) => a + b, 0) /
|
|
235
|
+
this.recentOperationTimes.length;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
recentAverage = averageTime;
|
|
239
|
+
}
|
|
240
|
+
const remaining = Math.max(0, this.total - this.completed);
|
|
241
|
+
const estimatedTimeRemaining = remaining * Math.min(averageTime, recentAverage);
|
|
242
|
+
const percentComplete = Math.min(100, Math.max(0, (this.completed / this.total) * 100 || 0));
|
|
243
|
+
const successRate = this.completed > 0
|
|
244
|
+
? Math.min(100, Math.max(0, (this.success / this.completed) * 100))
|
|
245
|
+
: 0;
|
|
246
|
+
this.statistics = {
|
|
247
|
+
avgTimePerItem: averageTime,
|
|
248
|
+
recentAvgTimePerItem: recentAverage,
|
|
249
|
+
totalTime: elapsed / 1000, // in seconds
|
|
250
|
+
estimatedTimeRemaining: estimatedTimeRemaining / 1000, // in seconds
|
|
251
|
+
itemsPerSecond: this.completed > 0 ? this.completed / (elapsed / 1000) : 0,
|
|
252
|
+
percentComplete: percentComplete,
|
|
253
|
+
successRate: successRate,
|
|
254
|
+
startTime: new Date(this.startTime).toISOString(),
|
|
255
|
+
estimatedEndTime: new Date(Date.now() + estimatedTimeRemaining).toISOString(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
_finalReport() {
|
|
259
|
+
// Progress bar already displays the summary visually
|
|
260
|
+
// Remove duplicate console output to keep it clean
|
|
261
|
+
}
|
|
262
|
+
getStatus() {
|
|
263
|
+
return {
|
|
264
|
+
total: this.total,
|
|
265
|
+
completed: this.completed,
|
|
266
|
+
success: this.success,
|
|
267
|
+
failed: this.failed,
|
|
268
|
+
language: this.language,
|
|
269
|
+
inProgress: !!(this.startTime && !this.isCompleted),
|
|
270
|
+
statistics: { ...this.statistics },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
setLogToConsole(value) {
|
|
274
|
+
this.logToConsole = value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export default ProgressTracker;
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt template utilities for translation and analysis
|
|
3
|
+
*/
|
|
4
|
+
const getLengthInstructions = (options) => {
|
|
5
|
+
// ... existing getLengthInstructions ...
|
|
6
|
+
if (!options || typeof options !== "object") {
|
|
7
|
+
console.warn("Invalid options provided to getLengthInstructions, using defaults");
|
|
8
|
+
options = {};
|
|
9
|
+
}
|
|
10
|
+
const { mode = "smart", lengthControl, targetLang, detectedContext } = options;
|
|
11
|
+
if (!targetLang || typeof targetLang !== "string") {
|
|
12
|
+
// Just return basic length instructions without failing
|
|
13
|
+
return "TRANSLATION LENGTH: Keep translation concise and natural.";
|
|
14
|
+
}
|
|
15
|
+
const context = detectedContext?.category || "general";
|
|
16
|
+
if (mode === "smart" && lengthControl?.rules?.smart) {
|
|
17
|
+
const langRules = lengthControl.rules.smart.byLanguage?.[targetLang] || {};
|
|
18
|
+
const contextRules = lengthControl.rules.smart.byContext?.[context] || {};
|
|
19
|
+
const langMax = typeof langRules.max === "number" ? langRules.max : 0.15;
|
|
20
|
+
const contextMax = typeof contextRules.max === "number" ? contextRules.max : 0.15;
|
|
21
|
+
return `TRANSLATION LENGTH REQUIREMENTS [${targetLang}]:
|
|
22
|
+
1. Maximum allowed length: ${Math.round(langMax * 100)}% longer than source
|
|
23
|
+
2. Context-specific [${context}] limit: ${Math.round(contextMax * 100)}% longer than source
|
|
24
|
+
3. Shorter translations are preferred when possible
|
|
25
|
+
4. Maintain semantic completeness while being concise`;
|
|
26
|
+
}
|
|
27
|
+
const templates = {
|
|
28
|
+
strict: () => {
|
|
29
|
+
const strictLimit = lengthControl?.rules?.strict;
|
|
30
|
+
const limit = typeof strictLimit === "number" ? strictLimit : 1.0;
|
|
31
|
+
return `CRITICAL: Translation must not exceed ${Math.round(limit * 100)}% of source length. Prefer shorter translations.`;
|
|
32
|
+
},
|
|
33
|
+
flexible: () => {
|
|
34
|
+
const flexibleLimit = lengthControl?.rules?.flexible;
|
|
35
|
+
const limit = typeof flexibleLimit === "number" ? flexibleLimit : 1.2;
|
|
36
|
+
return `IMPORTANT: Keep translation concise. Target length should not exceed source length by more than ${Math.round(limit * 100)}%.`;
|
|
37
|
+
},
|
|
38
|
+
exact: () => {
|
|
39
|
+
const exactLimit = lengthControl?.rules?.exact;
|
|
40
|
+
const limit = typeof exactLimit === "number" ? exactLimit : 1.05;
|
|
41
|
+
return `STRICT: Translation must closely match source length (max ${Math.round(limit * 100)}% deviation).`;
|
|
42
|
+
},
|
|
43
|
+
relaxed: () => {
|
|
44
|
+
const relaxedLimit = lengthControl?.rules?.relaxed;
|
|
45
|
+
const limit = typeof relaxedLimit === "number" ? relaxedLimit : 1.5;
|
|
46
|
+
return `GUIDELINE: Translation should be concise but can be up to ${Math.round(limit * 100)}% longer if needed.`;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const templateFn = templates[mode];
|
|
50
|
+
if (typeof templateFn === "function") {
|
|
51
|
+
try {
|
|
52
|
+
return templateFn();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.warn(`Error generating length template for mode ${mode}:`, error.message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return "TRANSLATION LENGTH: Keep translation concise and natural.";
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Base translation prompt template
|
|
62
|
+
*/
|
|
63
|
+
const baseTranslationPromptTemplate = (sourceLang, targetLang, text, options = {}) => {
|
|
64
|
+
if (!sourceLang || typeof sourceLang !== "string") {
|
|
65
|
+
console.warn("Invalid sourceLang provided to baseTranslationPromptTemplate");
|
|
66
|
+
sourceLang = "en";
|
|
67
|
+
}
|
|
68
|
+
if (!targetLang || typeof targetLang !== "string") {
|
|
69
|
+
console.warn("Invalid targetLang provided to baseTranslationPromptTemplate");
|
|
70
|
+
targetLang = "es";
|
|
71
|
+
}
|
|
72
|
+
if (typeof text !== "string") {
|
|
73
|
+
console.warn("Invalid text provided to baseTranslationPromptTemplate");
|
|
74
|
+
text = "";
|
|
75
|
+
}
|
|
76
|
+
if (!options || typeof options !== "object") {
|
|
77
|
+
console.warn("Invalid options provided to baseTranslationPromptTemplate, using defaults");
|
|
78
|
+
options = {};
|
|
79
|
+
}
|
|
80
|
+
const context = options.detectedContext && typeof options.detectedContext === "object"
|
|
81
|
+
? options.detectedContext
|
|
82
|
+
: {
|
|
83
|
+
category: "general",
|
|
84
|
+
confidence: 1.0,
|
|
85
|
+
prompt: "Provide a natural translation",
|
|
86
|
+
};
|
|
87
|
+
const safeContext = {
|
|
88
|
+
category: context.category || "general",
|
|
89
|
+
confidence: typeof context.confidence === "number" ? context.confidence : 1.0,
|
|
90
|
+
prompt: context.prompt || "Provide a natural translation",
|
|
91
|
+
existingTranslation: context.existingTranslation,
|
|
92
|
+
similarTranslation: context.similarTranslation,
|
|
93
|
+
};
|
|
94
|
+
const lengthInstructions = getLengthInstructions({
|
|
95
|
+
...options,
|
|
96
|
+
targetLang: targetLang, // Pass targetLang explicitly
|
|
97
|
+
detectedContext: safeContext, // Also pass context
|
|
98
|
+
});
|
|
99
|
+
let additionalInstructions = "";
|
|
100
|
+
if (safeContext.existingTranslation && typeof safeContext.existingTranslation === "string") {
|
|
101
|
+
const truncatedTranslation = safeContext.existingTranslation.length > 200
|
|
102
|
+
? safeContext.existingTranslation.substring(0, 200) + "..."
|
|
103
|
+
: safeContext.existingTranslation;
|
|
104
|
+
additionalInstructions = `\nREVISION REQUEST: The existing translation "${truncatedTranslation}" has length issues. Please provide a corrected version that matches the source text length requirements.`;
|
|
105
|
+
}
|
|
106
|
+
else if (safeContext.similarTranslation) {
|
|
107
|
+
const { source, target, similarity } = safeContext.similarTranslation;
|
|
108
|
+
const simPercent = Math.round(similarity * 100);
|
|
109
|
+
additionalInstructions = `\nREFERENCE CONTEXT (${simPercent}% Match):
|
|
110
|
+
Previously, a similar phrase: "${source}"
|
|
111
|
+
Was translated as: "${target}"
|
|
112
|
+
Please maintain consistency with this style and vocabulary where appropriate.`;
|
|
113
|
+
}
|
|
114
|
+
const formality = options.styleGuide?.formality || "neutral";
|
|
115
|
+
const toneOfVoice = options.styleGuide?.toneOfVoice || "professional";
|
|
116
|
+
return `
|
|
117
|
+
Translation Task: ${sourceLang} → ${targetLang}
|
|
118
|
+
${additionalInstructions}
|
|
119
|
+
|
|
120
|
+
Category: ${safeContext.category}
|
|
121
|
+
Context Instructions: ${safeContext.prompt}
|
|
122
|
+
|
|
123
|
+
LENGTH CONTROL:
|
|
124
|
+
${lengthInstructions}
|
|
125
|
+
|
|
126
|
+
STRICT OUTPUT REQUIREMENTS:
|
|
127
|
+
1. RETURN ONLY THE TRANSLATED TEXT
|
|
128
|
+
2. NO EXPLANATIONS OR COMMENTARY
|
|
129
|
+
3. NO <think> BLOCKS OR MARKDOWN
|
|
130
|
+
4. NO QUOTES OR FORMATTING
|
|
131
|
+
5. PRESERVE ALL PLACEHOLDERS EXACTLY AS THEY APPEAR
|
|
132
|
+
|
|
133
|
+
CRITICAL PLACEHOLDER RULES:
|
|
134
|
+
- Keep ALL placeholders like {message}, {name}, {value} EXACTLY as they appear in the source
|
|
135
|
+
- DO NOT translate placeholder names: {message} must stay {message}, NOT {mesaj} or {mensaje}
|
|
136
|
+
- DO NOT add content around placeholders: {message} must NOT become {mesaj}{message} or Bạn{message}
|
|
137
|
+
- DO NOT modify the curly braces: { and } must remain unchanged
|
|
138
|
+
- Placeholders are variables that will be replaced with dynamic content later
|
|
139
|
+
- Only translate the text AROUND the placeholders, never the placeholders themselves
|
|
140
|
+
|
|
141
|
+
Style: ${formality}, ${toneOfVoice}
|
|
142
|
+
|
|
143
|
+
Text to Translate:
|
|
144
|
+
${text}`;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Base analysis prompt template
|
|
148
|
+
*/
|
|
149
|
+
const baseAnalysisPromptTemplate = (text, options = {}) => {
|
|
150
|
+
if (typeof text !== "string") {
|
|
151
|
+
console.warn("Invalid text provided to baseAnalysisPromptTemplate");
|
|
152
|
+
text = "";
|
|
153
|
+
}
|
|
154
|
+
if (!options || typeof options !== "object") {
|
|
155
|
+
console.warn("Invalid options provided to baseAnalysisPromptTemplate, using defaults");
|
|
156
|
+
options = {};
|
|
157
|
+
}
|
|
158
|
+
let categories = "technical, marketing, legal, defi, ui, general";
|
|
159
|
+
if (options.categories && typeof options.categories === "object") {
|
|
160
|
+
try {
|
|
161
|
+
const categoryKeys = Object.keys(options.categories);
|
|
162
|
+
if (Array.isArray(categoryKeys) && categoryKeys.length > 0) {
|
|
163
|
+
categories = categoryKeys.join(", ");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.warn("Error processing categories:", error.message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const maxTextLength = 1500;
|
|
171
|
+
const truncatedText = text.length > maxTextLength ? text.substring(0, maxTextLength) + "..." : text;
|
|
172
|
+
const allowNewCategories = options.allowNewCategories === true;
|
|
173
|
+
const categoryNote = allowNewCategories
|
|
174
|
+
? ", or suggest a new category if none of these fit"
|
|
175
|
+
: "";
|
|
176
|
+
return `
|
|
177
|
+
TASK: Analyze the following text and determine its context category.
|
|
178
|
+
|
|
179
|
+
TEXT TO ANALYZE:
|
|
180
|
+
"""
|
|
181
|
+
${truncatedText}
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
AVAILABLE CATEGORIES: ${categories}${categoryNote}
|
|
185
|
+
|
|
186
|
+
INSTRUCTIONS:
|
|
187
|
+
1. Identify the primary context category of the text
|
|
188
|
+
2. Provide a confidence score (0.0-1.0)
|
|
189
|
+
3. Suggest 3-5 keywords that are relevant to this text
|
|
190
|
+
4. Provide a brief explanation of your categorization
|
|
191
|
+
|
|
192
|
+
FORMAT YOUR RESPONSE AS JSON:
|
|
193
|
+
{
|
|
194
|
+
"category": "category_name",
|
|
195
|
+
"confidence": 0.0-1.0,
|
|
196
|
+
"keywords": ["keyword1", "keyword2", "keyword3"],
|
|
197
|
+
"explanation": "Brief explanation of why this category was chosen"
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
};
|
|
201
|
+
// Provider-specific templates for translation
|
|
202
|
+
const translationPrompts = {
|
|
203
|
+
gemini: (sourceLang, targetLang, text, options) => ({
|
|
204
|
+
contents: [
|
|
205
|
+
{
|
|
206
|
+
parts: [
|
|
207
|
+
{
|
|
208
|
+
text: `${baseTranslationPromptTemplate(sourceLang, targetLang, text, options)}
|
|
209
|
+
Original Text: "${text}"`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
}),
|
|
215
|
+
openai: (sourceLang, targetLang, text, options) => ({
|
|
216
|
+
messages: [
|
|
217
|
+
{
|
|
218
|
+
role: "system",
|
|
219
|
+
content: baseTranslationPromptTemplate(sourceLang, targetLang, text, options),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
role: "user",
|
|
223
|
+
content: text,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
}),
|
|
227
|
+
dashscope: (sourceLang, targetLang, text, options) => ({
|
|
228
|
+
messages: [
|
|
229
|
+
{
|
|
230
|
+
role: "system",
|
|
231
|
+
content: baseTranslationPromptTemplate(sourceLang, targetLang, text, options),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
role: "user",
|
|
235
|
+
content: text,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
}),
|
|
239
|
+
deepseek: (sourceLang, targetLang, text, options) => ({
|
|
240
|
+
messages: [
|
|
241
|
+
{
|
|
242
|
+
role: "system",
|
|
243
|
+
content: baseTranslationPromptTemplate(sourceLang, targetLang, text, options),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: text,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
}),
|
|
251
|
+
xai: (sourceLang, targetLang, text, options) => ({
|
|
252
|
+
messages: [
|
|
253
|
+
{
|
|
254
|
+
role: "system",
|
|
255
|
+
content: baseTranslationPromptTemplate(sourceLang, targetLang, text, options),
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
role: "user",
|
|
259
|
+
content: text,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
}),
|
|
263
|
+
default: (sourceLang, targetLang, text, options) => ({
|
|
264
|
+
messages: [
|
|
265
|
+
{
|
|
266
|
+
role: "system",
|
|
267
|
+
content: baseTranslationPromptTemplate(sourceLang, targetLang, text, options),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
role: "user",
|
|
271
|
+
content: text,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
// Provider-specific templates for analysis
|
|
277
|
+
const analysisPrompts = {
|
|
278
|
+
gemini: (text, options) => ({
|
|
279
|
+
contents: [
|
|
280
|
+
{
|
|
281
|
+
parts: [
|
|
282
|
+
{
|
|
283
|
+
text: baseAnalysisPromptTemplate(text, options),
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
}),
|
|
289
|
+
openai: (text, options) => ({
|
|
290
|
+
messages: [
|
|
291
|
+
{
|
|
292
|
+
role: "system",
|
|
293
|
+
content: "You are a context analysis assistant that helps identify the category and context of text.",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
role: "user",
|
|
297
|
+
content: baseAnalysisPromptTemplate(text, options),
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
}),
|
|
301
|
+
dashscope: (text, options) => ({
|
|
302
|
+
model: options.model || "qwen-plus",
|
|
303
|
+
input: {
|
|
304
|
+
messages: [
|
|
305
|
+
{
|
|
306
|
+
role: "system",
|
|
307
|
+
content: "You are a context analysis assistant that helps identify the category and context of text.",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
role: "user",
|
|
311
|
+
content: baseAnalysisPromptTemplate(text, options),
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
parameters: {
|
|
316
|
+
temperature: options.temperature || 0.2,
|
|
317
|
+
max_tokens: options.maxTokens || 1000,
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
deepseek: (text, options) => ({
|
|
321
|
+
messages: [
|
|
322
|
+
{
|
|
323
|
+
role: "system",
|
|
324
|
+
content: "You are a context analysis assistant that helps identify the category and context of text.",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
role: "user",
|
|
328
|
+
content: baseAnalysisPromptTemplate(text, options),
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
}),
|
|
332
|
+
xai: (text, options) => ({
|
|
333
|
+
messages: [
|
|
334
|
+
{
|
|
335
|
+
role: "system",
|
|
336
|
+
content: "You are a context analysis assistant that helps identify the category and context of text.",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
role: "user",
|
|
340
|
+
content: baseAnalysisPromptTemplate(text, options),
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
}),
|
|
344
|
+
default: (text, options) => ({
|
|
345
|
+
messages: [
|
|
346
|
+
{
|
|
347
|
+
role: "system",
|
|
348
|
+
content: "You are a context analysis assistant that helps identify the category and context of text.",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
role: "user",
|
|
352
|
+
content: baseAnalysisPromptTemplate(text, options),
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
}),
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Get translation prompt
|
|
359
|
+
*/
|
|
360
|
+
const getPrompt = (provider, sourceLang, targetLang, text, options = {}) => {
|
|
361
|
+
if (!provider || typeof provider !== "string") {
|
|
362
|
+
console.warn("Invalid provider provided to getPrompt, using default");
|
|
363
|
+
provider = "default";
|
|
364
|
+
}
|
|
365
|
+
if (!sourceLang || typeof sourceLang !== "string") {
|
|
366
|
+
console.warn("Invalid sourceLang provided to getPrompt, using en");
|
|
367
|
+
sourceLang = "en";
|
|
368
|
+
}
|
|
369
|
+
if (!targetLang || typeof targetLang !== "string") {
|
|
370
|
+
console.warn("Invalid targetLang provided to getPrompt, using es");
|
|
371
|
+
targetLang = "es";
|
|
372
|
+
}
|
|
373
|
+
if (typeof text !== "string") {
|
|
374
|
+
console.warn("Invalid text provided to getPrompt, using empty string");
|
|
375
|
+
text = "";
|
|
376
|
+
}
|
|
377
|
+
if (!options || typeof options !== "object") {
|
|
378
|
+
console.warn("Invalid options provided to getPrompt, using defaults");
|
|
379
|
+
options = {};
|
|
380
|
+
}
|
|
381
|
+
const promptGenerator = translationPrompts[provider] || translationPrompts.default;
|
|
382
|
+
try {
|
|
383
|
+
return promptGenerator(sourceLang, targetLang, text, options);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
console.error(`Error generating prompt for provider ${provider}:`, error.message);
|
|
387
|
+
return translationPrompts.default(sourceLang, targetLang, text, options);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Get analysis prompt
|
|
392
|
+
*/
|
|
393
|
+
const getAnalysisPrompt = (provider, text, options = {}) => {
|
|
394
|
+
if (!provider || typeof provider !== "string") {
|
|
395
|
+
console.warn("Invalid provider provided to getAnalysisPrompt, using default");
|
|
396
|
+
provider = "default";
|
|
397
|
+
}
|
|
398
|
+
if (typeof text !== "string") {
|
|
399
|
+
console.warn("Invalid text provided to getAnalysisPrompt, using empty string");
|
|
400
|
+
text = "";
|
|
401
|
+
}
|
|
402
|
+
if (!options || typeof options !== "object") {
|
|
403
|
+
console.warn("Invalid options provided to getAnalysisPrompt, using defaults");
|
|
404
|
+
options = {};
|
|
405
|
+
}
|
|
406
|
+
const promptGenerator = analysisPrompts[provider] || analysisPrompts.default;
|
|
407
|
+
try {
|
|
408
|
+
return promptGenerator(text, options);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
console.error(`Error generating analysis prompt for provider ${provider}:`, error.message);
|
|
412
|
+
return analysisPrompts.default(text, options);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
export { getPrompt, getAnalysisPrompt };
|