glotfile 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli.js +292 -100
- package/dist/server/server.js +263 -94
- package/dist/ui/assets/index-DC5Ppxxa.js +1848 -0
- package/dist/ui/assets/index-DVTJ7ZX_.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BHjDAL9d.js +0 -1847
- package/dist/ui/assets/index-iW_TzurC.css +0 -1
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) {
|
|
@@ -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
|
}
|
|
@@ -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,
|
|
@@ -2340,15 +2343,15 @@ function getAdapter(name) {
|
|
|
2340
2343
|
}
|
|
2341
2344
|
|
|
2342
2345
|
// src/server/api.ts
|
|
2343
|
-
import { readFileSync as readFileSync13, existsSync as existsSync9, readdirSync as readdirSync7, rmSync as rmSync4 } from "fs";
|
|
2346
|
+
import { readFileSync as readFileSync13, existsSync as existsSync9, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "fs";
|
|
2344
2347
|
import { dirname as dirname2, resolve as resolve8, basename, relative as relative3, sep } from "path";
|
|
2345
2348
|
|
|
2346
2349
|
// src/server/ai/anthropic.ts
|
|
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()) {
|
|
@@ -2785,7 +2810,27 @@ var OllamaProvider = class extends OpenAIProvider {
|
|
|
2785
2810
|
super(config, client ?? loadOpenAIClient(ollamaClientOptions(config)));
|
|
2786
2811
|
}
|
|
2787
2812
|
supportsVision() {
|
|
2788
|
-
return
|
|
2813
|
+
return this.config.vision === true;
|
|
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;
|
|
2789
2834
|
}
|
|
2790
2835
|
};
|
|
2791
2836
|
|
|
@@ -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;
|
|
@@ -3424,18 +3469,37 @@ function coerceAi(raw) {
|
|
|
3424
3469
|
model: typeof a.model === "string" && a.model ? a.model : DEFAULT_AI.model,
|
|
3425
3470
|
endpoint: typeof a.endpoint === "string" ? a.endpoint : null,
|
|
3426
3471
|
region: typeof a.region === "string" ? a.region : null,
|
|
3427
|
-
batchSize: typeof a.batchSize === "number" && a.batchSize > 0 ? a.batchSize : DEFAULT_AI.batchSize
|
|
3472
|
+
batchSize: typeof a.batchSize === "number" && a.batchSize > 0 ? a.batchSize : DEFAULT_AI.batchSize,
|
|
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
|
|
3428
3478
|
};
|
|
3429
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
|
+
}
|
|
3430
3488
|
function loadLocalSettings(projectRoot) {
|
|
3431
3489
|
const raw = readJson2(settingsPath(projectRoot));
|
|
3432
|
-
|
|
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 };
|
|
3433
3495
|
}
|
|
3434
3496
|
function saveLocalSettings(projectRoot, patch) {
|
|
3435
3497
|
const path = settingsPath(projectRoot);
|
|
3436
3498
|
const merged = { ...readJson2(path) };
|
|
3437
3499
|
if (patch.ai !== void 0) merged.ai = patch.ai;
|
|
3438
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;
|
|
3439
3503
|
ensureGlotfileDir(projectRoot);
|
|
3440
3504
|
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
3441
3505
|
}
|
|
@@ -3523,32 +3587,90 @@ function createApi(deps) {
|
|
|
3523
3587
|
saveLocalSettings(projectRoot, patch);
|
|
3524
3588
|
return c.json({ ok: true });
|
|
3525
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
|
+
});
|
|
3526
3627
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
3527
3628
|
app.get("/files", (c) => {
|
|
3528
3629
|
const found = /* @__PURE__ */ new Map();
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
}
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
abs = resolve8(projectRoot, `${name}.json`);
|
|
3539
|
-
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3540
|
-
abs = resolve8(projectRoot, name);
|
|
3541
|
-
} else {
|
|
3542
|
-
continue;
|
|
3543
|
-
}
|
|
3544
|
-
if (found.has(abs)) continue;
|
|
3630
|
+
const activeRel = relative3(projectRoot, deps.statePath);
|
|
3631
|
+
found.set(deps.statePath, {
|
|
3632
|
+
name: basename(deps.statePath),
|
|
3633
|
+
path: deps.statePath,
|
|
3634
|
+
relDir: activeRel !== basename(activeRel) ? dirname2(activeRel) : void 0
|
|
3635
|
+
});
|
|
3636
|
+
function walk(dir, depth) {
|
|
3637
|
+
if (depth > 4) return;
|
|
3638
|
+
let entries = [];
|
|
3545
3639
|
try {
|
|
3546
|
-
|
|
3547
|
-
found.set(abs, { name: basename(abs), path: abs });
|
|
3640
|
+
entries = readdirSync7(dir);
|
|
3548
3641
|
} catch {
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
for (const name of entries) {
|
|
3645
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3646
|
+
const abs = resolve8(dir, name);
|
|
3647
|
+
let filePath = null;
|
|
3648
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync9(resolve8(abs, "config.json"))) {
|
|
3649
|
+
filePath = resolve8(dir, `${name}.json`);
|
|
3650
|
+
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3651
|
+
filePath = abs;
|
|
3652
|
+
} else {
|
|
3653
|
+
try {
|
|
3654
|
+
if (statSync4(abs).isDirectory()) walk(abs, depth + 1);
|
|
3655
|
+
} catch {
|
|
3656
|
+
}
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
if (found.has(filePath)) continue;
|
|
3660
|
+
try {
|
|
3661
|
+
loadState(filePath);
|
|
3662
|
+
const rel = relative3(projectRoot, filePath);
|
|
3663
|
+
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname2(rel) : void 0 });
|
|
3664
|
+
} catch {
|
|
3665
|
+
}
|
|
3549
3666
|
}
|
|
3550
3667
|
}
|
|
3551
|
-
|
|
3668
|
+
walk(projectRoot, 0);
|
|
3669
|
+
const files = [...found.values()].sort((a, b) => {
|
|
3670
|
+
const ka = a.relDir ? `${a.relDir}/${a.name}` : a.name;
|
|
3671
|
+
const kb = b.relDir ? `${b.relDir}/${b.name}` : b.name;
|
|
3672
|
+
return ka.localeCompare(kb);
|
|
3673
|
+
});
|
|
3552
3674
|
return c.json(files);
|
|
3553
3675
|
});
|
|
3554
3676
|
app.post("/file", async (c) => {
|
|
@@ -3680,7 +3802,7 @@ function createApi(deps) {
|
|
|
3680
3802
|
return c.json({ removed });
|
|
3681
3803
|
});
|
|
3682
3804
|
app.post("/keys/bulk-meta", async (c) => {
|
|
3683
|
-
const { keys, addTags, removeTags, skipTranslate } = await c.req.json();
|
|
3805
|
+
const { keys, addTags, removeTags, skipTranslate, clearContext } = await c.req.json();
|
|
3684
3806
|
if (!Array.isArray(keys) || keys.length === 0) return c.json({ error: "keys must be a non-empty array" }, 400);
|
|
3685
3807
|
const s = load();
|
|
3686
3808
|
let updated = 0;
|
|
@@ -3698,6 +3820,11 @@ function createApi(deps) {
|
|
|
3698
3820
|
if (skipTranslate) setMetadata(s, key, { skipTranslate: true });
|
|
3699
3821
|
else delete entry.skipTranslate;
|
|
3700
3822
|
}
|
|
3823
|
+
if (clearContext === true) {
|
|
3824
|
+
delete entry.context;
|
|
3825
|
+
delete entry.contextSource;
|
|
3826
|
+
delete entry.contextAt;
|
|
3827
|
+
}
|
|
3701
3828
|
updated++;
|
|
3702
3829
|
}
|
|
3703
3830
|
persist(s);
|
|
@@ -3978,7 +4105,7 @@ function createApi(deps) {
|
|
|
3978
4105
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
3979
4106
|
let totalWritten = 0;
|
|
3980
4107
|
const allErrors = [];
|
|
3981
|
-
const system = buildSystemPrompt();
|
|
4108
|
+
const system = buildSystemPrompt(reqs.some((r) => r.plural !== void 0));
|
|
3982
4109
|
const reqById = new Map(reqs.map((r) => [r.id, r]));
|
|
3983
4110
|
const localeTotals = /* @__PURE__ */ new Map();
|
|
3984
4111
|
for (const r of reqs) localeTotals.set(r.targetLocale, (localeTotals.get(r.targetLocale) ?? 0) + 1);
|
|
@@ -3994,8 +4121,9 @@ function createApi(deps) {
|
|
|
3994
4121
|
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
3995
4122
|
},
|
|
3996
4123
|
onBatchComplete: (done, total, batchResults, locale) => {
|
|
3997
|
-
const
|
|
3998
|
-
|
|
4124
|
+
const fresh = load();
|
|
4125
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
4126
|
+
persist(fresh);
|
|
3999
4127
|
totalWritten += written;
|
|
4000
4128
|
allErrors.push(...errors);
|
|
4001
4129
|
appendLog(projectRoot, {
|
|
@@ -4006,7 +4134,7 @@ function createApi(deps) {
|
|
|
4006
4134
|
system,
|
|
4007
4135
|
items: batchResults.map((r) => {
|
|
4008
4136
|
const req = reqById.get(r.id);
|
|
4009
|
-
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 };
|
|
4010
4138
|
}),
|
|
4011
4139
|
results: batchResults
|
|
4012
4140
|
});
|
|
@@ -4021,7 +4149,7 @@ function createApi(deps) {
|
|
|
4021
4149
|
onLocaleDone: (locale) => {
|
|
4022
4150
|
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
4023
4151
|
}
|
|
4024
|
-
},
|
|
4152
|
+
}, aiCfg.concurrency, signal);
|
|
4025
4153
|
if (!signal?.aborted) {
|
|
4026
4154
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
4027
4155
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -4052,14 +4180,15 @@ function createApi(deps) {
|
|
|
4052
4180
|
}
|
|
4053
4181
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, projectRoot, provider.supportsVision());
|
|
4054
4182
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4055
|
-
const results = await runLocaleParallel(toTranslate, provider);
|
|
4056
|
-
|
|
4183
|
+
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4184
|
+
const latest = load();
|
|
4185
|
+
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
4057
4186
|
const entry = {
|
|
4058
4187
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4059
4188
|
kind: "translate",
|
|
4060
4189
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
4061
4190
|
model: aiCfg.model,
|
|
4062
|
-
system: buildSystemPrompt(),
|
|
4191
|
+
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
4063
4192
|
// Log the screenshot PATH only — never the image bytes.
|
|
4064
4193
|
items: toTranslate.map((r) => ({
|
|
4065
4194
|
id: r.id,
|
|
@@ -4068,13 +4197,13 @@ function createApi(deps) {
|
|
|
4068
4197
|
targetLocale: r.targetLocale,
|
|
4069
4198
|
context: r.context,
|
|
4070
4199
|
glossary: r.glossary,
|
|
4071
|
-
screenshot:
|
|
4200
|
+
screenshot: latest.keys[r.key]?.screenshot
|
|
4072
4201
|
})),
|
|
4073
4202
|
results
|
|
4074
4203
|
};
|
|
4075
4204
|
appendLog(projectRoot, entry);
|
|
4205
|
+
persist(latest);
|
|
4076
4206
|
}
|
|
4077
|
-
persist(s);
|
|
4078
4207
|
return c.json({ requested: reqs.length, written, errors });
|
|
4079
4208
|
}));
|
|
4080
4209
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
@@ -4130,55 +4259,95 @@ function createApi(deps) {
|
|
|
4130
4259
|
return c.json({ indexed: true, scannedAt: cache2.scannedAt, used: computeUsedKeys(load(), cache2) });
|
|
4131
4260
|
});
|
|
4132
4261
|
app.post("/context/build", async (c) => {
|
|
4262
|
+
const signal = c.req.raw.signal;
|
|
4133
4263
|
const body = await c.req.json().catch(() => ({}));
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
)
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
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
|
+
}, cache2, body.lastRunAt);
|
|
4278
|
+
if (!targets.length) {
|
|
4279
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: 0, written: 0, errors: [] }) });
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
4283
|
+
let provider;
|
|
4284
|
+
try {
|
|
4285
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4286
|
+
} catch (e) {
|
|
4287
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
|
|
4291
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
4292
|
+
for (const target of targets) {
|
|
4293
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
4294
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
4295
|
+
key: r.key,
|
|
4296
|
+
file,
|
|
4297
|
+
line: r.line,
|
|
4298
|
+
col: r.col,
|
|
4299
|
+
scanner: r.scanner
|
|
4300
|
+
}))
|
|
4301
|
+
);
|
|
4302
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
4303
|
+
}
|
|
4304
|
+
const system = buildContextSystemPrompt();
|
|
4305
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
4306
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
4307
|
+
const chunks = [];
|
|
4308
|
+
for (let i = 0; i < targets.length; i += batchSize) chunks.push(targets.slice(i, i + batchSize));
|
|
4309
|
+
let totalWritten = 0;
|
|
4310
|
+
let totalDone = 0;
|
|
4311
|
+
const allErrors = [];
|
|
4312
|
+
let next = 0;
|
|
4313
|
+
async function worker() {
|
|
4314
|
+
while (next < chunks.length) {
|
|
4315
|
+
if (signal?.aborted) break;
|
|
4316
|
+
const chunk2 = chunks[next++];
|
|
4317
|
+
let raw;
|
|
4318
|
+
try {
|
|
4319
|
+
raw = await provider.complete({ system, content: [{ type: "text", text: buildContextBatchPrompt(chunk2) }], schema: CONTEXT_BATCH_SCHEMA });
|
|
4320
|
+
} catch (e) {
|
|
4321
|
+
totalDone += chunk2.length;
|
|
4322
|
+
allErrors.push(...chunk2.map((t) => ({ key: t.key, error: e.message })));
|
|
4323
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done: totalDone, total: targets.length, written: totalWritten }) });
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
if (signal?.aborted) break;
|
|
4327
|
+
const batch = raw;
|
|
4328
|
+
const fresh = load();
|
|
4329
|
+
const { written, errors } = applyContext(fresh, chunk2, batch.items ?? []);
|
|
4330
|
+
appendLog(projectRoot, {
|
|
4331
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4332
|
+
kind: "context",
|
|
4333
|
+
summary: `Generated context for ${chunk2.length} key(s)`,
|
|
4334
|
+
model: aiCfg.model,
|
|
4335
|
+
system,
|
|
4336
|
+
items: chunk2.map((t) => ({ id: t.id, key: t.key, source: t.source })),
|
|
4337
|
+
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
|
|
4338
|
+
});
|
|
4339
|
+
persist(fresh);
|
|
4340
|
+
totalWritten += written;
|
|
4341
|
+
totalDone += chunk2.length;
|
|
4342
|
+
allErrors.push(...errors);
|
|
4343
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done: totalDone, total: targets.length, written: totalWritten }) });
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
4347
|
+
if (signal?.aborted) return;
|
|
4348
|
+
console.log(`[context] ${totalWritten} context(s) written${allErrors.length ? `, ${allErrors.length} error(s)` : ""}`);
|
|
4349
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
4178
4350
|
});
|
|
4179
|
-
persist(s);
|
|
4180
|
-
console.log(`[context] ${written} context(s) written${errors.length ? `, ${errors.length} error(s)` : ""}`);
|
|
4181
|
-
return c.json({ requested: targets.length, written, errors });
|
|
4182
4351
|
});
|
|
4183
4352
|
app.onError(
|
|
4184
4353
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|