glotfile 0.4.4 → 0.4.5
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/server/cli.js +94 -42
- package/dist/server/server.js +79 -42
- package/package.json +1 -1
package/dist/server/cli.js
CHANGED
|
@@ -1853,6 +1853,7 @@ function buildSystemPrompt(hasPluralItems) {
|
|
|
1853
1853
|
"- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
|
|
1854
1854
|
"- Glossary: a term marked do-not-translate MUST appear unchanged in the translation. A term with a forced translation for the target locale MUST use that exact translation.",
|
|
1855
1855
|
"- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
|
|
1856
|
+
`- Quotation marks inside a translation MUST use the target language's typographic quote characters (e.g. \u201EGerman\u201C, \xABFrench\xBB, \u201CEnglish\u201D, \u2019 for apostrophes). Never emit a raw ASCII double-quote (") inside a translated string \u2014 it corrupts the JSON reply. If the source uses ASCII quotes, convert them to the target language's typographic quotes.`,
|
|
1856
1857
|
"- Match the register and capitalization conventions of the target language and of UI microcopy.",
|
|
1857
1858
|
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
1858
1859
|
];
|
|
@@ -1942,6 +1943,16 @@ var init_provider = __esm({
|
|
|
1942
1943
|
});
|
|
1943
1944
|
|
|
1944
1945
|
// src/server/ai/batch.ts
|
|
1946
|
+
function parseReplyItems(text) {
|
|
1947
|
+
let parsed;
|
|
1948
|
+
try {
|
|
1949
|
+
parsed = JSON.parse(text);
|
|
1950
|
+
} catch {
|
|
1951
|
+
throw new MalformedReplyError(text);
|
|
1952
|
+
}
|
|
1953
|
+
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
1954
|
+
return parsed.items;
|
|
1955
|
+
}
|
|
1945
1956
|
function chunk(items, size) {
|
|
1946
1957
|
const out = [];
|
|
1947
1958
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -1977,24 +1988,46 @@ function validatePlural(req, forms) {
|
|
|
1977
1988
|
function validateReply(req, item) {
|
|
1978
1989
|
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
1979
1990
|
}
|
|
1980
|
-
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal) {
|
|
1991
|
+
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
1992
|
+
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
1993
|
+
async function resolveBatch(batch, isRetry = false) {
|
|
1994
|
+
let reply;
|
|
1995
|
+
try {
|
|
1996
|
+
reply = await callBatch(batch, signal);
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
if (!(err instanceof MalformedReplyError)) throw err;
|
|
1999
|
+
onMalformedReply?.(err.raw, batch.length);
|
|
2000
|
+
if (signal?.aborted) return failBatch(batch);
|
|
2001
|
+
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
2002
|
+
const mid = Math.ceil(batch.length / 2);
|
|
2003
|
+
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
2004
|
+
}
|
|
2005
|
+
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
2006
|
+
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
2007
|
+
}
|
|
1981
2008
|
const results = [];
|
|
1982
2009
|
const total = reqs.length;
|
|
1983
2010
|
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
1984
2011
|
if (signal?.aborted) break;
|
|
1985
|
-
const
|
|
1986
|
-
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
1987
|
-
const batchResults = [];
|
|
1988
|
-
for (const req of batch) batchResults.push(validateReply(req, byId.get(req.id)));
|
|
2012
|
+
const batchResults = await resolveBatch(batch);
|
|
1989
2013
|
results.push(...batchResults);
|
|
1990
2014
|
onBatchComplete?.(results.length, total, batchResults);
|
|
1991
2015
|
}
|
|
1992
2016
|
return results;
|
|
1993
2017
|
}
|
|
2018
|
+
var MalformedReplyError;
|
|
1994
2019
|
var init_batch = __esm({
|
|
1995
2020
|
"src/server/ai/batch.ts"() {
|
|
1996
2021
|
"use strict";
|
|
1997
2022
|
init_placeholders();
|
|
2023
|
+
MalformedReplyError = class extends Error {
|
|
2024
|
+
constructor(raw) {
|
|
2025
|
+
super("Model reply was not valid translation JSON.");
|
|
2026
|
+
this.raw = raw;
|
|
2027
|
+
this.name = "MalformedReplyError";
|
|
2028
|
+
}
|
|
2029
|
+
raw;
|
|
2030
|
+
};
|
|
1998
2031
|
}
|
|
1999
2032
|
});
|
|
2000
2033
|
|
|
@@ -2023,8 +2056,8 @@ var init_anthropic = __esm({
|
|
|
2023
2056
|
supportsVision() {
|
|
2024
2057
|
return true;
|
|
2025
2058
|
}
|
|
2026
|
-
translate(reqs, onBatchComplete, signal) {
|
|
2027
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
2059
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
2060
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
2028
2061
|
}
|
|
2029
2062
|
// Build the user message as content blocks: each unique key's screenshot is
|
|
2030
2063
|
// sent once (a key recurs once per target locale in a batch — dedupe by key),
|
|
@@ -2071,13 +2104,8 @@ var init_anthropic = __esm({
|
|
|
2071
2104
|
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
2072
2105
|
messages: [{ role: "user", content }]
|
|
2073
2106
|
}, { signal });
|
|
2074
|
-
const text = res.content.find((b) => b.type === "text")?.text ?? "
|
|
2075
|
-
|
|
2076
|
-
const parsed = JSON.parse(text);
|
|
2077
|
-
return parsed.items ?? [];
|
|
2078
|
-
} catch {
|
|
2079
|
-
return [];
|
|
2080
|
-
}
|
|
2107
|
+
const text = res.content.find((b) => b.type === "text")?.text ?? "";
|
|
2108
|
+
return parseReplyItems(text);
|
|
2081
2109
|
}
|
|
2082
2110
|
};
|
|
2083
2111
|
}
|
|
@@ -2119,8 +2147,8 @@ var init_openai = __esm({
|
|
|
2119
2147
|
supportsVision() {
|
|
2120
2148
|
return true;
|
|
2121
2149
|
}
|
|
2122
|
-
translate(reqs, onBatchComplete, signal) {
|
|
2123
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
2150
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
2151
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
2124
2152
|
}
|
|
2125
2153
|
// User content as an array of parts: each unique key's screenshot once (as an
|
|
2126
2154
|
// image_url data URL), then the batch prompt text describing every item.
|
|
@@ -2169,13 +2197,8 @@ var init_openai = __esm({
|
|
|
2169
2197
|
{ role: "user", content: this.buildUserContent(batch) }
|
|
2170
2198
|
]
|
|
2171
2199
|
}, { signal });
|
|
2172
|
-
const text = res.choices?.[0]?.message?.content ?? "
|
|
2173
|
-
|
|
2174
|
-
const parsed = JSON.parse(text);
|
|
2175
|
-
return parsed.items ?? [];
|
|
2176
|
-
} catch {
|
|
2177
|
-
return [];
|
|
2178
|
-
}
|
|
2200
|
+
const text = res.choices?.[0]?.message?.content ?? "";
|
|
2201
|
+
return parseReplyItems(text);
|
|
2179
2202
|
}
|
|
2180
2203
|
};
|
|
2181
2204
|
}
|
|
@@ -2228,8 +2251,8 @@ var init_bedrock = __esm({
|
|
|
2228
2251
|
supportsVision() {
|
|
2229
2252
|
return !this.isMeta();
|
|
2230
2253
|
}
|
|
2231
|
-
translate(reqs, onBatchComplete, signal) {
|
|
2232
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
2254
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
2255
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
2233
2256
|
}
|
|
2234
2257
|
buildContentBlocks(batch) {
|
|
2235
2258
|
const blocks = [];
|
|
@@ -2293,13 +2316,8 @@ var init_bedrock = __esm({
|
|
|
2293
2316
|
const blocks = res.output?.message?.content ?? [];
|
|
2294
2317
|
const tool = blocks.find((b) => b.toolUse)?.toolUse;
|
|
2295
2318
|
if (tool?.input?.items) return tool.input.items;
|
|
2296
|
-
const text = blocks.find((b) => b.text)?.text ?? "
|
|
2297
|
-
|
|
2298
|
-
const parsed = JSON.parse(text);
|
|
2299
|
-
return parsed.items ?? [];
|
|
2300
|
-
} catch {
|
|
2301
|
-
return [];
|
|
2302
|
-
}
|
|
2319
|
+
const text = blocks.find((b) => b.text)?.text ?? "";
|
|
2320
|
+
return parseReplyItems(text);
|
|
2303
2321
|
}
|
|
2304
2322
|
};
|
|
2305
2323
|
}
|
|
@@ -2442,8 +2460,8 @@ var init_claudecode = __esm({
|
|
|
2442
2460
|
supportsVision() {
|
|
2443
2461
|
return false;
|
|
2444
2462
|
}
|
|
2445
|
-
translate(reqs, onBatchComplete, signal) {
|
|
2446
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
2463
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
2464
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
2447
2465
|
}
|
|
2448
2466
|
async complete(req) {
|
|
2449
2467
|
const systemParts = [req.system, `Respond with valid JSON matching this schema: ${JSON.stringify(req.schema)}`];
|
|
@@ -2466,12 +2484,7 @@ var init_claudecode = __esm({
|
|
|
2466
2484
|
throw err;
|
|
2467
2485
|
}
|
|
2468
2486
|
if (signal?.aborted) return [];
|
|
2469
|
-
|
|
2470
|
-
const parsed = JSON.parse(stripFences(result));
|
|
2471
|
-
return parsed.items ?? [];
|
|
2472
|
-
} catch {
|
|
2473
|
-
return [];
|
|
2474
|
-
}
|
|
2487
|
+
return parseReplyItems(stripFences(result));
|
|
2475
2488
|
}
|
|
2476
2489
|
};
|
|
2477
2490
|
}
|
|
@@ -2759,7 +2772,7 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
2759
2772
|
const localeResults = await provider.translate(group, (_localeDone, _localeTotal, batchResults) => {
|
|
2760
2773
|
done += batchResults.length;
|
|
2761
2774
|
hooks.onBatchComplete?.(done, total, batchResults, locale);
|
|
2762
|
-
}, signal);
|
|
2775
|
+
}, signal, (raw, batchSize) => hooks.onMalformedReply?.(raw, batchSize, locale));
|
|
2763
2776
|
allResults.push(...localeResults);
|
|
2764
2777
|
if (!signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
2765
2778
|
}
|
|
@@ -5266,6 +5279,19 @@ function createApi(deps) {
|
|
|
5266
5279
|
},
|
|
5267
5280
|
onLocaleDone: (locale) => {
|
|
5268
5281
|
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
5282
|
+
},
|
|
5283
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
5284
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
5285
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
5286
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
5287
|
+
appendLog(projectRoot, {
|
|
5288
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5289
|
+
kind: "translate",
|
|
5290
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
5291
|
+
model: aiCfg.model,
|
|
5292
|
+
locale,
|
|
5293
|
+
raw
|
|
5294
|
+
});
|
|
5269
5295
|
}
|
|
5270
5296
|
}, aiCfg.concurrency, signal);
|
|
5271
5297
|
if (!signal?.aborted) {
|
|
@@ -5298,7 +5324,19 @@ function createApi(deps) {
|
|
|
5298
5324
|
}
|
|
5299
5325
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
5300
5326
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
5301
|
-
const results = await runLocaleParallel(toTranslate, provider, {
|
|
5327
|
+
const results = await runLocaleParallel(toTranslate, provider, {
|
|
5328
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
5329
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
5330
|
+
appendLog(projectRoot, {
|
|
5331
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5332
|
+
kind: "translate",
|
|
5333
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
5334
|
+
model: aiCfg.model,
|
|
5335
|
+
locale,
|
|
5336
|
+
raw
|
|
5337
|
+
});
|
|
5338
|
+
}
|
|
5339
|
+
}, aiCfg.concurrency);
|
|
5302
5340
|
const latest = load();
|
|
5303
5341
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
5304
5342
|
const entry = {
|
|
@@ -5906,6 +5944,20 @@ async function runTranslate(args) {
|
|
|
5906
5944
|
errors.push(...batchApplied.errors);
|
|
5907
5945
|
saveState(args.statePath, state);
|
|
5908
5946
|
process.stdout.write(`\r ${done}/${total} translated`);
|
|
5947
|
+
},
|
|
5948
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
5949
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
5950
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
5951
|
+
console.error(`
|
|
5952
|
+
malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
5953
|
+
appendLog(projectRoot, {
|
|
5954
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5955
|
+
kind: "translate",
|
|
5956
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
5957
|
+
model: ai.model,
|
|
5958
|
+
locale,
|
|
5959
|
+
raw
|
|
5960
|
+
});
|
|
5909
5961
|
}
|
|
5910
5962
|
});
|
|
5911
5963
|
process.stdout.write("\n");
|
package/dist/server/server.js
CHANGED
|
@@ -1560,7 +1560,7 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
1560
1560
|
const localeResults = await provider.translate(group, (_localeDone, _localeTotal, batchResults) => {
|
|
1561
1561
|
done += batchResults.length;
|
|
1562
1562
|
hooks.onBatchComplete?.(done, total, batchResults, locale);
|
|
1563
|
-
}, signal);
|
|
1563
|
+
}, signal, (raw, batchSize) => hooks.onMalformedReply?.(raw, batchSize, locale));
|
|
1564
1564
|
allResults.push(...localeResults);
|
|
1565
1565
|
if (!signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
1566
1566
|
}
|
|
@@ -2869,6 +2869,7 @@ function buildSystemPrompt(hasPluralItems) {
|
|
|
2869
2869
|
"- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
|
|
2870
2870
|
"- Glossary: a term marked do-not-translate MUST appear unchanged in the translation. A term with a forced translation for the target locale MUST use that exact translation.",
|
|
2871
2871
|
"- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
|
|
2872
|
+
`- Quotation marks inside a translation MUST use the target language's typographic quote characters (e.g. \u201EGerman\u201C, \xABFrench\xBB, \u201CEnglish\u201D, \u2019 for apostrophes). Never emit a raw ASCII double-quote (") inside a translated string \u2014 it corrupts the JSON reply. If the source uses ASCII quotes, convert them to the target language's typographic quotes.`,
|
|
2872
2873
|
"- Match the register and capitalization conventions of the target language and of UI microcopy.",
|
|
2873
2874
|
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
2874
2875
|
];
|
|
@@ -2952,6 +2953,24 @@ var BATCH_SCHEMA = {
|
|
|
2952
2953
|
};
|
|
2953
2954
|
|
|
2954
2955
|
// src/server/ai/batch.ts
|
|
2956
|
+
var MalformedReplyError = class extends Error {
|
|
2957
|
+
constructor(raw) {
|
|
2958
|
+
super("Model reply was not valid translation JSON.");
|
|
2959
|
+
this.raw = raw;
|
|
2960
|
+
this.name = "MalformedReplyError";
|
|
2961
|
+
}
|
|
2962
|
+
raw;
|
|
2963
|
+
};
|
|
2964
|
+
function parseReplyItems(text) {
|
|
2965
|
+
let parsed;
|
|
2966
|
+
try {
|
|
2967
|
+
parsed = JSON.parse(text);
|
|
2968
|
+
} catch {
|
|
2969
|
+
throw new MalformedReplyError(text);
|
|
2970
|
+
}
|
|
2971
|
+
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
2972
|
+
return parsed.items;
|
|
2973
|
+
}
|
|
2955
2974
|
function chunk(items, size) {
|
|
2956
2975
|
const out = [];
|
|
2957
2976
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -2987,15 +3006,28 @@ function validatePlural(req, forms) {
|
|
|
2987
3006
|
function validateReply(req, item) {
|
|
2988
3007
|
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
2989
3008
|
}
|
|
2990
|
-
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal) {
|
|
3009
|
+
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
3010
|
+
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
3011
|
+
async function resolveBatch(batch, isRetry = false) {
|
|
3012
|
+
let reply;
|
|
3013
|
+
try {
|
|
3014
|
+
reply = await callBatch(batch, signal);
|
|
3015
|
+
} catch (err) {
|
|
3016
|
+
if (!(err instanceof MalformedReplyError)) throw err;
|
|
3017
|
+
onMalformedReply?.(err.raw, batch.length);
|
|
3018
|
+
if (signal?.aborted) return failBatch(batch);
|
|
3019
|
+
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
3020
|
+
const mid = Math.ceil(batch.length / 2);
|
|
3021
|
+
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
3022
|
+
}
|
|
3023
|
+
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
3024
|
+
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
3025
|
+
}
|
|
2991
3026
|
const results = [];
|
|
2992
3027
|
const total = reqs.length;
|
|
2993
3028
|
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
2994
3029
|
if (signal?.aborted) break;
|
|
2995
|
-
const
|
|
2996
|
-
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
2997
|
-
const batchResults = [];
|
|
2998
|
-
for (const req of batch) batchResults.push(validateReply(req, byId.get(req.id)));
|
|
3030
|
+
const batchResults = await resolveBatch(batch);
|
|
2999
3031
|
results.push(...batchResults);
|
|
3000
3032
|
onBatchComplete?.(results.length, total, batchResults);
|
|
3001
3033
|
}
|
|
@@ -3020,8 +3052,8 @@ var AnthropicProvider = class {
|
|
|
3020
3052
|
supportsVision() {
|
|
3021
3053
|
return true;
|
|
3022
3054
|
}
|
|
3023
|
-
translate(reqs, onBatchComplete, signal) {
|
|
3024
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
3055
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
3056
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
3025
3057
|
}
|
|
3026
3058
|
// Build the user message as content blocks: each unique key's screenshot is
|
|
3027
3059
|
// sent once (a key recurs once per target locale in a batch — dedupe by key),
|
|
@@ -3068,13 +3100,8 @@ var AnthropicProvider = class {
|
|
|
3068
3100
|
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
3069
3101
|
messages: [{ role: "user", content }]
|
|
3070
3102
|
}, { signal });
|
|
3071
|
-
const text = res.content.find((b) => b.type === "text")?.text ?? "
|
|
3072
|
-
|
|
3073
|
-
const parsed = JSON.parse(text);
|
|
3074
|
-
return parsed.items ?? [];
|
|
3075
|
-
} catch {
|
|
3076
|
-
return [];
|
|
3077
|
-
}
|
|
3103
|
+
const text = res.content.find((b) => b.type === "text")?.text ?? "";
|
|
3104
|
+
return parseReplyItems(text);
|
|
3078
3105
|
}
|
|
3079
3106
|
};
|
|
3080
3107
|
|
|
@@ -3108,8 +3135,8 @@ var OpenAIProvider = class {
|
|
|
3108
3135
|
supportsVision() {
|
|
3109
3136
|
return true;
|
|
3110
3137
|
}
|
|
3111
|
-
translate(reqs, onBatchComplete, signal) {
|
|
3112
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
3138
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
3139
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
3113
3140
|
}
|
|
3114
3141
|
// User content as an array of parts: each unique key's screenshot once (as an
|
|
3115
3142
|
// image_url data URL), then the batch prompt text describing every item.
|
|
@@ -3158,13 +3185,8 @@ var OpenAIProvider = class {
|
|
|
3158
3185
|
{ role: "user", content: this.buildUserContent(batch) }
|
|
3159
3186
|
]
|
|
3160
3187
|
}, { signal });
|
|
3161
|
-
const text = res.choices?.[0]?.message?.content ?? "
|
|
3162
|
-
|
|
3163
|
-
const parsed = JSON.parse(text);
|
|
3164
|
-
return parsed.items ?? [];
|
|
3165
|
-
} catch {
|
|
3166
|
-
return [];
|
|
3167
|
-
}
|
|
3188
|
+
const text = res.choices?.[0]?.message?.content ?? "";
|
|
3189
|
+
return parseReplyItems(text);
|
|
3168
3190
|
}
|
|
3169
3191
|
};
|
|
3170
3192
|
|
|
@@ -3209,8 +3231,8 @@ var BedrockProvider = class {
|
|
|
3209
3231
|
supportsVision() {
|
|
3210
3232
|
return !this.isMeta();
|
|
3211
3233
|
}
|
|
3212
|
-
translate(reqs, onBatchComplete, signal) {
|
|
3213
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
3234
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
3235
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
3214
3236
|
}
|
|
3215
3237
|
buildContentBlocks(batch) {
|
|
3216
3238
|
const blocks = [];
|
|
@@ -3274,13 +3296,8 @@ var BedrockProvider = class {
|
|
|
3274
3296
|
const blocks = res.output?.message?.content ?? [];
|
|
3275
3297
|
const tool = blocks.find((b) => b.toolUse)?.toolUse;
|
|
3276
3298
|
if (tool?.input?.items) return tool.input.items;
|
|
3277
|
-
const text = blocks.find((b) => b.text)?.text ?? "
|
|
3278
|
-
|
|
3279
|
-
const parsed = JSON.parse(text);
|
|
3280
|
-
return parsed.items ?? [];
|
|
3281
|
-
} catch {
|
|
3282
|
-
return [];
|
|
3283
|
-
}
|
|
3299
|
+
const text = blocks.find((b) => b.text)?.text ?? "";
|
|
3300
|
+
return parseReplyItems(text);
|
|
3284
3301
|
}
|
|
3285
3302
|
};
|
|
3286
3303
|
|
|
@@ -3400,8 +3417,8 @@ var ClaudeCodeProvider = class {
|
|
|
3400
3417
|
supportsVision() {
|
|
3401
3418
|
return false;
|
|
3402
3419
|
}
|
|
3403
|
-
translate(reqs, onBatchComplete, signal) {
|
|
3404
|
-
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal);
|
|
3420
|
+
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
3421
|
+
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
3405
3422
|
}
|
|
3406
3423
|
async complete(req) {
|
|
3407
3424
|
const systemParts = [req.system, `Respond with valid JSON matching this schema: ${JSON.stringify(req.schema)}`];
|
|
@@ -3424,12 +3441,7 @@ var ClaudeCodeProvider = class {
|
|
|
3424
3441
|
throw err;
|
|
3425
3442
|
}
|
|
3426
3443
|
if (signal?.aborted) return [];
|
|
3427
|
-
|
|
3428
|
-
const parsed = JSON.parse(stripFences(result));
|
|
3429
|
-
return parsed.items ?? [];
|
|
3430
|
-
} catch {
|
|
3431
|
-
return [];
|
|
3432
|
-
}
|
|
3444
|
+
return parseReplyItems(stripFences(result));
|
|
3433
3445
|
}
|
|
3434
3446
|
};
|
|
3435
3447
|
|
|
@@ -4836,6 +4848,19 @@ function createApi(deps) {
|
|
|
4836
4848
|
},
|
|
4837
4849
|
onLocaleDone: (locale) => {
|
|
4838
4850
|
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
4851
|
+
},
|
|
4852
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
4853
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
4854
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
4855
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
4856
|
+
appendLog(projectRoot, {
|
|
4857
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4858
|
+
kind: "translate",
|
|
4859
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
4860
|
+
model: aiCfg.model,
|
|
4861
|
+
locale,
|
|
4862
|
+
raw
|
|
4863
|
+
});
|
|
4839
4864
|
}
|
|
4840
4865
|
}, aiCfg.concurrency, signal);
|
|
4841
4866
|
if (!signal?.aborted) {
|
|
@@ -4868,7 +4893,19 @@ function createApi(deps) {
|
|
|
4868
4893
|
}
|
|
4869
4894
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4870
4895
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4871
|
-
const results = await runLocaleParallel(toTranslate, provider, {
|
|
4896
|
+
const results = await runLocaleParallel(toTranslate, provider, {
|
|
4897
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
4898
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
4899
|
+
appendLog(projectRoot, {
|
|
4900
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4901
|
+
kind: "translate",
|
|
4902
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
4903
|
+
model: aiCfg.model,
|
|
4904
|
+
locale,
|
|
4905
|
+
raw
|
|
4906
|
+
});
|
|
4907
|
+
}
|
|
4908
|
+
}, aiCfg.concurrency);
|
|
4872
4909
|
const latest = load();
|
|
4873
4910
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
4874
4911
|
const entry = {
|