glotfile 0.3.1 → 0.4.1
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 +250 -80
- package/dist/server/server.js +221 -74
- package/dist/ui/assets/index-DfZmbiXq.js +1848 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-LjEnW4jC.js +0 -1847
package/dist/server/server.js
CHANGED
|
@@ -103,6 +103,7 @@ function isPluralForm(key) {
|
|
|
103
103
|
}
|
|
104
104
|
var LOCALE_CASES = ["lower-hyphen", "lower-underscore", "bcp47-hyphen", "bcp47-underscore"];
|
|
105
105
|
var PROVIDERS = ["anthropic", "openai", "bedrock", "openrouter", "ollama", "claude-code"];
|
|
106
|
+
var PROMPT_STYLES = ["default", "translategemma"];
|
|
106
107
|
var GlotfileError = class extends Error {
|
|
107
108
|
};
|
|
108
109
|
function isObject(v) {
|
|
@@ -1128,7 +1129,7 @@ function selectContextTargets(state, opts, cache2, lastRunAt) {
|
|
|
1128
1129
|
let candidates = [];
|
|
1129
1130
|
for (const key of Object.keys(state.keys).sort()) {
|
|
1130
1131
|
const entry = state.keys[key];
|
|
1131
|
-
if (entry.context) continue;
|
|
1132
|
+
if (entry.context && !opts.force) continue;
|
|
1132
1133
|
if (keySet && !keySet.has(key)) continue;
|
|
1133
1134
|
if (keyRe && !keyRe.test(key)) continue;
|
|
1134
1135
|
if (cutoff) {
|
|
@@ -1172,7 +1173,7 @@ function buildContextBatchPrompt(reqs) {
|
|
|
1172
1173
|
${s.lines}
|
|
1173
1174
|
\`\`\``;
|
|
1174
1175
|
}).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
|
|
1175
|
-
return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText
|
|
1176
|
+
return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText };
|
|
1176
1177
|
});
|
|
1177
1178
|
return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
|
|
1178
1179
|
}
|
|
@@ -1196,7 +1197,7 @@ var CONTEXT_BATCH_SCHEMA = {
|
|
|
1196
1197
|
required: ["items"],
|
|
1197
1198
|
additionalProperties: false
|
|
1198
1199
|
};
|
|
1199
|
-
function applyContext(state, reqs, results, clock = systemClock) {
|
|
1200
|
+
function applyContext(state, reqs, results, clock = systemClock, force = false) {
|
|
1200
1201
|
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
1201
1202
|
let written = 0;
|
|
1202
1203
|
const errors = [];
|
|
@@ -1217,7 +1218,7 @@ function applyContext(state, reqs, results, clock = systemClock) {
|
|
|
1217
1218
|
continue;
|
|
1218
1219
|
}
|
|
1219
1220
|
const entry = state.keys[req.key];
|
|
1220
|
-
if (!entry || entry.context) continue;
|
|
1221
|
+
if (!entry || entry.context && !force) continue;
|
|
1221
1222
|
entry.context = context;
|
|
1222
1223
|
entry.contextSource = "ai";
|
|
1223
1224
|
entry.contextAt = clock();
|
|
@@ -1433,6 +1434,7 @@ function selectRequests(state, opts) {
|
|
|
1433
1434
|
id: String(id++),
|
|
1434
1435
|
key,
|
|
1435
1436
|
source: other,
|
|
1437
|
+
sourceLocale: state.config.sourceLocale,
|
|
1436
1438
|
context: entry.context,
|
|
1437
1439
|
targetLocale: locale,
|
|
1438
1440
|
maxLength: entry.maxLength,
|
|
@@ -1453,6 +1455,7 @@ function selectRequests(state, opts) {
|
|
|
1453
1455
|
id: String(id++),
|
|
1454
1456
|
key,
|
|
1455
1457
|
source,
|
|
1458
|
+
sourceLocale: state.config.sourceLocale,
|
|
1456
1459
|
context: entry.context,
|
|
1457
1460
|
targetLocale: locale,
|
|
1458
1461
|
maxLength: entry.maxLength,
|
|
@@ -2347,8 +2350,8 @@ import { dirname as dirname2, resolve as resolve8, basename, relative as relativ
|
|
|
2347
2350
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2348
2351
|
|
|
2349
2352
|
// src/server/ai/provider.ts
|
|
2350
|
-
function buildSystemPrompt() {
|
|
2351
|
-
|
|
2353
|
+
function buildSystemPrompt(hasPluralItems) {
|
|
2354
|
+
const lines = [
|
|
2352
2355
|
"You are a professional software localization engine for a UI string catalog.",
|
|
2353
2356
|
"Your goal: translate each source UI string into its target locale accurately and idiomatically, as a native speaker would phrase it in a real app interface.",
|
|
2354
2357
|
"",
|
|
@@ -2360,20 +2363,28 @@ function buildSystemPrompt() {
|
|
|
2360
2363
|
"- 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.",
|
|
2361
2364
|
"- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
|
|
2362
2365
|
"- Match the register and capitalization conventions of the target language and of UI microcopy.",
|
|
2363
|
-
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2366
|
+
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
2367
|
+
];
|
|
2368
|
+
if (hasPluralItems) {
|
|
2369
|
+
lines.push(
|
|
2370
|
+
"",
|
|
2371
|
+
'Plural items: an item with a `plural` field gives you the source plural FORMS (keyed by CLDR category) and the `categories` REQUIRED for the target language. Return a `forms` object with one idiomatic translation per REQUIRED category \u2014 including categories the source language does not have (infer them from meaning). Keep the count token shown in the source forms (e.g. {count}) in every form that states a quantity; the `zero`, `one`, and `two` forms MAY omit it when that is natural in the target language \u2014 e.g. "No files", "One file", or a dual form that encodes the count grammatically (Arabic \u0645\u0644\u0641\u0627\u0646). Never introduce a placeholder the source did not have. For these items return `forms` instead of `translation`.'
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
return lines.join("\n");
|
|
2367
2375
|
}
|
|
2368
2376
|
function buildBatchPrompt(reqs) {
|
|
2369
2377
|
const targetLocale = reqs[0]?.targetLocale ?? "";
|
|
2378
|
+
const hasPluralItems = reqs.some((r) => r.plural !== void 0);
|
|
2370
2379
|
const items = reqs.map((r) => {
|
|
2371
2380
|
const base = {
|
|
2372
2381
|
id: r.id,
|
|
2373
2382
|
key: r.key,
|
|
2374
2383
|
context: r.context ?? null,
|
|
2375
2384
|
maxLength: r.maxLength ?? null,
|
|
2376
|
-
|
|
2385
|
+
// Wrap in braces so the model sees "{site}" not "site" — makes the visual
|
|
2386
|
+
// connection to the source string obvious and reduces rename errors.
|
|
2387
|
+
placeholders: r.placeholders.map((p) => `{${p}}`),
|
|
2377
2388
|
glossary: r.glossary ?? [],
|
|
2378
2389
|
hasScreenshot: r.image !== void 0
|
|
2379
2390
|
};
|
|
@@ -2382,10 +2393,24 @@ function buildBatchPrompt(reqs) {
|
|
|
2382
2393
|
}
|
|
2383
2394
|
return { ...base, source: r.source };
|
|
2384
2395
|
});
|
|
2396
|
+
const returnFormat = hasPluralItems ? 'For a scalar item (has `source`) return {"id","translation"}; for a plural item (has `plural`) return {"id","forms"} with one string per required category.' : 'Return {"id","translation"} for each item.';
|
|
2385
2397
|
return `Translate every item below into the target locale: ${targetLocale}. All items share this one target language.
|
|
2386
|
-
Glossary entries are constraints you MUST apply. Items with hasScreenshot:true have a screenshot supplied as a separate image block above; use it for context.
|
|
2398
|
+
Glossary entries are constraints you MUST apply. Items with hasScreenshot:true have a screenshot supplied as a separate image block above; use it for context. ${returnFormat} Return JSON {"items":[\u2026]}.
|
|
2387
2399
|
` + JSON.stringify(items, null, 2);
|
|
2388
2400
|
}
|
|
2401
|
+
function buildTranslateGemmaSystemPrompt(sourceLocale, targetLocale) {
|
|
2402
|
+
return `You are a professional ${sourceLocale} to ${targetLocale} translator. Your goal is to accurately convey the meaning and nuances of the original ${sourceLocale} text while adhering to ${targetLocale} grammar, vocabulary, and cultural sensitivities. Produce only the ${targetLocale} translation, without any additional explanations or commentary. Preserve every interpolation placeholder exactly as written (e.g. {site}, {count}, {name}) \u2014 do not translate, rename, or remove them. Preserve markdown formatting markers exactly as written (e.g. **bold**, *italic*, __underline__) \u2014 copy them into the translation in the same positions. Please translate the following ${sourceLocale} text into ${targetLocale}:`;
|
|
2403
|
+
}
|
|
2404
|
+
function buildTranslateGemmaUserPrompt(source) {
|
|
2405
|
+
return `
|
|
2406
|
+
|
|
2407
|
+
${source}`;
|
|
2408
|
+
}
|
|
2409
|
+
function parseTranslateGemmaResponse(text, id) {
|
|
2410
|
+
const translation = text.trim();
|
|
2411
|
+
if (!translation) return { id, error: "No translation returned" };
|
|
2412
|
+
return { id, translation };
|
|
2413
|
+
}
|
|
2389
2414
|
var BATCH_SCHEMA = {
|
|
2390
2415
|
type: "object",
|
|
2391
2416
|
properties: {
|
|
@@ -2531,7 +2556,7 @@ var AnthropicProvider = class {
|
|
|
2531
2556
|
const res = await this.client.messages.create({
|
|
2532
2557
|
model: this.config.model,
|
|
2533
2558
|
max_tokens: 8192,
|
|
2534
|
-
system: [{ type: "text", text: buildSystemPrompt(), cache_control: { type: "ephemeral" } }],
|
|
2559
|
+
system: [{ type: "text", text: buildSystemPrompt(batch.some((r) => r.plural !== void 0)), cache_control: { type: "ephemeral" } }],
|
|
2535
2560
|
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
2536
2561
|
messages: [{ role: "user", content }]
|
|
2537
2562
|
}, { signal });
|
|
@@ -2621,7 +2646,7 @@ var OpenAIProvider = class {
|
|
|
2621
2646
|
// via runBatched, so non-strict schema guidance is sufficient.
|
|
2622
2647
|
response_format: { type: "json_schema", json_schema: { name: "translations", schema: BATCH_SCHEMA, strict: false } },
|
|
2623
2648
|
messages: [
|
|
2624
|
-
{ role: "system", content: buildSystemPrompt() },
|
|
2649
|
+
{ role: "system", content: buildSystemPrompt(batch.some((r) => r.plural !== void 0)) },
|
|
2625
2650
|
{ role: "user", content: this.buildUserContent(batch) }
|
|
2626
2651
|
]
|
|
2627
2652
|
}, { signal });
|
|
@@ -2725,7 +2750,7 @@ var BedrockProvider = class {
|
|
|
2725
2750
|
buildInput(batch) {
|
|
2726
2751
|
const input = {
|
|
2727
2752
|
modelId: this.config.model,
|
|
2728
|
-
system: [{ text: buildSystemPrompt() }],
|
|
2753
|
+
system: [{ text: buildSystemPrompt(batch.some((r) => r.plural !== void 0)) }],
|
|
2729
2754
|
messages: [{ role: "user", content: this.buildContentBlocks(batch) }]
|
|
2730
2755
|
};
|
|
2731
2756
|
if (!this.isMeta()) {
|
|
@@ -2787,6 +2812,26 @@ var OllamaProvider = class extends OpenAIProvider {
|
|
|
2787
2812
|
supportsVision() {
|
|
2788
2813
|
return this.config.vision === true;
|
|
2789
2814
|
}
|
|
2815
|
+
// translategemma expects a per-item system prompt, a plain-text user message
|
|
2816
|
+
// (no JSON, no structured output), and returns a plain-text translation.
|
|
2817
|
+
async callBatch(batch, signal) {
|
|
2818
|
+
if (this.config.promptStyle !== "translategemma") {
|
|
2819
|
+
return super.callBatch(batch, signal);
|
|
2820
|
+
}
|
|
2821
|
+
const results = [];
|
|
2822
|
+
for (const req of batch) {
|
|
2823
|
+
const res = await this.client.chat.completions.create({
|
|
2824
|
+
model: this.config.model,
|
|
2825
|
+
messages: [
|
|
2826
|
+
{ role: "system", content: buildTranslateGemmaSystemPrompt(req.sourceLocale, req.targetLocale) },
|
|
2827
|
+
{ role: "user", content: buildTranslateGemmaUserPrompt(req.source) }
|
|
2828
|
+
]
|
|
2829
|
+
}, { signal });
|
|
2830
|
+
const text = res.choices?.[0]?.message?.content ?? "";
|
|
2831
|
+
results.push(parseTranslateGemmaResponse(text, req.id));
|
|
2832
|
+
}
|
|
2833
|
+
return results;
|
|
2834
|
+
}
|
|
2790
2835
|
};
|
|
2791
2836
|
|
|
2792
2837
|
// src/server/ai/claudecode.ts
|
|
@@ -2865,7 +2910,7 @@ var ClaudeCodeProvider = class {
|
|
|
2865
2910
|
const prompt = buildBatchPrompt(batch);
|
|
2866
2911
|
let result;
|
|
2867
2912
|
try {
|
|
2868
|
-
result = await this.spawnFn(prompt, buildSystemPrompt(), this.config.model);
|
|
2913
|
+
result = await this.spawnFn(prompt, buildSystemPrompt(batch.some((r) => r.plural !== void 0)), this.config.model);
|
|
2869
2914
|
} catch (err) {
|
|
2870
2915
|
if (signal?.aborted) return [];
|
|
2871
2916
|
throw err;
|
|
@@ -3425,18 +3470,36 @@ function coerceAi(raw) {
|
|
|
3425
3470
|
endpoint: typeof a.endpoint === "string" ? a.endpoint : null,
|
|
3426
3471
|
region: typeof a.region === "string" ? a.region : null,
|
|
3427
3472
|
batchSize: typeof a.batchSize === "number" && a.batchSize > 0 ? a.batchSize : DEFAULT_AI.batchSize,
|
|
3428
|
-
|
|
3473
|
+
concurrency: typeof a.concurrency === "number" && a.concurrency > 0 ? a.concurrency : void 0,
|
|
3474
|
+
contextBatchSize: typeof a.contextBatchSize === "number" && a.contextBatchSize > 0 ? a.contextBatchSize : void 0,
|
|
3475
|
+
contextConcurrency: typeof a.contextConcurrency === "number" && a.contextConcurrency > 0 ? a.contextConcurrency : void 0,
|
|
3476
|
+
vision: typeof a.vision === "boolean" ? a.vision : void 0,
|
|
3477
|
+
promptStyle: PROMPT_STYLES.includes(a.promptStyle) ? a.promptStyle : void 0
|
|
3429
3478
|
};
|
|
3430
3479
|
}
|
|
3480
|
+
function coerceProfiles(raw) {
|
|
3481
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
3482
|
+
const result = {};
|
|
3483
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
3484
|
+
if (typeof k === "string" && k.trim()) result[k] = coerceAi(v);
|
|
3485
|
+
}
|
|
3486
|
+
return result;
|
|
3487
|
+
}
|
|
3431
3488
|
function loadLocalSettings(projectRoot) {
|
|
3432
3489
|
const raw = readJson2(settingsPath(projectRoot));
|
|
3433
|
-
|
|
3490
|
+
const profiles = coerceProfiles(raw.profiles);
|
|
3491
|
+
const activeProfile = typeof raw.activeProfile === "string" && raw.activeProfile in profiles ? raw.activeProfile : null;
|
|
3492
|
+
const baseAi = coerceAi(raw.ai);
|
|
3493
|
+
const ai = activeProfile ? profiles[activeProfile] : baseAi;
|
|
3494
|
+
return { ai, editor: isEditorId(raw.editor) ? raw.editor : DEFAULT_EDITOR, profiles, activeProfile };
|
|
3434
3495
|
}
|
|
3435
3496
|
function saveLocalSettings(projectRoot, patch) {
|
|
3436
3497
|
const path = settingsPath(projectRoot);
|
|
3437
3498
|
const merged = { ...readJson2(path) };
|
|
3438
3499
|
if (patch.ai !== void 0) merged.ai = patch.ai;
|
|
3439
3500
|
if (patch.editor !== void 0) merged.editor = patch.editor;
|
|
3501
|
+
if (patch.profiles !== void 0) merged.profiles = patch.profiles;
|
|
3502
|
+
if (patch.activeProfile !== void 0) merged.activeProfile = patch.activeProfile;
|
|
3440
3503
|
ensureGlotfileDir(projectRoot);
|
|
3441
3504
|
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
3442
3505
|
}
|
|
@@ -3524,6 +3587,43 @@ function createApi(deps) {
|
|
|
3524
3587
|
saveLocalSettings(projectRoot, patch);
|
|
3525
3588
|
return c.json({ ok: true });
|
|
3526
3589
|
});
|
|
3590
|
+
app.get("/ai-profiles", (c) => {
|
|
3591
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3592
|
+
return c.json({ profiles: ls.profiles, activeProfile: ls.activeProfile });
|
|
3593
|
+
});
|
|
3594
|
+
app.put("/ai-profiles/:name", async (c) => {
|
|
3595
|
+
const name = c.req.param("name").trim();
|
|
3596
|
+
if (!name) return c.json({ error: "name required" }, 400);
|
|
3597
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3598
|
+
const err = aiConfigError(body);
|
|
3599
|
+
if (err) return c.json({ error: err }, 400);
|
|
3600
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3601
|
+
saveLocalSettings(projectRoot, { profiles: { ...ls.profiles, [name]: body } });
|
|
3602
|
+
return c.json({ ok: true });
|
|
3603
|
+
});
|
|
3604
|
+
app.delete("/ai-profiles/:name", (c) => {
|
|
3605
|
+
const name = c.req.param("name");
|
|
3606
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3607
|
+
if (!(name in ls.profiles)) return c.json({ error: "profile not found" }, 404);
|
|
3608
|
+
const profiles = { ...ls.profiles };
|
|
3609
|
+
delete profiles[name];
|
|
3610
|
+
const patch = { profiles };
|
|
3611
|
+
if (ls.activeProfile === name) patch.activeProfile = null;
|
|
3612
|
+
saveLocalSettings(projectRoot, patch);
|
|
3613
|
+
return c.json({ ok: true });
|
|
3614
|
+
});
|
|
3615
|
+
app.post("/ai-profiles/active", async (c) => {
|
|
3616
|
+
const { name } = await c.req.json().catch(() => ({}));
|
|
3617
|
+
if (name !== null && name !== void 0) {
|
|
3618
|
+
if (typeof name !== "string") return c.json({ error: "name must be a string or null" }, 400);
|
|
3619
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3620
|
+
if (name !== "" && !(name in ls.profiles)) return c.json({ error: "profile not found" }, 404);
|
|
3621
|
+
saveLocalSettings(projectRoot, { activeProfile: name || null });
|
|
3622
|
+
} else {
|
|
3623
|
+
saveLocalSettings(projectRoot, { activeProfile: null });
|
|
3624
|
+
}
|
|
3625
|
+
return c.json({ ok: true });
|
|
3626
|
+
});
|
|
3527
3627
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
3528
3628
|
app.get("/files", (c) => {
|
|
3529
3629
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -3702,7 +3802,7 @@ function createApi(deps) {
|
|
|
3702
3802
|
return c.json({ removed });
|
|
3703
3803
|
});
|
|
3704
3804
|
app.post("/keys/bulk-meta", async (c) => {
|
|
3705
|
-
const { keys, addTags, removeTags, skipTranslate } = await c.req.json();
|
|
3805
|
+
const { keys, addTags, removeTags, skipTranslate, clearContext } = await c.req.json();
|
|
3706
3806
|
if (!Array.isArray(keys) || keys.length === 0) return c.json({ error: "keys must be a non-empty array" }, 400);
|
|
3707
3807
|
const s = load();
|
|
3708
3808
|
let updated = 0;
|
|
@@ -3720,6 +3820,11 @@ function createApi(deps) {
|
|
|
3720
3820
|
if (skipTranslate) setMetadata(s, key, { skipTranslate: true });
|
|
3721
3821
|
else delete entry.skipTranslate;
|
|
3722
3822
|
}
|
|
3823
|
+
if (clearContext === true) {
|
|
3824
|
+
delete entry.context;
|
|
3825
|
+
delete entry.contextSource;
|
|
3826
|
+
delete entry.contextAt;
|
|
3827
|
+
}
|
|
3723
3828
|
updated++;
|
|
3724
3829
|
}
|
|
3725
3830
|
persist(s);
|
|
@@ -4000,7 +4105,7 @@ function createApi(deps) {
|
|
|
4000
4105
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4001
4106
|
let totalWritten = 0;
|
|
4002
4107
|
const allErrors = [];
|
|
4003
|
-
const system = buildSystemPrompt();
|
|
4108
|
+
const system = buildSystemPrompt(reqs.some((r) => r.plural !== void 0));
|
|
4004
4109
|
const reqById = new Map(reqs.map((r) => [r.id, r]));
|
|
4005
4110
|
const localeTotals = /* @__PURE__ */ new Map();
|
|
4006
4111
|
for (const r of reqs) localeTotals.set(r.targetLocale, (localeTotals.get(r.targetLocale) ?? 0) + 1);
|
|
@@ -4029,7 +4134,7 @@ function createApi(deps) {
|
|
|
4029
4134
|
system,
|
|
4030
4135
|
items: batchResults.map((r) => {
|
|
4031
4136
|
const req = reqById.get(r.id);
|
|
4032
|
-
return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ?
|
|
4137
|
+
return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ? fresh.keys[req.key]?.screenshot : void 0 };
|
|
4033
4138
|
}),
|
|
4034
4139
|
results: batchResults
|
|
4035
4140
|
});
|
|
@@ -4044,7 +4149,7 @@ function createApi(deps) {
|
|
|
4044
4149
|
onLocaleDone: (locale) => {
|
|
4045
4150
|
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
4046
4151
|
}
|
|
4047
|
-
},
|
|
4152
|
+
}, aiCfg.concurrency, signal);
|
|
4048
4153
|
if (!signal?.aborted) {
|
|
4049
4154
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
4050
4155
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -4075,14 +4180,15 @@ function createApi(deps) {
|
|
|
4075
4180
|
}
|
|
4076
4181
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, projectRoot, provider.supportsVision());
|
|
4077
4182
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4078
|
-
const results = await runLocaleParallel(toTranslate, provider);
|
|
4079
|
-
|
|
4183
|
+
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4184
|
+
const latest = load();
|
|
4185
|
+
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
4080
4186
|
const entry = {
|
|
4081
4187
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4082
4188
|
kind: "translate",
|
|
4083
4189
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
4084
4190
|
model: aiCfg.model,
|
|
4085
|
-
system: buildSystemPrompt(),
|
|
4191
|
+
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
4086
4192
|
// Log the screenshot PATH only — never the image bytes.
|
|
4087
4193
|
items: toTranslate.map((r) => ({
|
|
4088
4194
|
id: r.id,
|
|
@@ -4091,13 +4197,13 @@ function createApi(deps) {
|
|
|
4091
4197
|
targetLocale: r.targetLocale,
|
|
4092
4198
|
context: r.context,
|
|
4093
4199
|
glossary: r.glossary,
|
|
4094
|
-
screenshot:
|
|
4200
|
+
screenshot: latest.keys[r.key]?.screenshot
|
|
4095
4201
|
})),
|
|
4096
4202
|
results
|
|
4097
4203
|
};
|
|
4098
4204
|
appendLog(projectRoot, entry);
|
|
4205
|
+
persist(latest);
|
|
4099
4206
|
}
|
|
4100
|
-
persist(s);
|
|
4101
4207
|
return c.json({ requested: reqs.length, written, errors });
|
|
4102
4208
|
}));
|
|
4103
4209
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
@@ -4153,55 +4259,96 @@ function createApi(deps) {
|
|
|
4153
4259
|
return c.json({ indexed: true, scannedAt: cache2.scannedAt, used: computeUsedKeys(load(), cache2) });
|
|
4154
4260
|
});
|
|
4155
4261
|
app.post("/context/build", async (c) => {
|
|
4262
|
+
const signal = c.req.raw.signal;
|
|
4156
4263
|
const body = await c.req.json().catch(() => ({}));
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
);
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
system
|
|
4199
|
-
|
|
4200
|
-
|
|
4264
|
+
return streamSSE(c, async (stream) => {
|
|
4265
|
+
const s = load();
|
|
4266
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
4267
|
+
if (!cache2) {
|
|
4268
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: "No usage index found. Run 'glotfile scan' first." }) });
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
const targets = selectContextTargets(s, {
|
|
4272
|
+
all: body.all,
|
|
4273
|
+
keyGlob: body.keyGlob,
|
|
4274
|
+
limit: body.limit,
|
|
4275
|
+
since: body.since,
|
|
4276
|
+
keys: body.keys,
|
|
4277
|
+
force: body.force
|
|
4278
|
+
}, cache2, body.lastRunAt);
|
|
4279
|
+
if (!targets.length) {
|
|
4280
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: 0, written: 0, errors: [] }) });
|
|
4281
|
+
return;
|
|
4282
|
+
}
|
|
4283
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
4284
|
+
let provider;
|
|
4285
|
+
try {
|
|
4286
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4287
|
+
} catch (e) {
|
|
4288
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
|
|
4292
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
4293
|
+
for (const target of targets) {
|
|
4294
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
4295
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
4296
|
+
key: r.key,
|
|
4297
|
+
file,
|
|
4298
|
+
line: r.line,
|
|
4299
|
+
col: r.col,
|
|
4300
|
+
scanner: r.scanner
|
|
4301
|
+
}))
|
|
4302
|
+
);
|
|
4303
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
4304
|
+
}
|
|
4305
|
+
const system = buildContextSystemPrompt();
|
|
4306
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
4307
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
4308
|
+
const chunks = [];
|
|
4309
|
+
for (let i = 0; i < targets.length; i += batchSize) chunks.push(targets.slice(i, i + batchSize));
|
|
4310
|
+
let totalWritten = 0;
|
|
4311
|
+
let totalDone = 0;
|
|
4312
|
+
const allErrors = [];
|
|
4313
|
+
let next = 0;
|
|
4314
|
+
async function worker() {
|
|
4315
|
+
while (next < chunks.length) {
|
|
4316
|
+
if (signal?.aborted) break;
|
|
4317
|
+
const chunk2 = chunks[next++];
|
|
4318
|
+
let raw;
|
|
4319
|
+
try {
|
|
4320
|
+
raw = await provider.complete({ system, content: [{ type: "text", text: buildContextBatchPrompt(chunk2) }], schema: CONTEXT_BATCH_SCHEMA });
|
|
4321
|
+
} catch (e) {
|
|
4322
|
+
totalDone += chunk2.length;
|
|
4323
|
+
allErrors.push(...chunk2.map((t) => ({ key: t.key, error: e.message })));
|
|
4324
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done: totalDone, total: targets.length, written: totalWritten }) });
|
|
4325
|
+
continue;
|
|
4326
|
+
}
|
|
4327
|
+
if (signal?.aborted) break;
|
|
4328
|
+
const batch = raw;
|
|
4329
|
+
const fresh = load();
|
|
4330
|
+
const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], void 0, body.force === true);
|
|
4331
|
+
appendLog(projectRoot, {
|
|
4332
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4333
|
+
kind: "context",
|
|
4334
|
+
summary: `Generated context for ${chunk2.length} key(s)`,
|
|
4335
|
+
model: aiCfg.model,
|
|
4336
|
+
system,
|
|
4337
|
+
items: chunk2.map((t) => ({ id: t.id, key: t.key, source: t.source })),
|
|
4338
|
+
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
|
|
4339
|
+
});
|
|
4340
|
+
persist(fresh);
|
|
4341
|
+
totalWritten += written;
|
|
4342
|
+
totalDone += chunk2.length;
|
|
4343
|
+
allErrors.push(...errors);
|
|
4344
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done: totalDone, total: targets.length, written: totalWritten }) });
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
4348
|
+
if (signal?.aborted) return;
|
|
4349
|
+
console.log(`[context] ${totalWritten} context(s) written${allErrors.length ? `, ${allErrors.length} error(s)` : ""}`);
|
|
4350
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
4201
4351
|
});
|
|
4202
|
-
persist(s);
|
|
4203
|
-
console.log(`[context] ${written} context(s) written${errors.length ? `, ${errors.length} error(s)` : ""}`);
|
|
4204
|
-
return c.json({ requested: targets.length, written, errors });
|
|
4205
4352
|
});
|
|
4206
4353
|
app.onError(
|
|
4207
4354
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|