locai 2.2.0 ā 2.3.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/dist/commands/interactive-review.d.ts +9 -0
- package/dist/commands/interactive-review.js +109 -0
- package/dist/commands/review.d.ts +1 -37
- package/dist/commands/review.js +40 -160
- package/dist/core/adapters/android-adapter.d.ts +12 -0
- package/dist/core/adapters/android-adapter.js +107 -0
- package/dist/core/adapters/factory.js +4 -0
- package/dist/core/adapters/ios-adapter.d.ts +10 -0
- package/dist/core/adapters/ios-adapter.js +45 -0
- package/dist/providers/openai.js +3 -3
- package/package.json +4 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import ConfidenceScorer from "../utils/confidence-scorer.js";
|
|
5
|
+
export class InteractiveReviewer {
|
|
6
|
+
queue;
|
|
7
|
+
decisions;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.queue = [];
|
|
10
|
+
this.decisions = {
|
|
11
|
+
accepted: [],
|
|
12
|
+
edited: [],
|
|
13
|
+
rejected: [],
|
|
14
|
+
skipped: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
loadQueue() {
|
|
18
|
+
const reviewFile = path.join(process.cwd(), ".localize-cache", "review-queue.json");
|
|
19
|
+
if (!fs.existsSync(reviewFile))
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(fs.readFileSync(reviewFile, "utf8"));
|
|
23
|
+
this.queue = data.items || [];
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async start() {
|
|
31
|
+
if (!this.loadQueue() || this.queue.length === 0) {
|
|
32
|
+
console.log("No items to review.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.clear();
|
|
36
|
+
console.log(`\nš Interactive Review Mode (${this.queue.length} items)\n`);
|
|
37
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
38
|
+
const item = this.queue[i];
|
|
39
|
+
const progress = `[${i + 1}/${this.queue.length}]`;
|
|
40
|
+
console.log(`\n${progress} ${item.language.toUpperCase()} | Key: ${item.key}`);
|
|
41
|
+
console.log(`Confidence: ${ConfidenceScorer.formatConfidence(item.confidence.score)} (${item.confidence.level})`);
|
|
42
|
+
const response = await prompts({
|
|
43
|
+
type: "select",
|
|
44
|
+
name: "action",
|
|
45
|
+
message: `Translation: "${item.translation}"\nSource: "${item.source}"\nAction:`,
|
|
46
|
+
choices: [
|
|
47
|
+
{ title: "ā
Approve", value: "approve" },
|
|
48
|
+
{ title: "āļø Edit", value: "edit" },
|
|
49
|
+
{ title: "āļø Skip", value: "skip" },
|
|
50
|
+
{ title: "šŖ Exit", value: "exit" },
|
|
51
|
+
],
|
|
52
|
+
initial: 0,
|
|
53
|
+
});
|
|
54
|
+
if (response.action === "exit")
|
|
55
|
+
break;
|
|
56
|
+
await this.handleAction(response.action, item);
|
|
57
|
+
}
|
|
58
|
+
this.saveDecisions();
|
|
59
|
+
}
|
|
60
|
+
async handleAction(action, item) {
|
|
61
|
+
switch (action) {
|
|
62
|
+
case "approve":
|
|
63
|
+
this.decisions.accepted.push(item);
|
|
64
|
+
break;
|
|
65
|
+
case "edit":
|
|
66
|
+
const editResponse = await prompts({
|
|
67
|
+
type: "text",
|
|
68
|
+
name: "newTranslation",
|
|
69
|
+
message: "Enter new translation:",
|
|
70
|
+
initial: item.translation,
|
|
71
|
+
});
|
|
72
|
+
if (editResponse.newTranslation) {
|
|
73
|
+
item.translation = editResponse.newTranslation;
|
|
74
|
+
item.edited = true;
|
|
75
|
+
this.decisions.edited.push(item);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this.decisions.skipped.push(item);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "skip":
|
|
82
|
+
this.decisions.skipped.push(item);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
saveDecisions() {
|
|
87
|
+
// Reuse existing ReviewCommand logic or implement save/apply logic here
|
|
88
|
+
// For now, let's save to a decision file that ReviewCommand can pick up or apply directly
|
|
89
|
+
const cacheDir = path.join(process.cwd(), ".localize-cache");
|
|
90
|
+
if (!fs.existsSync(cacheDir))
|
|
91
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
92
|
+
const decisionsFile = path.join(cacheDir, "review-decisions.json");
|
|
93
|
+
const data = {
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
decisions: this.decisions,
|
|
96
|
+
stats: {
|
|
97
|
+
accepted: this.decisions.accepted.length,
|
|
98
|
+
edited: this.decisions.edited.length,
|
|
99
|
+
rejected: this.decisions.rejected.length,
|
|
100
|
+
skipped: this.decisions.skipped.length,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
fs.writeFileSync(decisionsFile, JSON.stringify(data, null, 2));
|
|
104
|
+
console.log(`\nā
Review complete! Decisions saved to ${decisionsFile}`);
|
|
105
|
+
console.log(`Run 'locai review --apply' to apply changes to files.`);
|
|
106
|
+
// Note: The existing CLI 'review' command applies immediately in its class.
|
|
107
|
+
// We might want to trigger apply here or ensuring ReviewCommand is used.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive Review Command.
|
|
3
|
-
* Allows manual review of low-confidence translations.
|
|
4
|
-
*/
|
|
5
1
|
interface ReviewItem {
|
|
6
2
|
key: string;
|
|
7
3
|
language: string;
|
|
@@ -28,7 +24,6 @@ interface ReviewCommandConfig {
|
|
|
28
24
|
declare class ReviewCommand {
|
|
29
25
|
private config;
|
|
30
26
|
private reviewQueue;
|
|
31
|
-
private currentIndex;
|
|
32
27
|
private decisions;
|
|
33
28
|
/**
|
|
34
29
|
* Initialize ReviewCommand.
|
|
@@ -44,35 +39,10 @@ declare class ReviewCommand {
|
|
|
44
39
|
* Start interactive review session.
|
|
45
40
|
*/
|
|
46
41
|
startReview(): Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* Review a single translation item.
|
|
49
|
-
* @param {ReviewItem} item - Translation item to review.
|
|
50
|
-
* @param {number} current - Current item index (1-based).
|
|
51
|
-
* @param {number} total - Total items.
|
|
52
|
-
* @returns {Promise<string>} - Decision result.
|
|
53
|
-
*/
|
|
54
|
-
reviewItem(item: ReviewItem, current: number, total: number): Promise<string>;
|
|
55
42
|
/**
|
|
56
43
|
* Handle user action input.
|
|
57
|
-
* @param {string} action - User action code.
|
|
58
|
-
* @param {ReviewItem} item - Current item.
|
|
59
|
-
* @returns {Promise<string>} - Result status.
|
|
60
|
-
*/
|
|
61
|
-
handleAction(action: string, item: ReviewItem): Promise<string>;
|
|
62
|
-
/**
|
|
63
|
-
* Edit translation interactively.
|
|
64
|
-
* @param {ReviewItem} item - Item to edit.
|
|
65
|
-
* @returns {Promise<ReviewItem>} - Edited item.
|
|
66
|
-
*/
|
|
67
|
-
editTranslation(item: ReviewItem): Promise<ReviewItem>;
|
|
68
|
-
/**
|
|
69
|
-
* Show help information to the user.
|
|
70
|
-
*/
|
|
71
|
-
showHelp(): void;
|
|
72
|
-
/**
|
|
73
|
-
* Show summary of review session.
|
|
74
44
|
*/
|
|
75
|
-
|
|
45
|
+
private handleAction;
|
|
76
46
|
/**
|
|
77
47
|
* Save review decisions to disk and apply them.
|
|
78
48
|
*/
|
|
@@ -88,12 +58,6 @@ declare class ReviewCommand {
|
|
|
88
58
|
* @param {any} value - Value to set.
|
|
89
59
|
*/
|
|
90
60
|
setNestedValue(obj: any, path: string, value: any): void;
|
|
91
|
-
/**
|
|
92
|
-
* Get user input from terminal.
|
|
93
|
-
* @param {string} prompt - Prompt to display.
|
|
94
|
-
* @returns {Promise<string>} - User input.
|
|
95
|
-
*/
|
|
96
|
-
getUserInput(prompt: string): Promise<string>;
|
|
97
61
|
/**
|
|
98
62
|
* Export review queue to JSON.
|
|
99
63
|
* @param {string} format - Export format (json or csv).
|
package/dist/commands/review.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive Review Command.
|
|
3
|
-
* Allows manual review of low-confidence translations.
|
|
4
|
-
*/
|
|
5
1
|
import fs from "fs";
|
|
6
2
|
import path from "path";
|
|
7
|
-
import
|
|
3
|
+
import prompts from "prompts";
|
|
8
4
|
import ConfidenceScorer from "../utils/confidence-scorer.js";
|
|
9
5
|
class ReviewCommand {
|
|
10
6
|
config;
|
|
11
7
|
reviewQueue;
|
|
12
|
-
currentIndex;
|
|
13
8
|
decisions;
|
|
14
9
|
/**
|
|
15
10
|
* Initialize ReviewCommand.
|
|
@@ -18,7 +13,6 @@ class ReviewCommand {
|
|
|
18
13
|
constructor(config) {
|
|
19
14
|
this.config = config;
|
|
20
15
|
this.reviewQueue = [];
|
|
21
|
-
this.currentIndex = 0;
|
|
22
16
|
this.decisions = {
|
|
23
17
|
accepted: [],
|
|
24
18
|
edited: [],
|
|
@@ -40,7 +34,6 @@ class ReviewCommand {
|
|
|
40
34
|
try {
|
|
41
35
|
const data = JSON.parse(fs.readFileSync(reviewFile, "utf8"));
|
|
42
36
|
this.reviewQueue = data.items || [];
|
|
43
|
-
console.log(`\nLoaded ${this.reviewQueue.length} items for review\n`);
|
|
44
37
|
return true;
|
|
45
38
|
}
|
|
46
39
|
catch (error) {
|
|
@@ -56,159 +49,63 @@ class ReviewCommand {
|
|
|
56
49
|
return;
|
|
57
50
|
}
|
|
58
51
|
if (this.reviewQueue.length === 0) {
|
|
59
|
-
console.log("
|
|
52
|
+
console.log("No items to review.");
|
|
60
53
|
return;
|
|
61
54
|
}
|
|
62
|
-
console.
|
|
63
|
-
console.log(
|
|
64
|
-
console.log("ā".repeat(70));
|
|
55
|
+
console.clear();
|
|
56
|
+
console.log(`\nInteractive Review Mode (${this.reviewQueue.length} items)\n`);
|
|
65
57
|
for (let i = 0; i < this.reviewQueue.length; i++) {
|
|
66
|
-
this.currentIndex = i;
|
|
67
58
|
const item = this.reviewQueue[i];
|
|
68
|
-
const
|
|
69
|
-
|
|
59
|
+
const progress = `[${i + 1}/${this.reviewQueue.length}]`;
|
|
60
|
+
console.log(`\n${progress} ${item.language.toUpperCase()} | Key: ${item.key}`);
|
|
61
|
+
console.log(`Confidence: ${ConfidenceScorer.formatConfidence(item.confidence.score)} (${item.confidence.level})`);
|
|
62
|
+
const response = await prompts({
|
|
63
|
+
type: "select",
|
|
64
|
+
name: "action",
|
|
65
|
+
message: `Translation: "${item.translation}"\nSource: "${item.source}"\nAction:`,
|
|
66
|
+
choices: [
|
|
67
|
+
{ title: "[Approve]", value: "approve" },
|
|
68
|
+
{ title: "[Edit]", value: "edit" },
|
|
69
|
+
{ title: "[Skip]", value: "skip" },
|
|
70
|
+
{ title: "[Exit]", value: "exit" },
|
|
71
|
+
],
|
|
72
|
+
initial: 0,
|
|
73
|
+
});
|
|
74
|
+
if (response.action === "exit")
|
|
70
75
|
break;
|
|
71
|
-
|
|
76
|
+
await this.handleAction(response.action, item);
|
|
72
77
|
}
|
|
73
|
-
this.showSummary();
|
|
74
78
|
this.saveDecisions();
|
|
75
79
|
}
|
|
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
80
|
/**
|
|
111
81
|
* Handle user action input.
|
|
112
|
-
* @param {string} action - User action code.
|
|
113
|
-
* @param {ReviewItem} item - Current item.
|
|
114
|
-
* @returns {Promise<string>} - Result status.
|
|
115
82
|
*/
|
|
116
83
|
async handleAction(action, item) {
|
|
117
84
|
switch (action) {
|
|
118
|
-
case "
|
|
119
|
-
case "accept":
|
|
85
|
+
case "approve":
|
|
120
86
|
this.decisions.accepted.push(item);
|
|
121
|
-
|
|
122
|
-
return "continue";
|
|
87
|
+
break;
|
|
123
88
|
case "edit": {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
89
|
+
const editResponse = await prompts({
|
|
90
|
+
type: "text",
|
|
91
|
+
name: "newTranslation",
|
|
92
|
+
message: "Enter new translation:",
|
|
93
|
+
initial: item.translation,
|
|
94
|
+
});
|
|
95
|
+
if (editResponse.newTranslation) {
|
|
96
|
+
item.translation = editResponse.newTranslation;
|
|
97
|
+
item.edited = true;
|
|
98
|
+
this.decisions.edited.push(item);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.decisions.skipped.push(item);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
128
104
|
}
|
|
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
105
|
case "skip":
|
|
136
106
|
this.decisions.skipped.push(item);
|
|
137
|
-
|
|
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;
|
|
107
|
+
break;
|
|
166
108
|
}
|
|
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
109
|
}
|
|
213
110
|
/**
|
|
214
111
|
* Save review decisions to disk and apply them.
|
|
@@ -230,7 +127,7 @@ class ReviewCommand {
|
|
|
230
127
|
},
|
|
231
128
|
};
|
|
232
129
|
fs.writeFileSync(decisionsFile, JSON.stringify(data, null, 2));
|
|
233
|
-
console.log(`\
|
|
130
|
+
console.log(`\nReview complete! Decisions saved to ${decisionsFile}`);
|
|
234
131
|
// Apply accepted and edited translations
|
|
235
132
|
this.applyDecisions();
|
|
236
133
|
}
|
|
@@ -284,23 +181,6 @@ class ReviewCommand {
|
|
|
284
181
|
}
|
|
285
182
|
current[keys[keys.length - 1]] = value;
|
|
286
183
|
}
|
|
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
184
|
/**
|
|
305
185
|
* Export review queue to JSON.
|
|
306
186
|
* @param {string} format - Export format (json or csv).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FileFormatAdapter } from "./base-adapter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for Android XML resources files (strings.xml)
|
|
4
|
+
*/
|
|
5
|
+
export declare class AndroidAdapter implements FileFormatAdapter {
|
|
6
|
+
extensions: string[];
|
|
7
|
+
private parser;
|
|
8
|
+
private builder;
|
|
9
|
+
constructor();
|
|
10
|
+
parse(content: string): Promise<Record<string, any>>;
|
|
11
|
+
serialize(data: Record<string, any>): Promise<string>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { XMLParser, XMLBuilder } from "fast-xml-parser";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for Android XML resources files (strings.xml)
|
|
4
|
+
*/
|
|
5
|
+
export class AndroidAdapter {
|
|
6
|
+
extensions = [".xml"];
|
|
7
|
+
parser;
|
|
8
|
+
builder;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
textNodeName: "#text",
|
|
14
|
+
isArray: (name) => name === "string" || name === "string-array" || name === "item",
|
|
15
|
+
});
|
|
16
|
+
this.builder = new XMLBuilder({
|
|
17
|
+
ignoreAttributes: false,
|
|
18
|
+
attributeNamePrefix: "@_",
|
|
19
|
+
textNodeName: "#text",
|
|
20
|
+
format: true,
|
|
21
|
+
indentBy: " ",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async parse(content) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = this.parser.parse(content);
|
|
27
|
+
// Extract strings resources
|
|
28
|
+
const resources = parsed.resources || {};
|
|
29
|
+
const result = {};
|
|
30
|
+
// Handle regular strings
|
|
31
|
+
if (resources.string) {
|
|
32
|
+
resources.string.forEach((item) => {
|
|
33
|
+
const key = item["@_name"];
|
|
34
|
+
const value = item["#text"];
|
|
35
|
+
if (key) {
|
|
36
|
+
result[key] = value || "";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Handle string arrays (flattened as array_name.index)
|
|
41
|
+
if (resources["string-array"]) {
|
|
42
|
+
resources["string-array"].forEach((array) => {
|
|
43
|
+
const arrayName = array["@_name"];
|
|
44
|
+
if (array.item) {
|
|
45
|
+
array.item.forEach((value, index) => {
|
|
46
|
+
result[`${arrayName}.${index}`] = value;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new Error(`Failed to parse Android XML: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async serialize(data) {
|
|
58
|
+
const resources = {
|
|
59
|
+
string: [],
|
|
60
|
+
"string-array": [],
|
|
61
|
+
};
|
|
62
|
+
// Collect keys to handle arrays properly
|
|
63
|
+
const arrays = {};
|
|
64
|
+
const strings = {};
|
|
65
|
+
for (const [key, value] of Object.entries(data)) {
|
|
66
|
+
if (key.includes(".")) {
|
|
67
|
+
const [arrayName, indexStr] = key.split(".");
|
|
68
|
+
if (!isNaN(Number(indexStr))) {
|
|
69
|
+
if (!arrays[arrayName])
|
|
70
|
+
arrays[arrayName] = [];
|
|
71
|
+
arrays[arrayName][Number(indexStr)] = value;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
strings[key] = value;
|
|
76
|
+
}
|
|
77
|
+
// Add strings
|
|
78
|
+
for (const [key, value] of Object.entries(strings)) {
|
|
79
|
+
resources.string.push({
|
|
80
|
+
"@_name": key,
|
|
81
|
+
"#text": value,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Add arrays
|
|
85
|
+
for (const [key, items] of Object.entries(arrays)) {
|
|
86
|
+
resources["string-array"].push({
|
|
87
|
+
"@_name": key,
|
|
88
|
+
item: items.filter((i) => i !== undefined),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Sort by name for consistency
|
|
92
|
+
resources.string.sort((a, b) => a["@_name"].localeCompare(b["@_name"]));
|
|
93
|
+
resources["string-array"].sort((a, b) => a["@_name"].localeCompare(b["@_name"]));
|
|
94
|
+
if (resources["string-array"].length === 0)
|
|
95
|
+
delete resources["string-array"];
|
|
96
|
+
if (resources.string.length === 0)
|
|
97
|
+
delete resources.string;
|
|
98
|
+
const xmlObj = {
|
|
99
|
+
"?xml": {
|
|
100
|
+
"@_version": "1.0",
|
|
101
|
+
"@_encoding": "utf-8",
|
|
102
|
+
},
|
|
103
|
+
resources: resources,
|
|
104
|
+
};
|
|
105
|
+
return this.builder.build(xmlObj);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -3,6 +3,8 @@ import { JsonAdapter } from "./json-adapter.js";
|
|
|
3
3
|
import { YamlAdapter } from "./yaml-adapter.js";
|
|
4
4
|
import { PoAdapter } from "./po-adapter.js";
|
|
5
5
|
import { PropertiesAdapter } from "./properties-adapter.js";
|
|
6
|
+
import { AndroidAdapter } from "./android-adapter.js";
|
|
7
|
+
import { IosAdapter } from "./ios-adapter.js";
|
|
6
8
|
/**
|
|
7
9
|
* Factory class to manage file format adapters
|
|
8
10
|
*/
|
|
@@ -12,6 +14,8 @@ export class FormatFactory {
|
|
|
12
14
|
new YamlAdapter(),
|
|
13
15
|
new PoAdapter(),
|
|
14
16
|
new PropertiesAdapter(),
|
|
17
|
+
new AndroidAdapter(),
|
|
18
|
+
new IosAdapter(),
|
|
15
19
|
];
|
|
16
20
|
/**
|
|
17
21
|
* Get adapter for a specific file path
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FileFormatAdapter } from "./base-adapter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for iOS .strings files
|
|
4
|
+
* Format: "KEY" = "VALUE";
|
|
5
|
+
*/
|
|
6
|
+
export declare class IosAdapter implements FileFormatAdapter {
|
|
7
|
+
extensions: string[];
|
|
8
|
+
parse(content: string): Promise<Record<string, any>>;
|
|
9
|
+
serialize(data: Record<string, any>): Promise<string>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for iOS .strings files
|
|
3
|
+
* Format: "KEY" = "VALUE";
|
|
4
|
+
*/
|
|
5
|
+
export class IosAdapter {
|
|
6
|
+
extensions = [".strings"];
|
|
7
|
+
async parse(content) {
|
|
8
|
+
const result = {};
|
|
9
|
+
const lines = content.split(/\r?\n/);
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
// Skip comments and empty lines
|
|
13
|
+
if (!trimmed || trimmed.startsWith("/*") || trimmed.startsWith("//")) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
// Parse "key" = "value";
|
|
17
|
+
// Handle escaped quotes in value
|
|
18
|
+
const match = trimmed.match(/^"(.+)"\s*=\s*"(.*)";$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
const key = match[1];
|
|
21
|
+
let value = match[2];
|
|
22
|
+
// Unescape quotes and newlines if needed
|
|
23
|
+
value = value.replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
24
|
+
result[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
async serialize(data) {
|
|
30
|
+
let output = "";
|
|
31
|
+
// Sort keys for deterministic output
|
|
32
|
+
const keys = Object.keys(data).sort();
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
let value = data[key];
|
|
35
|
+
if (typeof value !== "string") {
|
|
36
|
+
value = String(value);
|
|
37
|
+
}
|
|
38
|
+
// Escape quotes and newlines
|
|
39
|
+
const safeKey = key; // Keys usually don't have quotes, but simplistic handling
|
|
40
|
+
const safeValue = value.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
41
|
+
output += `"${safeKey}" = "${safeValue}";\n`;
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -40,7 +40,7 @@ class OpenAIProvider extends BaseProvider {
|
|
|
40
40
|
return RetryHelper.withRetry(async () => {
|
|
41
41
|
try {
|
|
42
42
|
const response = await this.client.post(this.getEndpoint(), {
|
|
43
|
-
model: config.model || "gpt-
|
|
43
|
+
model: config.model || "gpt-4o",
|
|
44
44
|
...promptData,
|
|
45
45
|
temperature: config.temperature,
|
|
46
46
|
max_completion_tokens: config.maxTokens || config.max_tokens,
|
|
@@ -65,7 +65,7 @@ class OpenAIProvider extends BaseProvider {
|
|
|
65
65
|
}
|
|
66
66
|
async analyze(prompt, options = {}) {
|
|
67
67
|
const config = this.getConfig({
|
|
68
|
-
model: options.model || "gpt-
|
|
68
|
+
model: options.model || "gpt-4o",
|
|
69
69
|
temperature: options.temperature || 0.2,
|
|
70
70
|
maxTokens: options.maxTokens || 1000,
|
|
71
71
|
});
|
|
@@ -94,7 +94,7 @@ class OpenAIProvider extends BaseProvider {
|
|
|
94
94
|
}
|
|
95
95
|
async chat(messages, options = {}) {
|
|
96
96
|
const config = this.getConfig({
|
|
97
|
-
model: options.model || "gpt-
|
|
97
|
+
model: options.model || "gpt-4o",
|
|
98
98
|
temperature: options.temperature || 0.3,
|
|
99
99
|
maxTokens: options.maxTokens || 2000,
|
|
100
100
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/core/orchestrator.js",
|
|
6
6
|
"types": "dist/core/orchestrator.d.ts",
|
|
@@ -69,17 +69,20 @@
|
|
|
69
69
|
"commander": "^14.0.2",
|
|
70
70
|
"dot-properties": "^1.1.0",
|
|
71
71
|
"dotenv": "^17.2.3",
|
|
72
|
+
"fast-xml-parser": "^5.3.3",
|
|
72
73
|
"gettext-parser": "^8.0.0",
|
|
73
74
|
"jiti": "^2.6.1",
|
|
74
75
|
"js-yaml": "^4.1.1",
|
|
75
76
|
"lru-cache": "^11.2.2",
|
|
76
77
|
"p-limit": "^7.2.0",
|
|
78
|
+
"prompts": "^2.4.2",
|
|
77
79
|
"vectra": "^0.11.1"
|
|
78
80
|
},
|
|
79
81
|
"devDependencies": {
|
|
80
82
|
"@eslint/js": "^9.39.1",
|
|
81
83
|
"@types/js-yaml": "^4.0.9",
|
|
82
84
|
"@types/node": "^25.0.1",
|
|
85
|
+
"@types/prompts": "^2.4.9",
|
|
83
86
|
"@vitest/coverage-v8": "^4.0.15",
|
|
84
87
|
"eslint": "^9.39.1",
|
|
85
88
|
"eslint-config-prettier": "^10.1.8",
|