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,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Review Command.
|
|
3
|
+
* Allows manual review of low-confidence translations.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import readline from "readline";
|
|
8
|
+
import ConfidenceScorer from "../utils/confidence-scorer.js";
|
|
9
|
+
class ReviewCommand {
|
|
10
|
+
config;
|
|
11
|
+
reviewQueue;
|
|
12
|
+
currentIndex;
|
|
13
|
+
decisions;
|
|
14
|
+
/**
|
|
15
|
+
* Initialize ReviewCommand.
|
|
16
|
+
* @param {ReviewCommandConfig} config - Configuration object.
|
|
17
|
+
*/
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.reviewQueue = [];
|
|
21
|
+
this.currentIndex = 0;
|
|
22
|
+
this.decisions = {
|
|
23
|
+
accepted: [],
|
|
24
|
+
edited: [],
|
|
25
|
+
rejected: [],
|
|
26
|
+
skipped: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load review queue from cache file.
|
|
31
|
+
* @returns {boolean} - True if loaded successfully, else false.
|
|
32
|
+
*/
|
|
33
|
+
loadReviewQueue() {
|
|
34
|
+
const reviewFile = path.join(process.cwd(), ".localize-cache", "review-queue.json");
|
|
35
|
+
if (!fs.existsSync(reviewFile)) {
|
|
36
|
+
console.log("\nNo review queue found.");
|
|
37
|
+
console.log(" Run translation with confidence scoring first: localize translate --min-confidence 0.8");
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(fs.readFileSync(reviewFile, "utf8"));
|
|
42
|
+
this.reviewQueue = data.items || [];
|
|
43
|
+
console.log(`\nLoaded ${this.reviewQueue.length} items for review\n`);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(`Error loading review queue: ${error.message}`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Start interactive review session.
|
|
53
|
+
*/
|
|
54
|
+
async startReview() {
|
|
55
|
+
if (!this.loadReviewQueue()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (this.reviewQueue.length === 0) {
|
|
59
|
+
console.log("\nNo items need review. All translations meet quality threshold!\n");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log("━".repeat(70));
|
|
63
|
+
console.log(`Review Queue: ${this.reviewQueue.length} translations need attention`);
|
|
64
|
+
console.log("━".repeat(70));
|
|
65
|
+
for (let i = 0; i < this.reviewQueue.length; i++) {
|
|
66
|
+
this.currentIndex = i;
|
|
67
|
+
const item = this.reviewQueue[i];
|
|
68
|
+
const decision = await this.reviewItem(item, i + 1, this.reviewQueue.length);
|
|
69
|
+
if (decision === "quit") {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
this.showSummary();
|
|
74
|
+
this.saveDecisions();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Review a single translation item.
|
|
78
|
+
* @param {ReviewItem} item - Translation item to review.
|
|
79
|
+
* @param {number} current - Current item index (1-based).
|
|
80
|
+
* @param {number} total - Total items.
|
|
81
|
+
* @returns {Promise<string>} - Decision result.
|
|
82
|
+
*/
|
|
83
|
+
async reviewItem(item, current, total) {
|
|
84
|
+
console.log(`\n[${current}/${total}] Translation Review`);
|
|
85
|
+
console.log("━".repeat(70));
|
|
86
|
+
console.log(`Key: ${item.key}`);
|
|
87
|
+
console.log(`Language: ${item.language}`);
|
|
88
|
+
const score = item.confidence?.score || 0;
|
|
89
|
+
const level = item.confidence?.level || "unknown";
|
|
90
|
+
console.log(`Confidence: ${ConfidenceScorer.formatConfidence(score)} (${level})`);
|
|
91
|
+
console.log(`Category: ${item.category || "general"}`);
|
|
92
|
+
console.log(`\nSource:`);
|
|
93
|
+
console.log(` "${item.source}"`);
|
|
94
|
+
console.log(`\nTranslation:`);
|
|
95
|
+
console.log(` "${item.translation}"`);
|
|
96
|
+
// Show issues if any
|
|
97
|
+
if (item.confidence?.issues && item.confidence.issues.length > 0) {
|
|
98
|
+
console.log(`\nIssues Detected:`);
|
|
99
|
+
item.confidence.issues.forEach((issue) => {
|
|
100
|
+
const icon = issue.severity === "critical" ? "[!]" : "[*]";
|
|
101
|
+
console.log(` ${icon} ${issue.message}`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
console.log(`\nActions:`);
|
|
105
|
+
console.log(` [A] Accept [E] Edit [R] Reject [S] Skip`);
|
|
106
|
+
console.log(` [N] Next [Q] Quit [?] Help`);
|
|
107
|
+
const action = await this.getUserInput("\nYour choice: ");
|
|
108
|
+
return this.handleAction(action.toLowerCase(), item);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Handle user action input.
|
|
112
|
+
* @param {string} action - User action code.
|
|
113
|
+
* @param {ReviewItem} item - Current item.
|
|
114
|
+
* @returns {Promise<string>} - Result status.
|
|
115
|
+
*/
|
|
116
|
+
async handleAction(action, item) {
|
|
117
|
+
switch (action) {
|
|
118
|
+
case "a":
|
|
119
|
+
case "accept":
|
|
120
|
+
this.decisions.accepted.push(item);
|
|
121
|
+
console.log("Accepted");
|
|
122
|
+
return "continue";
|
|
123
|
+
case "edit": {
|
|
124
|
+
const edited = await this.editTranslation(item);
|
|
125
|
+
this.decisions.edited.push(edited);
|
|
126
|
+
console.log("Edited and saved");
|
|
127
|
+
return "continue";
|
|
128
|
+
}
|
|
129
|
+
case "r":
|
|
130
|
+
case "reject":
|
|
131
|
+
this.decisions.rejected.push(item);
|
|
132
|
+
console.log("Rejected - will be retranslated");
|
|
133
|
+
return "continue";
|
|
134
|
+
case "s":
|
|
135
|
+
case "skip":
|
|
136
|
+
this.decisions.skipped.push(item);
|
|
137
|
+
console.log("Skipped");
|
|
138
|
+
return "continue";
|
|
139
|
+
case "n":
|
|
140
|
+
case "next":
|
|
141
|
+
return "continue";
|
|
142
|
+
case "q":
|
|
143
|
+
case "quit":
|
|
144
|
+
console.log("\n👋 Exiting review session...");
|
|
145
|
+
return "quit";
|
|
146
|
+
case "?":
|
|
147
|
+
case "help":
|
|
148
|
+
this.showHelp();
|
|
149
|
+
return this.handleAction(await this.getUserInput("\nYour choice: "), item);
|
|
150
|
+
default:
|
|
151
|
+
console.log("Invalid action. Try again.");
|
|
152
|
+
return this.handleAction(await this.getUserInput("\nYour choice: "), item);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Edit translation interactively.
|
|
157
|
+
* @param {ReviewItem} item - Item to edit.
|
|
158
|
+
* @returns {Promise<ReviewItem>} - Edited item.
|
|
159
|
+
*/
|
|
160
|
+
async editTranslation(item) {
|
|
161
|
+
console.log(`\nCurrent: "${item.translation}"`);
|
|
162
|
+
const newTranslation = await this.getUserInput("New translation: ");
|
|
163
|
+
if (newTranslation.trim() === "") {
|
|
164
|
+
console.log("Warning: Empty translation, keeping original");
|
|
165
|
+
return item;
|
|
166
|
+
}
|
|
167
|
+
// Recalculate confidence for edited translation
|
|
168
|
+
const newConfidence = ConfidenceScorer.calculateConfidence({
|
|
169
|
+
aiConfidence: 1.0, // Manual edit gets high confidence
|
|
170
|
+
sourceText: item.source,
|
|
171
|
+
translation: newTranslation,
|
|
172
|
+
sourceLang: item.sourceLang || this.config.source,
|
|
173
|
+
targetLang: item.language,
|
|
174
|
+
provider: "manual",
|
|
175
|
+
category: item.category,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
...item,
|
|
179
|
+
translation: newTranslation,
|
|
180
|
+
confidence: newConfidence,
|
|
181
|
+
edited: true,
|
|
182
|
+
editedAt: new Date().toISOString(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Show help information to the user.
|
|
187
|
+
*/
|
|
188
|
+
showHelp() {
|
|
189
|
+
console.log("\n━━━ Help ━━━");
|
|
190
|
+
console.log("A (Accept): Approve translation as-is");
|
|
191
|
+
console.log("E (Edit): Modify the translation manually");
|
|
192
|
+
console.log("R (Reject): Mark for retranslation");
|
|
193
|
+
console.log("S (Skip): Skip for now, review later");
|
|
194
|
+
console.log("N (Next): Skip to next item");
|
|
195
|
+
console.log("Q (Quit): Exit review session");
|
|
196
|
+
console.log("? (Help): Show this help message");
|
|
197
|
+
console.log("━".repeat(70));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Show summary of review session.
|
|
201
|
+
*/
|
|
202
|
+
showSummary() {
|
|
203
|
+
console.log("\n━".repeat(70));
|
|
204
|
+
console.log("Review Summary");
|
|
205
|
+
console.log("━".repeat(70));
|
|
206
|
+
console.log(`Accepted: ${this.decisions.accepted.length}`);
|
|
207
|
+
console.log(`Edited: ${this.decisions.edited.length}`);
|
|
208
|
+
console.log(`Rejected: ${this.decisions.rejected.length}`);
|
|
209
|
+
console.log(`Skipped: ${this.decisions.skipped.length}`);
|
|
210
|
+
console.log(`Total: ${this.decisions.accepted.length + this.decisions.edited.length + this.decisions.rejected.length + this.decisions.skipped.length}/${this.reviewQueue.length}`);
|
|
211
|
+
console.log("━".repeat(70));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Save review decisions to disk and apply them.
|
|
215
|
+
*/
|
|
216
|
+
saveDecisions() {
|
|
217
|
+
const cacheDir = path.join(process.cwd(), ".localize-cache");
|
|
218
|
+
if (!fs.existsSync(cacheDir)) {
|
|
219
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
const decisionsFile = path.join(cacheDir, "review-decisions.json");
|
|
222
|
+
const data = {
|
|
223
|
+
timestamp: new Date().toISOString(),
|
|
224
|
+
decisions: this.decisions,
|
|
225
|
+
stats: {
|
|
226
|
+
accepted: this.decisions.accepted.length,
|
|
227
|
+
edited: this.decisions.edited.length,
|
|
228
|
+
rejected: this.decisions.rejected.length,
|
|
229
|
+
skipped: this.decisions.skipped.length,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
fs.writeFileSync(decisionsFile, JSON.stringify(data, null, 2));
|
|
233
|
+
console.log(`\nDecisions saved to: ${decisionsFile}`);
|
|
234
|
+
// Apply accepted and edited translations
|
|
235
|
+
this.applyDecisions();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Apply accepted and edited translations to locale files.
|
|
239
|
+
*/
|
|
240
|
+
applyDecisions() {
|
|
241
|
+
const toApply = [...this.decisions.accepted, ...this.decisions.edited];
|
|
242
|
+
if (toApply.length === 0) {
|
|
243
|
+
console.log(" No changes to apply");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
console.log(`\nApplying ${toApply.length} approved translations...`);
|
|
247
|
+
const byLanguage = {};
|
|
248
|
+
toApply.forEach((item) => {
|
|
249
|
+
if (!byLanguage[item.language]) {
|
|
250
|
+
byLanguage[item.language] = [];
|
|
251
|
+
}
|
|
252
|
+
byLanguage[item.language].push(item);
|
|
253
|
+
});
|
|
254
|
+
Object.entries(byLanguage).forEach(([lang, items]) => {
|
|
255
|
+
const localeFile = path.join(this.config.localesDir, `${lang}.json`);
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
257
|
+
let localeData = {};
|
|
258
|
+
if (fs.existsSync(localeFile)) {
|
|
259
|
+
localeData = JSON.parse(fs.readFileSync(localeFile, "utf8"));
|
|
260
|
+
}
|
|
261
|
+
items.forEach((item) => {
|
|
262
|
+
this.setNestedValue(localeData, item.key, item.translation);
|
|
263
|
+
});
|
|
264
|
+
fs.writeFileSync(localeFile, JSON.stringify(localeData, null, 2) + "\n");
|
|
265
|
+
console.log(` Updated ${lang}.json (${items.length} translations)`);
|
|
266
|
+
});
|
|
267
|
+
console.log("\nAll approved translations applied!");
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Set nested value in object using dot notation.
|
|
271
|
+
* @param {any} obj - Object to modify.
|
|
272
|
+
* @param {string} path - Dot notation path.
|
|
273
|
+
* @param {any} value - Value to set.
|
|
274
|
+
*/
|
|
275
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
|
+
setNestedValue(obj, path, value) {
|
|
277
|
+
const keys = path.split(".");
|
|
278
|
+
let current = obj;
|
|
279
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
280
|
+
if (!current[keys[i]]) {
|
|
281
|
+
current[keys[i]] = {};
|
|
282
|
+
}
|
|
283
|
+
current = current[keys[i]];
|
|
284
|
+
}
|
|
285
|
+
current[keys[keys.length - 1]] = value;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get user input from terminal.
|
|
289
|
+
* @param {string} prompt - Prompt to display.
|
|
290
|
+
* @returns {Promise<string>} - User input.
|
|
291
|
+
*/
|
|
292
|
+
getUserInput(prompt) {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
const rl = readline.createInterface({
|
|
295
|
+
input: process.stdin,
|
|
296
|
+
output: process.stdout,
|
|
297
|
+
});
|
|
298
|
+
rl.question(prompt, (answer) => {
|
|
299
|
+
rl.close();
|
|
300
|
+
resolve(answer.trim());
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Export review queue to JSON.
|
|
306
|
+
* @param {string} format - Export format (json or csv).
|
|
307
|
+
*/
|
|
308
|
+
exportReviewQueue(format = "json") {
|
|
309
|
+
if (this.reviewQueue.length === 0 && !this.loadReviewQueue()) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const timestamp = new Date().toISOString().replace(/:/g, "-").split(".")[0];
|
|
313
|
+
const filename = `review-queue-${timestamp}.${format}`;
|
|
314
|
+
if (format === "json") {
|
|
315
|
+
fs.writeFileSync(filename, JSON.stringify(this.reviewQueue, null, 2));
|
|
316
|
+
console.log(`\nReview queue exported to: ${filename}`);
|
|
317
|
+
}
|
|
318
|
+
else if (format === "csv") {
|
|
319
|
+
const csv = this.convertToCSV(this.reviewQueue);
|
|
320
|
+
fs.writeFileSync(filename, csv);
|
|
321
|
+
console.log(`\nReview queue exported to: ${filename}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Convert review queue to CSV format.
|
|
326
|
+
* @param {ReviewItem[]} items - Items to convert.
|
|
327
|
+
* @returns {string} - CSV content.
|
|
328
|
+
*/
|
|
329
|
+
convertToCSV(items) {
|
|
330
|
+
const headers = [
|
|
331
|
+
"Key",
|
|
332
|
+
"Language",
|
|
333
|
+
"Source",
|
|
334
|
+
"Translation",
|
|
335
|
+
"Confidence",
|
|
336
|
+
"Level",
|
|
337
|
+
"Category",
|
|
338
|
+
"Issues",
|
|
339
|
+
];
|
|
340
|
+
const rows = items.map((item) => [
|
|
341
|
+
item.key,
|
|
342
|
+
item.language,
|
|
343
|
+
item.source,
|
|
344
|
+
item.translation,
|
|
345
|
+
item.confidence.score.toFixed(3),
|
|
346
|
+
item.confidence.level,
|
|
347
|
+
item.category || "general",
|
|
348
|
+
item.confidence.issues.map((i) => i.message).join("; "),
|
|
349
|
+
]);
|
|
350
|
+
const csvContent = [
|
|
351
|
+
headers.join(","),
|
|
352
|
+
...rows.map((row) => row.map((cell) => `"${cell}"`).join(",")),
|
|
353
|
+
].join("\n");
|
|
354
|
+
return csvContent;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export default ReviewCommand;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import translationService from "../services/translation-service.js";
|
|
2
|
+
/**
|
|
3
|
+
* Main translator function to process source file and create translations.
|
|
4
|
+
* @param {string} file - Source file path.
|
|
5
|
+
* @param {TranslationOptions} options - Translation options.
|
|
6
|
+
* @returns {Promise<GlobalStats>} - Translation result.
|
|
7
|
+
*/
|
|
8
|
+
async function translateFile(file, options) {
|
|
9
|
+
return await translationService.translateFile(file, options);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Find locale files based on source language.
|
|
13
|
+
* @param {string} localesDir - Locales directory.
|
|
14
|
+
* @param {string} sourceLang - Source language code.
|
|
15
|
+
* @returns {Promise<Array<string>>} - Array of locale file paths.
|
|
16
|
+
*/
|
|
17
|
+
async function findLocaleFiles(localesDir, sourceLang) {
|
|
18
|
+
return await translationService.findLocaleFiles(localesDir, sourceLang);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validate and fix existing translations that have length issues.
|
|
22
|
+
* @param {string} file - File path to validate.
|
|
23
|
+
* @param {TranslationOptions} options - Validation options.
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
async function validateAndFixExistingTranslations(file, options) {
|
|
27
|
+
return await translationService.validateAndFixExistingTranslations(file, options);
|
|
28
|
+
}
|
|
29
|
+
export { translateFile, findLocaleFiles, validateAndFixExistingTranslations };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { loadConfig as c12LoadConfig } from "c12";
|
|
2
|
+
/**
|
|
3
|
+
* Type-safe configuration helper
|
|
4
|
+
*/
|
|
5
|
+
export function defineConfig(config) {
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load configuration using c12
|
|
10
|
+
*/
|
|
11
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
12
|
+
const { config, configFile, layers } = await c12LoadConfig({
|
|
13
|
+
name: "localize",
|
|
14
|
+
configFile: "localize.config",
|
|
15
|
+
rcFile: ".localizerc",
|
|
16
|
+
dotenv: true,
|
|
17
|
+
cwd,
|
|
18
|
+
defaults: {
|
|
19
|
+
source: "en",
|
|
20
|
+
targets: [],
|
|
21
|
+
localesDir: "./locales",
|
|
22
|
+
concurrencyLimit: 5,
|
|
23
|
+
cacheEnabled: true,
|
|
24
|
+
retryOptions: {
|
|
25
|
+
maxRetries: 2,
|
|
26
|
+
initialDelay: 1000,
|
|
27
|
+
maxDelay: 10000,
|
|
28
|
+
},
|
|
29
|
+
context: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
detection: { threshold: 2, minConfidence: 0.6 },
|
|
32
|
+
useAI: false,
|
|
33
|
+
minTextLength: 50,
|
|
34
|
+
categories: {},
|
|
35
|
+
fallback: {
|
|
36
|
+
category: "general",
|
|
37
|
+
prompt: "Provide a natural translation",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
apiConfig: {},
|
|
41
|
+
fileOperations: {
|
|
42
|
+
format: "json",
|
|
43
|
+
fileStructure: "flat",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
config: config || {},
|
|
49
|
+
configFile,
|
|
50
|
+
layers,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { JsonAdapter } from "./json-adapter.js";
|
|
3
|
+
import { YamlAdapter } from "./yaml-adapter.js";
|
|
4
|
+
import { PoAdapter } from "./po-adapter.js";
|
|
5
|
+
import { PropertiesAdapter } from "./properties-adapter.js";
|
|
6
|
+
/**
|
|
7
|
+
* Factory class to manage file format adapters
|
|
8
|
+
*/
|
|
9
|
+
export class FormatFactory {
|
|
10
|
+
static adapters = [
|
|
11
|
+
new JsonAdapter(),
|
|
12
|
+
new YamlAdapter(),
|
|
13
|
+
new PoAdapter(),
|
|
14
|
+
new PropertiesAdapter(),
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Get adapter for a specific file path
|
|
18
|
+
* @param filePath - Path to file
|
|
19
|
+
*/
|
|
20
|
+
static getAdapter(filePath) {
|
|
21
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
22
|
+
const adapter = this.adapters.find((a) => a.extensions.includes(ext));
|
|
23
|
+
if (!adapter) {
|
|
24
|
+
// Default to JSON if unknown, or throw error?
|
|
25
|
+
// For robustness, maybe throw or default to JSON but warn.
|
|
26
|
+
// Let's default to JSON for .arb or weird unknown formats if they look like json,
|
|
27
|
+
// but strict checking is establishing explicitly.
|
|
28
|
+
// Fallback to JSON for now as it's the safest bet for web.
|
|
29
|
+
return new JsonAdapter();
|
|
30
|
+
}
|
|
31
|
+
return adapter;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get adapter by format name (json, yaml, etc.)
|
|
35
|
+
*/
|
|
36
|
+
static getAdapterByFormat(format) {
|
|
37
|
+
const formatted = format.startsWith(".") ? format : `.${format}`;
|
|
38
|
+
const adapter = this.adapters.find((a) => a.extensions.includes(formatted));
|
|
39
|
+
if (!adapter) {
|
|
40
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
41
|
+
}
|
|
42
|
+
return adapter;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for JSON files
|
|
3
|
+
*/
|
|
4
|
+
export class JsonAdapter {
|
|
5
|
+
extensions = [".json", ".arb"];
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
async parse(content) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(content);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
throw new Error(`JSON parse error: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
async serialize(data, options = {}) {
|
|
17
|
+
const indent = options.indent || 4;
|
|
18
|
+
return JSON.stringify(data, null, indent);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import gettextParser from "gettext-parser";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for Gettext PO files
|
|
4
|
+
*/
|
|
5
|
+
export class PoAdapter {
|
|
6
|
+
extensions = [".po", ".pot"];
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
async parse(content) {
|
|
9
|
+
try {
|
|
10
|
+
const po = gettextParser.po.parse(content);
|
|
11
|
+
const result = {};
|
|
12
|
+
// Convert PO structure to flat key-value pair
|
|
13
|
+
// msgid -> msgstr[0]
|
|
14
|
+
for (const context in po.translations) {
|
|
15
|
+
for (const msgid in po.translations[context]) {
|
|
16
|
+
if (msgid === "")
|
|
17
|
+
continue; // Skip header
|
|
18
|
+
const entry = po.translations[context][msgid];
|
|
19
|
+
result[msgid] = entry.msgstr[0];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error(`PO parse error: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
async serialize(data, options = {}) {
|
|
30
|
+
const translations = {
|
|
31
|
+
"": {},
|
|
32
|
+
};
|
|
33
|
+
// Convert flat object back to PO structure
|
|
34
|
+
for (const [key, value] of Object.entries(data)) {
|
|
35
|
+
translations[""][key] = {
|
|
36
|
+
msgid: key,
|
|
37
|
+
msgstr: [value],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return gettextParser.po
|
|
41
|
+
.compile({
|
|
42
|
+
charset: "utf-8",
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": "text/plain; charset=utf-8",
|
|
45
|
+
},
|
|
46
|
+
translations,
|
|
47
|
+
})
|
|
48
|
+
.toString();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import dotProperties from "dot-properties";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for Java .properties files
|
|
4
|
+
*/
|
|
5
|
+
export class PropertiesAdapter {
|
|
6
|
+
extensions = [".properties"];
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
async parse(content) {
|
|
9
|
+
try {
|
|
10
|
+
return dotProperties.parse(content);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new Error(`Properties parse error: ${error.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
async serialize(data, options = {}) {
|
|
18
|
+
// dot-properties doesn't have a standardized stringify, simple implementation
|
|
19
|
+
// or used a different library, but let's do a simple one for now or use the lib if it supports it.
|
|
20
|
+
// Checking docs: dot-properties is mostly a parser. 'properties' npm package is better for writing but we installed dot-properties.
|
|
21
|
+
// Let's implement a robust serializer manually as it's simple enough for standard usage.
|
|
22
|
+
return Object.entries(data)
|
|
23
|
+
.map(([key, value]) => {
|
|
24
|
+
// Escape special characters
|
|
25
|
+
const escapedKey = key.replace(/([=:])/g, "\\$1").replace(/ /g, "\\ ");
|
|
26
|
+
const escapedValue = String(value).replace(/\n/g, "\\n");
|
|
27
|
+
return `${escapedKey}=${escapedValue}`;
|
|
28
|
+
})
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for YAML files
|
|
4
|
+
*/
|
|
5
|
+
export class YamlAdapter {
|
|
6
|
+
extensions = [".yaml", ".yml"];
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
async parse(content) {
|
|
9
|
+
try {
|
|
10
|
+
return yaml.load(content) || {};
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new Error(`YAML parse error: ${error.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
async serialize(data, options = {}) {
|
|
18
|
+
return yaml.dump(data, {
|
|
19
|
+
indent: options.indent || 2,
|
|
20
|
+
lineWidth: -1, // Disable line wrapping
|
|
21
|
+
noRefs: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|