@walkinissue/angy 0.2.17 → 0.2.19
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 +72 -189
- package/dist/client/Angy.svelte +751 -546
- package/dist/client/RotationWarningDialog.svelte +177 -0
- package/dist/client/TranslationHelperForm.svelte +111 -61
- package/dist/client/VibeTooltip.svelte +18 -14
- package/dist/client/dragItem.ts +65 -39
- package/dist/client/toggleQA.shared.ts +23 -15
- package/dist/client/translationDrafts.ts +59 -0
- package/dist/plugin.js +102 -10
- package/dist/server/types.ts +30 -8
- package/dist/server.d.ts +30 -5
- package/dist/server.js +519 -142
- package/dist/server.js.map +3 -3
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -3,10 +3,11 @@ import { json as json4 } from "@sveltejs/kit";
|
|
|
3
3
|
|
|
4
4
|
// src/lib/server/commit.ts
|
|
5
5
|
import { json } from "@sveltejs/kit";
|
|
6
|
+
import { basename as basename3 } from "node:path";
|
|
6
7
|
|
|
7
8
|
// src/lib/server/catalog.ts
|
|
8
9
|
import { constants as fsConstants } from "node:fs";
|
|
9
|
-
import { access, copyFile, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
10
|
+
import { access, copyFile, readFile as readFile2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
10
11
|
import { dirname as dirname2, extname as extname2, join, basename as basename2 } from "node:path";
|
|
11
12
|
import gettextParser from "gettext-parser";
|
|
12
13
|
import Fuse from "fuse.js";
|
|
@@ -24,6 +25,30 @@ var CONFIG_FILENAMES = [
|
|
|
24
25
|
"angy.config.mjs",
|
|
25
26
|
"angy.config.cjs"
|
|
26
27
|
];
|
|
28
|
+
var NON_REASONING_SUGGESTION_MODELS = [
|
|
29
|
+
"gpt-4.1",
|
|
30
|
+
"gpt-4.1-mini",
|
|
31
|
+
"gpt-4.1-nano"
|
|
32
|
+
];
|
|
33
|
+
var GPT54_REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh"];
|
|
34
|
+
var GPT51_REASONING_EFFORTS = ["none", "low", "medium", "high"];
|
|
35
|
+
var GPT5_REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
36
|
+
var GPT5_PRO_REASONING_EFFORTS = ["high"];
|
|
37
|
+
var GPT5X_PRO_REASONING_EFFORTS = ["medium", "high", "xhigh"];
|
|
38
|
+
var REASONING_EFFORTS_BY_MODEL = {
|
|
39
|
+
"gpt-5.4": GPT54_REASONING_EFFORTS,
|
|
40
|
+
"gpt-5.4-mini": GPT54_REASONING_EFFORTS,
|
|
41
|
+
"gpt-5.4-nano": GPT54_REASONING_EFFORTS,
|
|
42
|
+
"gpt-5.2": GPT54_REASONING_EFFORTS,
|
|
43
|
+
"gpt-5.1": GPT51_REASONING_EFFORTS,
|
|
44
|
+
"gpt-5": GPT5_REASONING_EFFORTS,
|
|
45
|
+
"gpt-5-pro": GPT5_PRO_REASONING_EFFORTS,
|
|
46
|
+
"gpt-5.2-pro": GPT5X_PRO_REASONING_EFFORTS,
|
|
47
|
+
"gpt-5.4-pro": GPT5X_PRO_REASONING_EFFORTS
|
|
48
|
+
};
|
|
49
|
+
var DEFAULT_SUGGESTION_MODEL = {
|
|
50
|
+
model: "gpt-4.1-mini"
|
|
51
|
+
};
|
|
27
52
|
function buildDefaultSystemMessage(sourceLocale, targetLocale) {
|
|
28
53
|
return `You are an expert product localization assistant translating ${sourceLocale} UI copy into polished ${targetLocale}. Keep already-${targetLocale} text unchanged. Preserve placeholders like {0}, {1}, ICU fragments, HTML-like markers such as <0/> and <strong>, line breaks, punctuation, capitalization, and button-like brevity. Prefer terminology and syntax that stay close to the translated examples so the product voice remains cohesive. Do not invent extra context, do not expand abbreviations unless the examples already do, and avoid semantic drift. Return only high-confidence translation suggestions for the provided untranslated strings.`;
|
|
29
54
|
}
|
|
@@ -50,6 +75,51 @@ function validateSuggestionProvider(suggestionProvider) {
|
|
|
50
75
|
throw new Error(`[angy] suggestionProvider must be a function.`);
|
|
51
76
|
}
|
|
52
77
|
}
|
|
78
|
+
function isNonReasoningSuggestionModel(model) {
|
|
79
|
+
return NON_REASONING_SUGGESTION_MODELS.includes(model);
|
|
80
|
+
}
|
|
81
|
+
function isSupportedSuggestionModel(model) {
|
|
82
|
+
return isNonReasoningSuggestionModel(model) || Object.prototype.hasOwnProperty.call(REASONING_EFFORTS_BY_MODEL, model);
|
|
83
|
+
}
|
|
84
|
+
function normalizeSuggestionModelConfig(input) {
|
|
85
|
+
const suggestionModel = input ?? DEFAULT_SUGGESTION_MODEL;
|
|
86
|
+
if (typeof suggestionModel !== "object" || suggestionModel == null || Array.isArray(suggestionModel)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[angy] suggestionModel must be an object like { model: "gpt-4.1-mini" }.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (typeof suggestionModel.model !== "string" || !suggestionModel.model.trim()) {
|
|
92
|
+
throw new Error(`[angy] suggestionModel.model is required and must be a non-empty string.`);
|
|
93
|
+
}
|
|
94
|
+
if (!isSupportedSuggestionModel(suggestionModel.model)) {
|
|
95
|
+
throw new Error(`[angy] Unsupported suggestionModel.model "${suggestionModel.model}".`);
|
|
96
|
+
}
|
|
97
|
+
if (isNonReasoningSuggestionModel(suggestionModel.model)) {
|
|
98
|
+
if (!("reasoning" in suggestionModel) || suggestionModel.reasoning === void 0 || suggestionModel.reasoning === null) {
|
|
99
|
+
return { model: suggestionModel.model, reasoning: null };
|
|
100
|
+
}
|
|
101
|
+
throw new Error(
|
|
102
|
+
`[angy] suggestionModel.reasoning is not supported for ${suggestionModel.model}.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const allowedReasoning = REASONING_EFFORTS_BY_MODEL[suggestionModel.model];
|
|
106
|
+
const reasoning = suggestionModel.reasoning;
|
|
107
|
+
if (reasoning == null) {
|
|
108
|
+
if (suggestionModel.model === "gpt-5-pro") {
|
|
109
|
+
return { model: suggestionModel.model, reasoning: "high" };
|
|
110
|
+
}
|
|
111
|
+
return { model: suggestionModel.model };
|
|
112
|
+
}
|
|
113
|
+
if (!allowedReasoning.includes(reasoning)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`[angy] suggestionModel.reasoning "${reasoning}" is not supported for ${suggestionModel.model}.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
model: suggestionModel.model,
|
|
120
|
+
reasoning
|
|
121
|
+
};
|
|
122
|
+
}
|
|
53
123
|
var config = {
|
|
54
124
|
basePoPath: resolve(__dirname, "../../locales/en.po"),
|
|
55
125
|
workingPoPath: resolve(__dirname, "../../locales/en-working.po"),
|
|
@@ -58,7 +128,7 @@ var config = {
|
|
|
58
128
|
routePath: "/api/translations",
|
|
59
129
|
apiKey: "",
|
|
60
130
|
systemMessage: buildDefaultSystemMessage("sv", "en"),
|
|
61
|
-
suggestionModel:
|
|
131
|
+
suggestionModel: DEFAULT_SUGGESTION_MODEL,
|
|
62
132
|
watchIgnore: ["**/en-working.po"]
|
|
63
133
|
};
|
|
64
134
|
var loadedConfigRoot = null;
|
|
@@ -102,6 +172,21 @@ function validateLocaleAliasUsage(sourceLocale, targetLocale) {
|
|
|
102
172
|
throw new Error('[angy] targetLocale cannot be "base". Use an explicit target locale or "working".');
|
|
103
173
|
}
|
|
104
174
|
}
|
|
175
|
+
function validateCatalogPathSemantics(next) {
|
|
176
|
+
const baseLocale = inferLocaleFromCatalogPath(next.basePoPath);
|
|
177
|
+
const workingLocale = inferLocaleFromCatalogPath(next.workingPoPath);
|
|
178
|
+
const expectedWorkingLocale = `${next.targetLocale}-working`;
|
|
179
|
+
if (baseLocale !== next.targetLocale) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[angy] basePoPath must point to the ${next.targetLocale}.po catalog. Received "${baseLocale ?? "unknown"}".`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (workingLocale !== expectedWorkingLocale) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`[angy] workingPoPath must point to the ${expectedWorkingLocale}.po catalog. Received "${workingLocale ?? "unknown"}".`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
105
190
|
function suspendWorkingCatalogWatch(path, delayMs = 800) {
|
|
106
191
|
const normalizedPath = normalizeFsPath(path);
|
|
107
192
|
for (const controller of workingCatalogWatchControllers) {
|
|
@@ -124,12 +209,14 @@ function resolveConfiguredLocaleAliases(next) {
|
|
|
124
209
|
const resolvedSourceLocale = resolveLocaleAlias(rawSourceLocale, next);
|
|
125
210
|
const resolvedTargetLocale = resolveLocaleAlias(rawTargetLocale, next);
|
|
126
211
|
const usesDefaultSystemMessage = next.systemMessage === buildDefaultSystemMessage(rawSourceLocale, rawTargetLocale);
|
|
127
|
-
|
|
212
|
+
const resolved = {
|
|
128
213
|
...next,
|
|
129
214
|
sourceLocale: resolvedSourceLocale,
|
|
130
215
|
targetLocale: resolvedTargetLocale,
|
|
131
216
|
systemMessage: usesDefaultSystemMessage ? buildDefaultSystemMessage(resolvedSourceLocale, resolvedTargetLocale) : next.systemMessage
|
|
132
217
|
};
|
|
218
|
+
validateCatalogPathSemantics(resolved);
|
|
219
|
+
return resolved;
|
|
133
220
|
}
|
|
134
221
|
function completeAngyConfig(input) {
|
|
135
222
|
assertNonEmptyString(input.basePoPath, "basePoPath");
|
|
@@ -137,8 +224,8 @@ function completeAngyConfig(input) {
|
|
|
137
224
|
assertNonEmptyString(input.sourceLocale, "sourceLocale");
|
|
138
225
|
assertNonEmptyString(input.targetLocale, "targetLocale");
|
|
139
226
|
validateLocaleAliasUsage(input.sourceLocale, input.targetLocale);
|
|
140
|
-
if (typeof input.apiKey !== "string") {
|
|
141
|
-
throw new Error(`[angy] apiKey
|
|
227
|
+
if (typeof input.apiKey !== "string" && typeof input.apiKey !== "undefined") {
|
|
228
|
+
throw new Error(`[angy] apiKey must be a string when provided. Use an empty string to disable suggestions.`);
|
|
142
229
|
}
|
|
143
230
|
validateRoutePath(input.routePath);
|
|
144
231
|
validateWatchIgnore(input.watchIgnore);
|
|
@@ -149,15 +236,15 @@ function completeAngyConfig(input) {
|
|
|
149
236
|
sourceLocale: input.sourceLocale,
|
|
150
237
|
targetLocale: input.targetLocale,
|
|
151
238
|
routePath: input.routePath ?? "/api/translations",
|
|
152
|
-
apiKey: input.apiKey,
|
|
239
|
+
apiKey: input.apiKey ?? "",
|
|
153
240
|
systemMessage: input.systemMessage ?? buildDefaultSystemMessage(input.sourceLocale, input.targetLocale),
|
|
154
|
-
suggestionModel: input.suggestionModel
|
|
241
|
+
suggestionModel: normalizeSuggestionModelConfig(input.suggestionModel),
|
|
155
242
|
watchIgnore: input.watchIgnore ?? ["**/en-working.po"],
|
|
156
243
|
suggestionProvider: input.suggestionProvider
|
|
157
244
|
};
|
|
158
245
|
}
|
|
159
246
|
function defineAngyConfig(config2) {
|
|
160
|
-
return completeAngyConfig(config2);
|
|
247
|
+
return resolveConfiguredLocaleAliases(completeAngyConfig(config2));
|
|
161
248
|
}
|
|
162
249
|
async function fileExists(path) {
|
|
163
250
|
try {
|
|
@@ -177,13 +264,19 @@ async function loadTsConfigModule(path) {
|
|
|
177
264
|
const tempPath = `${path}.angy.tmp.mjs`;
|
|
178
265
|
await writeFile(tempPath, transformed.code, "utf8");
|
|
179
266
|
try {
|
|
180
|
-
return await import(
|
|
267
|
+
return await import(
|
|
268
|
+
/* @vite-ignore */
|
|
269
|
+
`${pathToFileURL(tempPath).href}?t=${Date.now()}`
|
|
270
|
+
);
|
|
181
271
|
} finally {
|
|
182
272
|
await unlink(tempPath).catch(() => void 0);
|
|
183
273
|
}
|
|
184
274
|
}
|
|
185
275
|
async function loadJsConfigModule(path) {
|
|
186
|
-
return import(
|
|
276
|
+
return import(
|
|
277
|
+
/* @vite-ignore */
|
|
278
|
+
pathToFileURL(path).href
|
|
279
|
+
);
|
|
187
280
|
}
|
|
188
281
|
async function loadAngyConfigFromRoot(root) {
|
|
189
282
|
if (loadedConfigRoot === root) {
|
|
@@ -207,6 +300,14 @@ async function loadAngyConfigFromRoot(root) {
|
|
|
207
300
|
|
|
208
301
|
// src/lib/server/catalog.ts
|
|
209
302
|
var runtimeTranslations = /* @__PURE__ */ new Map();
|
|
303
|
+
var CatalogIntegrityError = class extends Error {
|
|
304
|
+
issues;
|
|
305
|
+
constructor(message, issues) {
|
|
306
|
+
super(message);
|
|
307
|
+
this.name = "CatalogIntegrityError";
|
|
308
|
+
this.issues = issues;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
210
311
|
function catalogEntryKey(msgid, msgctxt) {
|
|
211
312
|
return `${msgctxt ?? ""}::${msgid}`;
|
|
212
313
|
}
|
|
@@ -280,20 +381,9 @@ function createFuse(entries) {
|
|
|
280
381
|
function buildEntryMap(entries) {
|
|
281
382
|
return new Map(entries.map((entry) => [catalogEntryKey(entry.msgid, entry.msgctxt), entry]));
|
|
282
383
|
}
|
|
283
|
-
function
|
|
284
|
-
if (!
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
...baseEntry,
|
|
289
|
-
msgstr: workingEntry.msgstr,
|
|
290
|
-
flags: workingEntry.flags,
|
|
291
|
-
previous: workingEntry.previous,
|
|
292
|
-
obsolete: workingEntry.obsolete,
|
|
293
|
-
hasTranslation: workingEntry.hasTranslation,
|
|
294
|
-
isFuzzy: workingEntry.isFuzzy,
|
|
295
|
-
translationOrigin: workingEntry.translationOrigin
|
|
296
|
-
};
|
|
384
|
+
function getEntryValue(entry) {
|
|
385
|
+
if (!entry?.msgstr?.length) return "";
|
|
386
|
+
return entry.msgstr.find((item) => item.trim().length > 0)?.trim() ?? "";
|
|
297
387
|
}
|
|
298
388
|
async function fileExists2(path) {
|
|
299
389
|
try {
|
|
@@ -303,27 +393,9 @@ async function fileExists2(path) {
|
|
|
303
393
|
return false;
|
|
304
394
|
}
|
|
305
395
|
}
|
|
306
|
-
async function readCatIndex(catalog) {
|
|
307
|
-
const { basePoPath, workingPoPath } = getTranslationHelperConfig();
|
|
308
|
-
let path = "";
|
|
309
|
-
if (catalog === "base") {
|
|
310
|
-
path = basePoPath;
|
|
311
|
-
} else {
|
|
312
|
-
const exists = await fileExists2(workingPoPath);
|
|
313
|
-
if (!exists) return null;
|
|
314
|
-
path = workingPoPath;
|
|
315
|
-
}
|
|
316
|
-
const raw = await readFile2(path);
|
|
317
|
-
const parsed = gettextParser.po.parse(raw);
|
|
318
|
-
const entries = flattenPoEntries(parsed, catalog);
|
|
319
|
-
return {
|
|
320
|
-
entries,
|
|
321
|
-
fuse: createFuse(entries)
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
396
|
async function readParsedCatalog(catalog) {
|
|
325
|
-
const { basePoPath
|
|
326
|
-
const path = catalog === "base" ? basePoPath :
|
|
397
|
+
const { basePoPath } = getTranslationHelperConfig();
|
|
398
|
+
const path = catalog === "base" ? basePoPath : await getEffectiveWorkingPoPath();
|
|
327
399
|
if (catalog === "working") {
|
|
328
400
|
const exists = await fileExists2(path);
|
|
329
401
|
if (!exists) return null;
|
|
@@ -338,15 +410,183 @@ async function ensureWorkingCatalog() {
|
|
|
338
410
|
await copyFile(basePoPath, workingPoPath);
|
|
339
411
|
}
|
|
340
412
|
}
|
|
413
|
+
function getWorkingDraftPoPath() {
|
|
414
|
+
const { workingPoPath } = getTranslationHelperConfig();
|
|
415
|
+
const extension = extname2(workingPoPath);
|
|
416
|
+
const stem = workingPoPath.slice(0, workingPoPath.length - extension.length);
|
|
417
|
+
return `${stem}.angy-draft${extension}`;
|
|
418
|
+
}
|
|
419
|
+
async function getEffectiveWorkingPoPath() {
|
|
420
|
+
const draftPoPath = getWorkingDraftPoPath();
|
|
421
|
+
if (await fileExists2(draftPoPath)) {
|
|
422
|
+
return draftPoPath;
|
|
423
|
+
}
|
|
424
|
+
return getTranslationHelperConfig().workingPoPath;
|
|
425
|
+
}
|
|
426
|
+
async function ensureWorkingDraftCatalog() {
|
|
427
|
+
await ensureWorkingCatalog();
|
|
428
|
+
const { workingPoPath } = getTranslationHelperConfig();
|
|
429
|
+
const draftPoPath = getWorkingDraftPoPath();
|
|
430
|
+
const exists = await fileExists2(draftPoPath);
|
|
431
|
+
if (!exists) {
|
|
432
|
+
await copyFile(workingPoPath, draftPoPath);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
341
435
|
function removeFuzzyFlag(flagString) {
|
|
342
436
|
if (!flagString) return "";
|
|
343
437
|
return flagString.split(",").map((item) => item.trim()).filter(Boolean).filter((item) => item !== "fuzzy").join(", ");
|
|
344
438
|
}
|
|
345
439
|
async function writeWorkingCatalog(parsed) {
|
|
440
|
+
await ensureWorkingDraftCatalog();
|
|
441
|
+
const draftPoPath = getWorkingDraftPoPath();
|
|
442
|
+
const compiled = gettextParser.po.compile(parsed);
|
|
443
|
+
await writeFile2(draftPoPath, compiled);
|
|
444
|
+
}
|
|
445
|
+
async function promoteWorkingDraftToRuntime() {
|
|
446
|
+
await ensureWorkingDraftCatalog();
|
|
346
447
|
const { workingPoPath } = getTranslationHelperConfig();
|
|
448
|
+
const draftPoPath = getWorkingDraftPoPath();
|
|
449
|
+
const publishPath = `${workingPoPath}.angy-publish`;
|
|
450
|
+
const compiled = await readFile2(draftPoPath);
|
|
347
451
|
suspendWorkingCatalogWatch(workingPoPath);
|
|
348
|
-
|
|
349
|
-
await
|
|
452
|
+
await writeFile2(publishPath, compiled);
|
|
453
|
+
await rm(workingPoPath, { force: true });
|
|
454
|
+
await rename(publishPath, workingPoPath);
|
|
455
|
+
}
|
|
456
|
+
function collectCatalogIntegrityIssues(baseEntries, workingEntries) {
|
|
457
|
+
const issues = [];
|
|
458
|
+
const baseMap = buildEntryMap(baseEntries);
|
|
459
|
+
const workingMap = buildEntryMap(workingEntries);
|
|
460
|
+
for (const baseEntry of baseEntries) {
|
|
461
|
+
const key = catalogEntryKey(baseEntry.msgid, baseEntry.msgctxt);
|
|
462
|
+
const workingEntry = workingMap.get(key);
|
|
463
|
+
if (!workingEntry) {
|
|
464
|
+
issues.push({
|
|
465
|
+
type: "key_mismatch",
|
|
466
|
+
msgid: baseEntry.msgid,
|
|
467
|
+
msgctxt: baseEntry.msgctxt,
|
|
468
|
+
reason: "missing_in_working"
|
|
469
|
+
});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const baseValue = getEntryValue(baseEntry);
|
|
473
|
+
const workingValue = getEntryValue(workingEntry);
|
|
474
|
+
if (baseValue && !workingValue) {
|
|
475
|
+
issues.push({
|
|
476
|
+
type: "missing_working_translation",
|
|
477
|
+
msgid: baseEntry.msgid,
|
|
478
|
+
msgctxt: baseEntry.msgctxt,
|
|
479
|
+
baseValue
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const workingEntry of workingEntries) {
|
|
484
|
+
const key = catalogEntryKey(workingEntry.msgid, workingEntry.msgctxt);
|
|
485
|
+
if (!baseMap.has(key)) {
|
|
486
|
+
issues.push({
|
|
487
|
+
type: "key_mismatch",
|
|
488
|
+
msgid: workingEntry.msgid,
|
|
489
|
+
msgctxt: workingEntry.msgctxt,
|
|
490
|
+
reason: "missing_in_base"
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return issues;
|
|
495
|
+
}
|
|
496
|
+
function assertCatalogIntegrity(baseEntries, workingEntries) {
|
|
497
|
+
const issues = collectCatalogIntegrityIssues(baseEntries, workingEntries);
|
|
498
|
+
if (issues.length) {
|
|
499
|
+
throw new CatalogIntegrityError(
|
|
500
|
+
"Working catalog is out of sync with the base catalog. Regenerate or replace the working catalog before using Angy.",
|
|
501
|
+
issues
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function collectRotationImpact(baseEntries, workingMap) {
|
|
506
|
+
const impacted = [];
|
|
507
|
+
for (const baseEntry of baseEntries) {
|
|
508
|
+
const workingEntry = workingMap.get(catalogEntryKey(baseEntry.msgid, baseEntry.msgctxt));
|
|
509
|
+
if (!workingEntry) continue;
|
|
510
|
+
const baseValue = getEntryValue(baseEntry);
|
|
511
|
+
const workingValue = getEntryValue(workingEntry);
|
|
512
|
+
if (!workingValue) continue;
|
|
513
|
+
if (baseValue === workingValue) continue;
|
|
514
|
+
impacted.push({
|
|
515
|
+
msgid: baseEntry.msgid,
|
|
516
|
+
msgctxt: baseEntry.msgctxt,
|
|
517
|
+
baseValue,
|
|
518
|
+
workingValue
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
return impacted;
|
|
522
|
+
}
|
|
523
|
+
function resolveTranslationState(baseEntry, workingEntry) {
|
|
524
|
+
const baseValue = getEntryValue(baseEntry);
|
|
525
|
+
const workingValue = getEntryValue(workingEntry);
|
|
526
|
+
const activeEntry = workingEntry && workingValue ? workingEntry : baseEntry;
|
|
527
|
+
const fuzzy = Boolean(activeEntry.isFuzzy);
|
|
528
|
+
let translationOrigin = "base";
|
|
529
|
+
let translationStatus = "none";
|
|
530
|
+
let effectiveEntry = baseEntry;
|
|
531
|
+
if (workingEntry && workingValue) {
|
|
532
|
+
effectiveEntry = workingEntry;
|
|
533
|
+
if (!baseValue || workingValue !== baseValue) {
|
|
534
|
+
translationOrigin = "working";
|
|
535
|
+
translationStatus = "working";
|
|
536
|
+
} else {
|
|
537
|
+
translationOrigin = "base";
|
|
538
|
+
translationStatus = "base";
|
|
539
|
+
}
|
|
540
|
+
} else if (baseValue) {
|
|
541
|
+
effectiveEntry = baseEntry;
|
|
542
|
+
translationOrigin = "base";
|
|
543
|
+
translationStatus = "base";
|
|
544
|
+
}
|
|
545
|
+
if (fuzzy) {
|
|
546
|
+
translationStatus = "fuzzy";
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
effectiveEntry,
|
|
550
|
+
baseValue,
|
|
551
|
+
workingValue,
|
|
552
|
+
translationOrigin,
|
|
553
|
+
translationStatus,
|
|
554
|
+
hasTranslation: Boolean(getEntryValue(effectiveEntry)),
|
|
555
|
+
isFuzzy: fuzzy
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
async function readCatalogPair(options) {
|
|
559
|
+
if (options?.ensureWorking) {
|
|
560
|
+
await ensureWorkingCatalog();
|
|
561
|
+
}
|
|
562
|
+
const [baseParsed, workingParsed] = await Promise.all([
|
|
563
|
+
readParsedCatalog("base"),
|
|
564
|
+
readParsedCatalog("working")
|
|
565
|
+
]);
|
|
566
|
+
if (!baseParsed || !workingParsed) {
|
|
567
|
+
throw new Error("Unable to load base and working catalogs.");
|
|
568
|
+
}
|
|
569
|
+
const baseEntries = flattenPoEntries(baseParsed, "base");
|
|
570
|
+
const workingEntries = flattenPoEntries(workingParsed, "working");
|
|
571
|
+
assertCatalogIntegrity(baseEntries, workingEntries);
|
|
572
|
+
const baseMap = buildEntryMap(baseEntries);
|
|
573
|
+
const workingMap = buildEntryMap(workingEntries);
|
|
574
|
+
const rotationImpact = collectRotationImpact(baseEntries, workingMap);
|
|
575
|
+
return {
|
|
576
|
+
baseEntries,
|
|
577
|
+
workingEntries,
|
|
578
|
+
baseMap,
|
|
579
|
+
workingMap,
|
|
580
|
+
rotationImpact
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
async function getRotationPreflight() {
|
|
584
|
+
const { rotationImpact } = await readCatalogPair({ ensureWorking: true });
|
|
585
|
+
return {
|
|
586
|
+
safe: true,
|
|
587
|
+
status: "ok",
|
|
588
|
+
affected: rotationImpact
|
|
589
|
+
};
|
|
350
590
|
}
|
|
351
591
|
function timestampForBackup() {
|
|
352
592
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -357,18 +597,21 @@ function buildBackupPath(path, label) {
|
|
|
357
597
|
const stem = basename2(path, extension);
|
|
358
598
|
return join(directory, `${stem}.${label}.${timestampForBackup()}${extension}`);
|
|
359
599
|
}
|
|
360
|
-
async function rotateCatalogs() {
|
|
600
|
+
async function rotateCatalogs(options) {
|
|
361
601
|
const { basePoPath, workingPoPath } = getTranslationHelperConfig();
|
|
362
|
-
const
|
|
602
|
+
const draftPoPath = getWorkingDraftPoPath();
|
|
603
|
+
const effectiveWorkingPoPath = await getEffectiveWorkingPoPath();
|
|
604
|
+
const workingExists = await fileExists2(effectiveWorkingPoPath);
|
|
363
605
|
if (!workingExists) {
|
|
364
606
|
return { ok: false, error: "Working catalog does not exist" };
|
|
365
607
|
}
|
|
366
608
|
const baseBackupPath = buildBackupPath(basePoPath, "base-backup");
|
|
367
609
|
const workingBackupPath = buildBackupPath(workingPoPath, "working-backup");
|
|
368
610
|
await copyFile(basePoPath, baseBackupPath);
|
|
369
|
-
await copyFile(
|
|
370
|
-
await copyFile(
|
|
611
|
+
await copyFile(effectiveWorkingPoPath, workingBackupPath);
|
|
612
|
+
await copyFile(effectiveWorkingPoPath, basePoPath);
|
|
371
613
|
await copyFile(basePoPath, workingPoPath);
|
|
614
|
+
await copyFile(workingPoPath, draftPoPath);
|
|
372
615
|
return {
|
|
373
616
|
ok: true,
|
|
374
617
|
basePoPath,
|
|
@@ -384,10 +627,8 @@ function runtimeKey(msgid, msgctxt) {
|
|
|
384
627
|
}
|
|
385
628
|
async function writeTranslationToWorkingCatalog(resolvedMsgid, resolvedMsgctxt, translationValue) {
|
|
386
629
|
await ensureWorkingCatalog();
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
readParsedCatalog("working")
|
|
390
|
-
]);
|
|
630
|
+
await readCatalogPair({ ensureWorking: true });
|
|
631
|
+
const [baseParsed, workingParsed] = await Promise.all([readParsedCatalog("base"), readParsedCatalog("working")]);
|
|
391
632
|
if (!baseParsed || !workingParsed) {
|
|
392
633
|
return { ok: false, error: "Unable to load catalogs" };
|
|
393
634
|
}
|
|
@@ -409,11 +650,12 @@ async function writeTranslationToWorkingCatalog(resolvedMsgid, resolvedMsgctxt,
|
|
|
409
650
|
entry.comments.flag = removeFuzzyFlag(entry.comments.flag);
|
|
410
651
|
await writeWorkingCatalog(workingParsed);
|
|
411
652
|
runtimeTranslations.set(runtimeKey(resolvedMsgid, resolvedMsgctxt), translationValue);
|
|
653
|
+
const workingCatalogName = inferLocaleFromCatalogPath(getTranslationHelperConfig().workingPoPath) ?? "working";
|
|
412
654
|
return {
|
|
413
655
|
ok: true,
|
|
414
656
|
msgid: resolvedMsgid,
|
|
415
657
|
msgctxt: resolvedMsgctxt,
|
|
416
|
-
workingCatalog:
|
|
658
|
+
workingCatalog: workingCatalogName
|
|
417
659
|
};
|
|
418
660
|
}
|
|
419
661
|
function cloneEntryForWorking(baseEntry, msgid, msgctxt) {
|
|
@@ -443,6 +685,22 @@ async function handleCommitBatch(request) {
|
|
|
443
685
|
{ status: 400 }
|
|
444
686
|
);
|
|
445
687
|
}
|
|
688
|
+
try {
|
|
689
|
+
await readCatalogPair({ ensureWorking: true });
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (error instanceof CatalogIntegrityError) {
|
|
692
|
+
return json(
|
|
693
|
+
{
|
|
694
|
+
success: false,
|
|
695
|
+
error: error.message,
|
|
696
|
+
code: "catalog_integrity_error",
|
|
697
|
+
issues: error.issues
|
|
698
|
+
},
|
|
699
|
+
{ status: 409 }
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
446
704
|
const results = [];
|
|
447
705
|
for (const item of items) {
|
|
448
706
|
const resolvedMsgid = item.resolvedMsgid?.trim();
|
|
@@ -490,7 +748,7 @@ async function handleCommitBatch(request) {
|
|
|
490
748
|
}
|
|
491
749
|
return json({
|
|
492
750
|
success: true,
|
|
493
|
-
message: `Committed ${results.length} translations to
|
|
751
|
+
message: `Committed ${results.length} translations to Angy draft working catalog`,
|
|
494
752
|
results
|
|
495
753
|
});
|
|
496
754
|
}
|
|
@@ -525,21 +783,85 @@ async function handleCommit(request) {
|
|
|
525
783
|
}
|
|
526
784
|
return json({
|
|
527
785
|
success: true,
|
|
528
|
-
message: "Translation written to
|
|
786
|
+
message: "Translation written to Angy draft working catalog",
|
|
529
787
|
msgid: result.msgid,
|
|
530
788
|
msgctxt: result.msgctxt,
|
|
531
789
|
workingCatalog: result.workingCatalog
|
|
532
790
|
});
|
|
533
791
|
}
|
|
534
|
-
async function
|
|
535
|
-
|
|
792
|
+
async function handlePromoteWorkingPreview() {
|
|
793
|
+
try {
|
|
794
|
+
await readCatalogPair({ ensureWorking: true });
|
|
795
|
+
await promoteWorkingDraftToRuntime();
|
|
796
|
+
return json({
|
|
797
|
+
success: true,
|
|
798
|
+
message: `Promoted Angy draft into ${basename3(getTranslationHelperConfig().workingPoPath)}`
|
|
799
|
+
});
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (error instanceof CatalogIntegrityError) {
|
|
802
|
+
return json(
|
|
803
|
+
{
|
|
804
|
+
success: false,
|
|
805
|
+
error: error.message,
|
|
806
|
+
code: "catalog_integrity_error",
|
|
807
|
+
issues: error.issues
|
|
808
|
+
},
|
|
809
|
+
{ status: 409 }
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
throw error;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function handleRotatePreflight() {
|
|
816
|
+
try {
|
|
817
|
+
const preflight = await getRotationPreflight();
|
|
818
|
+
return json({
|
|
819
|
+
success: true,
|
|
820
|
+
safe: preflight.safe,
|
|
821
|
+
status: preflight.status,
|
|
822
|
+
affected: preflight.affected
|
|
823
|
+
});
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (error instanceof CatalogIntegrityError) {
|
|
826
|
+
return json(
|
|
827
|
+
{
|
|
828
|
+
success: false,
|
|
829
|
+
error: error.message,
|
|
830
|
+
code: "catalog_integrity_error",
|
|
831
|
+
issues: error.issues
|
|
832
|
+
},
|
|
833
|
+
{ status: 409 }
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async function handleRotateCatalogs(request) {
|
|
840
|
+
const data = await request.json().catch(() => null);
|
|
841
|
+
const confirmDestructive = data?.confirmDestructive === true;
|
|
842
|
+
const preflight = await getRotationPreflight();
|
|
843
|
+
if (!preflight.safe && !confirmDestructive) {
|
|
844
|
+
return json(
|
|
845
|
+
{
|
|
846
|
+
success: false,
|
|
847
|
+
error: "Catalog rotation would overwrite working-only translations. Confirm rotation explicitly to continue.",
|
|
848
|
+
code: "rotation_confirmation_required",
|
|
849
|
+
status: preflight.status,
|
|
850
|
+
affected: preflight.affected
|
|
851
|
+
},
|
|
852
|
+
{ status: 409 }
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const result = await rotateCatalogs({ allowOutOfSync: confirmDestructive });
|
|
536
856
|
if (!result.ok) {
|
|
537
857
|
return json(
|
|
538
858
|
{
|
|
539
859
|
success: false,
|
|
540
|
-
error: result.error
|
|
860
|
+
error: result.error,
|
|
861
|
+
status: "status" in result ? result.status : void 0,
|
|
862
|
+
affected: "affected" in result ? result.affected : void 0
|
|
541
863
|
},
|
|
542
|
-
{ status: 400 }
|
|
864
|
+
{ status: "status" in result && result.status === "out_of_sync" ? 409 : 400 }
|
|
543
865
|
);
|
|
544
866
|
}
|
|
545
867
|
return json({
|
|
@@ -725,13 +1047,16 @@ function buildAlternativePool(topDirectAlternatives, sharedReferenceAlternatives
|
|
|
725
1047
|
return alternatives.slice(0, MAX_ALTERNATIVES);
|
|
726
1048
|
}
|
|
727
1049
|
async function findTranslationContext(key, currentPath) {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1050
|
+
const catalogPair = await readCatalogPair({ ensureWorking: true });
|
|
1051
|
+
const effectiveEntries = catalogPair.baseEntries.map((entry) => {
|
|
1052
|
+
const state = resolveTranslationState(entry, catalogPair.workingMap.get(entryKey(entry.msgid, entry.msgctxt)));
|
|
1053
|
+
return {
|
|
1054
|
+
...state.effectiveEntry,
|
|
1055
|
+
hasTranslation: state.hasTranslation,
|
|
1056
|
+
isFuzzy: state.isFuzzy,
|
|
1057
|
+
translationOrigin: state.translationOrigin
|
|
1058
|
+
};
|
|
1059
|
+
});
|
|
735
1060
|
const effectiveIndex = {
|
|
736
1061
|
entries: effectiveEntries,
|
|
737
1062
|
fuse: createFuse(effectiveEntries)
|
|
@@ -789,47 +1114,48 @@ async function findTranslationContext(key, currentPath) {
|
|
|
789
1114
|
untranslatedSimilarityAlternatives
|
|
790
1115
|
),
|
|
791
1116
|
routeReference,
|
|
792
|
-
|
|
1117
|
+
baseMap: catalogPair.baseMap,
|
|
1118
|
+
workingMap: catalogPair.workingMap,
|
|
1119
|
+
catalogState: {
|
|
1120
|
+
status: "ok",
|
|
1121
|
+
affectedCount: catalogPair.rotationImpact.length
|
|
1122
|
+
}
|
|
793
1123
|
};
|
|
794
1124
|
}
|
|
795
1125
|
function toPublicEntry(entry, workingEntry) {
|
|
796
|
-
const
|
|
797
|
-
const baseValue = entry.msgstr[0] ?? "";
|
|
798
|
-
const workingValue = workingEntry?.msgstr[0] ?? "";
|
|
799
|
-
const workingDiffersFromBase = Boolean(workingEntry?.hasTranslation) && workingValue !== baseValue;
|
|
1126
|
+
const state = resolveTranslationState(entry, workingEntry);
|
|
800
1127
|
return {
|
|
801
1128
|
msgid: entry.msgid,
|
|
802
1129
|
msgctxt: entry.msgctxt,
|
|
803
1130
|
msgidPlural: entry.msgidPlural,
|
|
804
|
-
msgstr:
|
|
1131
|
+
msgstr: state.effectiveEntry.msgstr,
|
|
805
1132
|
references: entry.references,
|
|
806
1133
|
extractedComments: entry.extractedComments,
|
|
807
|
-
flags:
|
|
808
|
-
previous:
|
|
809
|
-
obsolete:
|
|
810
|
-
hasTranslation:
|
|
811
|
-
isFuzzy:
|
|
812
|
-
isCommittedToWorking:
|
|
813
|
-
matchesTargetTranslation:
|
|
814
|
-
translationOrigin:
|
|
1134
|
+
flags: state.effectiveEntry.flags,
|
|
1135
|
+
previous: state.effectiveEntry.previous,
|
|
1136
|
+
obsolete: state.effectiveEntry.obsolete,
|
|
1137
|
+
hasTranslation: state.hasTranslation,
|
|
1138
|
+
isFuzzy: state.isFuzzy,
|
|
1139
|
+
isCommittedToWorking: state.translationOrigin === "working",
|
|
1140
|
+
matchesTargetTranslation: state.translationOrigin === "base" && state.hasTranslation,
|
|
1141
|
+
translationOrigin: state.translationOrigin,
|
|
1142
|
+
translationStatus: state.translationStatus
|
|
815
1143
|
};
|
|
816
1144
|
}
|
|
817
1145
|
function toPublicAlternative(entry, score, workingEntry) {
|
|
818
|
-
const
|
|
819
|
-
const baseValue = entry.msgstr[0] ?? "";
|
|
820
|
-
const workingValue = workingEntry?.msgstr[0] ?? "";
|
|
821
|
-
const workingDiffersFromBase = Boolean(workingEntry?.hasTranslation) && workingValue !== baseValue;
|
|
1146
|
+
const state = resolveTranslationState(entry, workingEntry);
|
|
822
1147
|
return {
|
|
823
1148
|
msgid: entry.msgid,
|
|
824
1149
|
msgctxt: entry.msgctxt,
|
|
825
1150
|
score,
|
|
826
1151
|
references: entry.references,
|
|
827
|
-
msgstr:
|
|
828
|
-
hasTranslation:
|
|
829
|
-
isFuzzy:
|
|
830
|
-
isCommittedToWorking:
|
|
831
|
-
matchesTargetTranslation:
|
|
832
|
-
translationOrigin:
|
|
1152
|
+
msgstr: state.effectiveEntry.msgstr,
|
|
1153
|
+
hasTranslation: state.hasTranslation,
|
|
1154
|
+
isFuzzy: state.isFuzzy,
|
|
1155
|
+
isCommittedToWorking: state.translationOrigin === "working",
|
|
1156
|
+
matchesTargetTranslation: state.translationOrigin === "base" && state.hasTranslation,
|
|
1157
|
+
translationOrigin: state.translationOrigin,
|
|
1158
|
+
translationStatus: state.translationStatus
|
|
833
1159
|
};
|
|
834
1160
|
}
|
|
835
1161
|
async function handleContext(request) {
|
|
@@ -845,7 +1171,23 @@ async function handleContext(request) {
|
|
|
845
1171
|
{ status: 400 }
|
|
846
1172
|
);
|
|
847
1173
|
}
|
|
848
|
-
|
|
1174
|
+
let baseFound;
|
|
1175
|
+
try {
|
|
1176
|
+
baseFound = await findTranslationContext(key, currentPath);
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
if (error instanceof CatalogIntegrityError) {
|
|
1179
|
+
return json2(
|
|
1180
|
+
{
|
|
1181
|
+
success: false,
|
|
1182
|
+
error: error.message,
|
|
1183
|
+
code: "catalog_integrity_error",
|
|
1184
|
+
issues: error.issues
|
|
1185
|
+
},
|
|
1186
|
+
{ status: 409 }
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
849
1191
|
if (!baseFound) {
|
|
850
1192
|
return json2(
|
|
851
1193
|
{
|
|
@@ -855,7 +1197,7 @@ async function handleContext(request) {
|
|
|
855
1197
|
{ status: 404 }
|
|
856
1198
|
);
|
|
857
1199
|
}
|
|
858
|
-
const bestBase = baseFound.best.entry;
|
|
1200
|
+
const bestBase = baseFound.baseMap.get(entryKey(baseFound.best.entry.msgid, baseFound.best.entry.msgctxt)) ?? baseFound.best.entry;
|
|
859
1201
|
const bestWorking = baseFound.workingMap.get(entryKey(bestBase.msgid, bestBase.msgctxt));
|
|
860
1202
|
return json2({
|
|
861
1203
|
success: true,
|
|
@@ -863,10 +1205,12 @@ async function handleContext(request) {
|
|
|
863
1205
|
score: baseFound.best.score,
|
|
864
1206
|
via: baseFound.best.variant
|
|
865
1207
|
},
|
|
1208
|
+
catalogState: baseFound.catalogState,
|
|
866
1209
|
entry: toPublicEntry(bestBase, bestWorking),
|
|
867
1210
|
alternatives: baseFound.alternatives.map((item) => {
|
|
868
|
-
const
|
|
869
|
-
|
|
1211
|
+
const baseAlt = baseFound.baseMap.get(entryKey(item.entry.msgid, item.entry.msgctxt)) ?? item.entry;
|
|
1212
|
+
const workingAlt = baseFound.workingMap.get(entryKey(baseAlt.msgid, baseAlt.msgctxt));
|
|
1213
|
+
return toPublicAlternative(baseAlt, item.score, workingAlt);
|
|
870
1214
|
})
|
|
871
1215
|
});
|
|
872
1216
|
}
|
|
@@ -914,6 +1258,58 @@ function parseSuggestions(responseText) {
|
|
|
914
1258
|
return [];
|
|
915
1259
|
}
|
|
916
1260
|
}
|
|
1261
|
+
function buildSuggestionRequestBody({
|
|
1262
|
+
context,
|
|
1263
|
+
items,
|
|
1264
|
+
modelConfig,
|
|
1265
|
+
systemMessage
|
|
1266
|
+
}) {
|
|
1267
|
+
const body = {
|
|
1268
|
+
model: modelConfig.model,
|
|
1269
|
+
input: [
|
|
1270
|
+
{
|
|
1271
|
+
role: "system",
|
|
1272
|
+
content: [{ type: "input_text", text: systemMessage }]
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
role: "user",
|
|
1276
|
+
content: [{ type: "input_text", text: buildUserMessage(context, items) }]
|
|
1277
|
+
}
|
|
1278
|
+
],
|
|
1279
|
+
text: {
|
|
1280
|
+
format: {
|
|
1281
|
+
type: "json_schema",
|
|
1282
|
+
name: "translation_suggestions",
|
|
1283
|
+
schema: {
|
|
1284
|
+
type: "object",
|
|
1285
|
+
additionalProperties: false,
|
|
1286
|
+
properties: {
|
|
1287
|
+
items: {
|
|
1288
|
+
type: "array",
|
|
1289
|
+
items: {
|
|
1290
|
+
type: "object",
|
|
1291
|
+
additionalProperties: false,
|
|
1292
|
+
properties: {
|
|
1293
|
+
msgid: { type: "string" },
|
|
1294
|
+
msgctxt: { type: ["string", "null"] },
|
|
1295
|
+
suggestion: { type: "string" }
|
|
1296
|
+
},
|
|
1297
|
+
required: ["msgid", "msgctxt", "suggestion"]
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
required: ["items"]
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
if (modelConfig.reasoning && modelConfig.reasoning !== "none") {
|
|
1307
|
+
body.reasoning = {
|
|
1308
|
+
effort: modelConfig.reasoning
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
return body;
|
|
1312
|
+
}
|
|
917
1313
|
async function handleSuggestions(request) {
|
|
918
1314
|
const {
|
|
919
1315
|
apiKey,
|
|
@@ -942,7 +1338,7 @@ async function handleSuggestions(request) {
|
|
|
942
1338
|
sourceLocale,
|
|
943
1339
|
targetLocale,
|
|
944
1340
|
systemMessage,
|
|
945
|
-
|
|
1341
|
+
suggestionModel,
|
|
946
1342
|
apiKey
|
|
947
1343
|
});
|
|
948
1344
|
return json3({
|
|
@@ -960,7 +1356,8 @@ async function handleSuggestions(request) {
|
|
|
960
1356
|
}
|
|
961
1357
|
try {
|
|
962
1358
|
console.info("[angy] Suggestion request starting.", {
|
|
963
|
-
model: suggestionModel,
|
|
1359
|
+
model: suggestionModel.model,
|
|
1360
|
+
reasoning: suggestionModel.reasoning ?? null,
|
|
964
1361
|
sourceLocale,
|
|
965
1362
|
targetLocale,
|
|
966
1363
|
itemCount: items.length,
|
|
@@ -972,48 +1369,14 @@ async function handleSuggestions(request) {
|
|
|
972
1369
|
Authorization: `Bearer ${apiKey}`,
|
|
973
1370
|
"content-type": "application/json"
|
|
974
1371
|
},
|
|
975
|
-
body: JSON.stringify(
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
content: [{ type: "input_text", text: systemMessage }]
|
|
984
|
-
},
|
|
985
|
-
{
|
|
986
|
-
role: "user",
|
|
987
|
-
content: [{ type: "input_text", text: buildUserMessage(context, items) }]
|
|
988
|
-
}
|
|
989
|
-
],
|
|
990
|
-
text: {
|
|
991
|
-
format: {
|
|
992
|
-
type: "json_schema",
|
|
993
|
-
name: "translation_suggestions",
|
|
994
|
-
schema: {
|
|
995
|
-
type: "object",
|
|
996
|
-
additionalProperties: false,
|
|
997
|
-
properties: {
|
|
998
|
-
items: {
|
|
999
|
-
type: "array",
|
|
1000
|
-
items: {
|
|
1001
|
-
type: "object",
|
|
1002
|
-
additionalProperties: false,
|
|
1003
|
-
properties: {
|
|
1004
|
-
msgid: { type: "string" },
|
|
1005
|
-
msgctxt: { type: ["string", "null"] },
|
|
1006
|
-
suggestion: { type: "string" }
|
|
1007
|
-
},
|
|
1008
|
-
required: ["msgid", "msgctxt", "suggestion"]
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
},
|
|
1012
|
-
required: ["items"]
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
})
|
|
1372
|
+
body: JSON.stringify(
|
|
1373
|
+
buildSuggestionRequestBody({
|
|
1374
|
+
context,
|
|
1375
|
+
items,
|
|
1376
|
+
modelConfig: suggestionModel,
|
|
1377
|
+
systemMessage
|
|
1378
|
+
})
|
|
1379
|
+
)
|
|
1017
1380
|
});
|
|
1018
1381
|
if (!response.ok) {
|
|
1019
1382
|
const errorBody = await response.text().catch(() => "");
|
|
@@ -1078,7 +1441,13 @@ async function handleTranslationRequest(request, url, options) {
|
|
|
1078
1441
|
return handleSuggestions(request);
|
|
1079
1442
|
}
|
|
1080
1443
|
if (intent === "rotate-catalogs") {
|
|
1081
|
-
return handleRotateCatalogs();
|
|
1444
|
+
return handleRotateCatalogs(request);
|
|
1445
|
+
}
|
|
1446
|
+
if (intent === "rotate-preflight") {
|
|
1447
|
+
return handleRotatePreflight();
|
|
1448
|
+
}
|
|
1449
|
+
if (intent === "promote-working-preview") {
|
|
1450
|
+
return handlePromoteWorkingPreview();
|
|
1082
1451
|
}
|
|
1083
1452
|
return handleCommit(request);
|
|
1084
1453
|
}
|
|
@@ -1087,8 +1456,16 @@ function createAngyHandler(options) {
|
|
|
1087
1456
|
}
|
|
1088
1457
|
var handler = createAngyHandler();
|
|
1089
1458
|
export {
|
|
1459
|
+
CatalogIntegrityError,
|
|
1460
|
+
buildSuggestionRequestBody,
|
|
1461
|
+
collectCatalogIntegrityIssues,
|
|
1462
|
+
collectRotationImpact,
|
|
1090
1463
|
defineAngyConfig,
|
|
1464
|
+
getRotationPreflight,
|
|
1091
1465
|
handleTranslationRequest,
|
|
1092
|
-
handler
|
|
1466
|
+
handler,
|
|
1467
|
+
normalizeSuggestionModelConfig,
|
|
1468
|
+
readCatalogPair,
|
|
1469
|
+
resolveTranslationState
|
|
1093
1470
|
};
|
|
1094
1471
|
//# sourceMappingURL=server.js.map
|