glotfile 0.3.1 → 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 +246 -77
- package/dist/server/server.js +217 -71
- package/dist/ui/assets/index-DC5Ppxxa.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/cli.js
CHANGED
|
@@ -281,7 +281,7 @@ function defaultState() {
|
|
|
281
281
|
keys: {}
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
|
-
var CURRENT_VERSION, STATES, PLURAL_CATEGORIES, EXACT_SELECTOR_RE, LOCALE_CASES, PROVIDERS, GlotfileError;
|
|
284
|
+
var CURRENT_VERSION, STATES, PLURAL_CATEGORIES, EXACT_SELECTOR_RE, LOCALE_CASES, PROVIDERS, PROMPT_STYLES, GlotfileError;
|
|
285
285
|
var init_schema = __esm({
|
|
286
286
|
"src/server/schema.ts"() {
|
|
287
287
|
"use strict";
|
|
@@ -292,6 +292,7 @@ var init_schema = __esm({
|
|
|
292
292
|
EXACT_SELECTOR_RE = /^=\d+$/;
|
|
293
293
|
LOCALE_CASES = ["lower-hyphen", "lower-underscore", "bcp47-hyphen", "bcp47-underscore"];
|
|
294
294
|
PROVIDERS = ["anthropic", "openai", "bedrock", "openrouter", "ollama", "claude-code"];
|
|
295
|
+
PROMPT_STYLES = ["default", "translategemma"];
|
|
295
296
|
GlotfileError = class extends Error {
|
|
296
297
|
};
|
|
297
298
|
}
|
|
@@ -1563,8 +1564,8 @@ var init_export_run = __esm({
|
|
|
1563
1564
|
});
|
|
1564
1565
|
|
|
1565
1566
|
// src/server/ai/provider.ts
|
|
1566
|
-
function buildSystemPrompt() {
|
|
1567
|
-
|
|
1567
|
+
function buildSystemPrompt(hasPluralItems) {
|
|
1568
|
+
const lines = [
|
|
1568
1569
|
"You are a professional software localization engine for a UI string catalog.",
|
|
1569
1570
|
"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.",
|
|
1570
1571
|
"",
|
|
@@ -1576,20 +1577,28 @@ function buildSystemPrompt() {
|
|
|
1576
1577
|
"- 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.",
|
|
1577
1578
|
"- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
|
|
1578
1579
|
"- Match the register and capitalization conventions of the target language and of UI microcopy.",
|
|
1579
|
-
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1580
|
+
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
1581
|
+
];
|
|
1582
|
+
if (hasPluralItems) {
|
|
1583
|
+
lines.push(
|
|
1584
|
+
"",
|
|
1585
|
+
'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`.'
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
return lines.join("\n");
|
|
1583
1589
|
}
|
|
1584
1590
|
function buildBatchPrompt(reqs) {
|
|
1585
1591
|
const targetLocale = reqs[0]?.targetLocale ?? "";
|
|
1592
|
+
const hasPluralItems = reqs.some((r) => r.plural !== void 0);
|
|
1586
1593
|
const items = reqs.map((r) => {
|
|
1587
1594
|
const base = {
|
|
1588
1595
|
id: r.id,
|
|
1589
1596
|
key: r.key,
|
|
1590
1597
|
context: r.context ?? null,
|
|
1591
1598
|
maxLength: r.maxLength ?? null,
|
|
1592
|
-
|
|
1599
|
+
// Wrap in braces so the model sees "{site}" not "site" — makes the visual
|
|
1600
|
+
// connection to the source string obvious and reduces rename errors.
|
|
1601
|
+
placeholders: r.placeholders.map((p) => `{${p}}`),
|
|
1593
1602
|
glossary: r.glossary ?? [],
|
|
1594
1603
|
hasScreenshot: r.image !== void 0
|
|
1595
1604
|
};
|
|
@@ -1598,10 +1607,24 @@ function buildBatchPrompt(reqs) {
|
|
|
1598
1607
|
}
|
|
1599
1608
|
return { ...base, source: r.source };
|
|
1600
1609
|
});
|
|
1610
|
+
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.';
|
|
1601
1611
|
return `Translate every item below into the target locale: ${targetLocale}. All items share this one target language.
|
|
1602
|
-
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.
|
|
1612
|
+
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]}.
|
|
1603
1613
|
` + JSON.stringify(items, null, 2);
|
|
1604
1614
|
}
|
|
1615
|
+
function buildTranslateGemmaSystemPrompt(sourceLocale, targetLocale) {
|
|
1616
|
+
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}:`;
|
|
1617
|
+
}
|
|
1618
|
+
function buildTranslateGemmaUserPrompt(source) {
|
|
1619
|
+
return `
|
|
1620
|
+
|
|
1621
|
+
${source}`;
|
|
1622
|
+
}
|
|
1623
|
+
function parseTranslateGemmaResponse(text, id) {
|
|
1624
|
+
const translation = text.trim();
|
|
1625
|
+
if (!translation) return { id, error: "No translation returned" };
|
|
1626
|
+
return { id, translation };
|
|
1627
|
+
}
|
|
1605
1628
|
var BATCH_SCHEMA;
|
|
1606
1629
|
var init_provider = __esm({
|
|
1607
1630
|
"src/server/ai/provider.ts"() {
|
|
@@ -1766,7 +1789,7 @@ var init_anthropic = __esm({
|
|
|
1766
1789
|
const res = await this.client.messages.create({
|
|
1767
1790
|
model: this.config.model,
|
|
1768
1791
|
max_tokens: 8192,
|
|
1769
|
-
system: [{ type: "text", text: buildSystemPrompt(), cache_control: { type: "ephemeral" } }],
|
|
1792
|
+
system: [{ type: "text", text: buildSystemPrompt(batch.some((r) => r.plural !== void 0)), cache_control: { type: "ephemeral" } }],
|
|
1770
1793
|
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
1771
1794
|
messages: [{ role: "user", content }]
|
|
1772
1795
|
}, { signal });
|
|
@@ -1864,7 +1887,7 @@ var init_openai = __esm({
|
|
|
1864
1887
|
// via runBatched, so non-strict schema guidance is sufficient.
|
|
1865
1888
|
response_format: { type: "json_schema", json_schema: { name: "translations", schema: BATCH_SCHEMA, strict: false } },
|
|
1866
1889
|
messages: [
|
|
1867
|
-
{ role: "system", content: buildSystemPrompt() },
|
|
1890
|
+
{ role: "system", content: buildSystemPrompt(batch.some((r) => r.plural !== void 0)) },
|
|
1868
1891
|
{ role: "user", content: this.buildUserContent(batch) }
|
|
1869
1892
|
]
|
|
1870
1893
|
}, { signal });
|
|
@@ -1976,7 +1999,7 @@ var init_bedrock = __esm({
|
|
|
1976
1999
|
buildInput(batch) {
|
|
1977
2000
|
const input = {
|
|
1978
2001
|
modelId: this.config.model,
|
|
1979
|
-
system: [{ text: buildSystemPrompt() }],
|
|
2002
|
+
system: [{ text: buildSystemPrompt(batch.some((r) => r.plural !== void 0)) }],
|
|
1980
2003
|
messages: [{ role: "user", content: this.buildContentBlocks(batch) }]
|
|
1981
2004
|
};
|
|
1982
2005
|
if (!this.isMeta()) {
|
|
@@ -2043,6 +2066,7 @@ var OLLAMA_BASE_URL, OllamaProvider;
|
|
|
2043
2066
|
var init_ollama = __esm({
|
|
2044
2067
|
"src/server/ai/ollama.ts"() {
|
|
2045
2068
|
"use strict";
|
|
2069
|
+
init_provider();
|
|
2046
2070
|
init_openai();
|
|
2047
2071
|
OLLAMA_BASE_URL = "http://localhost:11434/v1";
|
|
2048
2072
|
OllamaProvider = class extends OpenAIProvider {
|
|
@@ -2052,6 +2076,26 @@ var init_ollama = __esm({
|
|
|
2052
2076
|
supportsVision() {
|
|
2053
2077
|
return this.config.vision === true;
|
|
2054
2078
|
}
|
|
2079
|
+
// translategemma expects a per-item system prompt, a plain-text user message
|
|
2080
|
+
// (no JSON, no structured output), and returns a plain-text translation.
|
|
2081
|
+
async callBatch(batch, signal) {
|
|
2082
|
+
if (this.config.promptStyle !== "translategemma") {
|
|
2083
|
+
return super.callBatch(batch, signal);
|
|
2084
|
+
}
|
|
2085
|
+
const results = [];
|
|
2086
|
+
for (const req of batch) {
|
|
2087
|
+
const res = await this.client.chat.completions.create({
|
|
2088
|
+
model: this.config.model,
|
|
2089
|
+
messages: [
|
|
2090
|
+
{ role: "system", content: buildTranslateGemmaSystemPrompt(req.sourceLocale, req.targetLocale) },
|
|
2091
|
+
{ role: "user", content: buildTranslateGemmaUserPrompt(req.source) }
|
|
2092
|
+
]
|
|
2093
|
+
}, { signal });
|
|
2094
|
+
const text = res.choices?.[0]?.message?.content ?? "";
|
|
2095
|
+
results.push(parseTranslateGemmaResponse(text, req.id));
|
|
2096
|
+
}
|
|
2097
|
+
return results;
|
|
2098
|
+
}
|
|
2055
2099
|
};
|
|
2056
2100
|
}
|
|
2057
2101
|
});
|
|
@@ -2138,7 +2182,7 @@ var init_claudecode = __esm({
|
|
|
2138
2182
|
const prompt = buildBatchPrompt(batch);
|
|
2139
2183
|
let result;
|
|
2140
2184
|
try {
|
|
2141
|
-
result = await this.spawnFn(prompt, buildSystemPrompt(), this.config.model);
|
|
2185
|
+
result = await this.spawnFn(prompt, buildSystemPrompt(batch.some((r) => r.plural !== void 0)), this.config.model);
|
|
2142
2186
|
} catch (err) {
|
|
2143
2187
|
if (signal?.aborted) return [];
|
|
2144
2188
|
throw err;
|
|
@@ -2226,18 +2270,36 @@ function coerceAi(raw) {
|
|
|
2226
2270
|
endpoint: typeof a.endpoint === "string" ? a.endpoint : null,
|
|
2227
2271
|
region: typeof a.region === "string" ? a.region : null,
|
|
2228
2272
|
batchSize: typeof a.batchSize === "number" && a.batchSize > 0 ? a.batchSize : DEFAULT_AI.batchSize,
|
|
2229
|
-
|
|
2273
|
+
concurrency: typeof a.concurrency === "number" && a.concurrency > 0 ? a.concurrency : void 0,
|
|
2274
|
+
contextBatchSize: typeof a.contextBatchSize === "number" && a.contextBatchSize > 0 ? a.contextBatchSize : void 0,
|
|
2275
|
+
contextConcurrency: typeof a.contextConcurrency === "number" && a.contextConcurrency > 0 ? a.contextConcurrency : void 0,
|
|
2276
|
+
vision: typeof a.vision === "boolean" ? a.vision : void 0,
|
|
2277
|
+
promptStyle: PROMPT_STYLES.includes(a.promptStyle) ? a.promptStyle : void 0
|
|
2230
2278
|
};
|
|
2231
2279
|
}
|
|
2280
|
+
function coerceProfiles(raw) {
|
|
2281
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
2282
|
+
const result = {};
|
|
2283
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
2284
|
+
if (typeof k === "string" && k.trim()) result[k] = coerceAi(v);
|
|
2285
|
+
}
|
|
2286
|
+
return result;
|
|
2287
|
+
}
|
|
2232
2288
|
function loadLocalSettings(projectRoot) {
|
|
2233
2289
|
const raw = readJson(settingsPath(projectRoot));
|
|
2234
|
-
|
|
2290
|
+
const profiles = coerceProfiles(raw.profiles);
|
|
2291
|
+
const activeProfile = typeof raw.activeProfile === "string" && raw.activeProfile in profiles ? raw.activeProfile : null;
|
|
2292
|
+
const baseAi = coerceAi(raw.ai);
|
|
2293
|
+
const ai = activeProfile ? profiles[activeProfile] : baseAi;
|
|
2294
|
+
return { ai, editor: isEditorId(raw.editor) ? raw.editor : DEFAULT_EDITOR, profiles, activeProfile };
|
|
2235
2295
|
}
|
|
2236
2296
|
function saveLocalSettings(projectRoot, patch) {
|
|
2237
2297
|
const path = settingsPath(projectRoot);
|
|
2238
2298
|
const merged = { ...readJson(path) };
|
|
2239
2299
|
if (patch.ai !== void 0) merged.ai = patch.ai;
|
|
2240
2300
|
if (patch.editor !== void 0) merged.editor = patch.editor;
|
|
2301
|
+
if (patch.profiles !== void 0) merged.profiles = patch.profiles;
|
|
2302
|
+
if (patch.activeProfile !== void 0) merged.activeProfile = patch.activeProfile;
|
|
2241
2303
|
ensureGlotfileDir(projectRoot);
|
|
2242
2304
|
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
2243
2305
|
}
|
|
@@ -2313,6 +2375,7 @@ function selectRequests(state, opts) {
|
|
|
2313
2375
|
id: String(id++),
|
|
2314
2376
|
key,
|
|
2315
2377
|
source: other,
|
|
2378
|
+
sourceLocale: state.config.sourceLocale,
|
|
2316
2379
|
context: entry.context,
|
|
2317
2380
|
targetLocale: locale,
|
|
2318
2381
|
maxLength: entry.maxLength,
|
|
@@ -2333,6 +2396,7 @@ function selectRequests(state, opts) {
|
|
|
2333
2396
|
id: String(id++),
|
|
2334
2397
|
key,
|
|
2335
2398
|
source,
|
|
2399
|
+
sourceLocale: state.config.sourceLocale,
|
|
2336
2400
|
context: entry.context,
|
|
2337
2401
|
targetLocale: locale,
|
|
2338
2402
|
maxLength: entry.maxLength,
|
|
@@ -2909,7 +2973,7 @@ function buildContextBatchPrompt(reqs) {
|
|
|
2909
2973
|
${s.lines}
|
|
2910
2974
|
\`\`\``;
|
|
2911
2975
|
}).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
|
|
2912
|
-
return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText
|
|
2976
|
+
return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText };
|
|
2913
2977
|
});
|
|
2914
2978
|
return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
|
|
2915
2979
|
}
|
|
@@ -3893,6 +3957,43 @@ function createApi(deps) {
|
|
|
3893
3957
|
saveLocalSettings(projectRoot, patch);
|
|
3894
3958
|
return c.json({ ok: true });
|
|
3895
3959
|
});
|
|
3960
|
+
app.get("/ai-profiles", (c) => {
|
|
3961
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3962
|
+
return c.json({ profiles: ls.profiles, activeProfile: ls.activeProfile });
|
|
3963
|
+
});
|
|
3964
|
+
app.put("/ai-profiles/:name", async (c) => {
|
|
3965
|
+
const name = c.req.param("name").trim();
|
|
3966
|
+
if (!name) return c.json({ error: "name required" }, 400);
|
|
3967
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3968
|
+
const err = aiConfigError(body);
|
|
3969
|
+
if (err) return c.json({ error: err }, 400);
|
|
3970
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3971
|
+
saveLocalSettings(projectRoot, { profiles: { ...ls.profiles, [name]: body } });
|
|
3972
|
+
return c.json({ ok: true });
|
|
3973
|
+
});
|
|
3974
|
+
app.delete("/ai-profiles/:name", (c) => {
|
|
3975
|
+
const name = c.req.param("name");
|
|
3976
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3977
|
+
if (!(name in ls.profiles)) return c.json({ error: "profile not found" }, 404);
|
|
3978
|
+
const profiles = { ...ls.profiles };
|
|
3979
|
+
delete profiles[name];
|
|
3980
|
+
const patch = { profiles };
|
|
3981
|
+
if (ls.activeProfile === name) patch.activeProfile = null;
|
|
3982
|
+
saveLocalSettings(projectRoot, patch);
|
|
3983
|
+
return c.json({ ok: true });
|
|
3984
|
+
});
|
|
3985
|
+
app.post("/ai-profiles/active", async (c) => {
|
|
3986
|
+
const { name } = await c.req.json().catch(() => ({}));
|
|
3987
|
+
if (name !== null && name !== void 0) {
|
|
3988
|
+
if (typeof name !== "string") return c.json({ error: "name must be a string or null" }, 400);
|
|
3989
|
+
const ls = loadLocalSettings(projectRoot);
|
|
3990
|
+
if (name !== "" && !(name in ls.profiles)) return c.json({ error: "profile not found" }, 404);
|
|
3991
|
+
saveLocalSettings(projectRoot, { activeProfile: name || null });
|
|
3992
|
+
} else {
|
|
3993
|
+
saveLocalSettings(projectRoot, { activeProfile: null });
|
|
3994
|
+
}
|
|
3995
|
+
return c.json({ ok: true });
|
|
3996
|
+
});
|
|
3896
3997
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
3897
3998
|
app.get("/files", (c) => {
|
|
3898
3999
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -4071,7 +4172,7 @@ function createApi(deps) {
|
|
|
4071
4172
|
return c.json({ removed });
|
|
4072
4173
|
});
|
|
4073
4174
|
app.post("/keys/bulk-meta", async (c) => {
|
|
4074
|
-
const { keys, addTags, removeTags, skipTranslate } = await c.req.json();
|
|
4175
|
+
const { keys, addTags, removeTags, skipTranslate, clearContext } = await c.req.json();
|
|
4075
4176
|
if (!Array.isArray(keys) || keys.length === 0) return c.json({ error: "keys must be a non-empty array" }, 400);
|
|
4076
4177
|
const s = load();
|
|
4077
4178
|
let updated = 0;
|
|
@@ -4089,6 +4190,11 @@ function createApi(deps) {
|
|
|
4089
4190
|
if (skipTranslate) setMetadata(s, key, { skipTranslate: true });
|
|
4090
4191
|
else delete entry.skipTranslate;
|
|
4091
4192
|
}
|
|
4193
|
+
if (clearContext === true) {
|
|
4194
|
+
delete entry.context;
|
|
4195
|
+
delete entry.contextSource;
|
|
4196
|
+
delete entry.contextAt;
|
|
4197
|
+
}
|
|
4092
4198
|
updated++;
|
|
4093
4199
|
}
|
|
4094
4200
|
persist(s);
|
|
@@ -4369,7 +4475,7 @@ function createApi(deps) {
|
|
|
4369
4475
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4370
4476
|
let totalWritten = 0;
|
|
4371
4477
|
const allErrors = [];
|
|
4372
|
-
const system = buildSystemPrompt();
|
|
4478
|
+
const system = buildSystemPrompt(reqs.some((r) => r.plural !== void 0));
|
|
4373
4479
|
const reqById = new Map(reqs.map((r) => [r.id, r]));
|
|
4374
4480
|
const localeTotals = /* @__PURE__ */ new Map();
|
|
4375
4481
|
for (const r of reqs) localeTotals.set(r.targetLocale, (localeTotals.get(r.targetLocale) ?? 0) + 1);
|
|
@@ -4398,7 +4504,7 @@ function createApi(deps) {
|
|
|
4398
4504
|
system,
|
|
4399
4505
|
items: batchResults.map((r) => {
|
|
4400
4506
|
const req = reqById.get(r.id);
|
|
4401
|
-
return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ?
|
|
4507
|
+
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 };
|
|
4402
4508
|
}),
|
|
4403
4509
|
results: batchResults
|
|
4404
4510
|
});
|
|
@@ -4413,7 +4519,7 @@ function createApi(deps) {
|
|
|
4413
4519
|
onLocaleDone: (locale) => {
|
|
4414
4520
|
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
4415
4521
|
}
|
|
4416
|
-
},
|
|
4522
|
+
}, aiCfg.concurrency, signal);
|
|
4417
4523
|
if (!signal?.aborted) {
|
|
4418
4524
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
4419
4525
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -4444,14 +4550,15 @@ function createApi(deps) {
|
|
|
4444
4550
|
}
|
|
4445
4551
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, projectRoot, provider.supportsVision());
|
|
4446
4552
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4447
|
-
const results = await runLocaleParallel(toTranslate, provider);
|
|
4448
|
-
|
|
4553
|
+
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4554
|
+
const latest = load();
|
|
4555
|
+
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
4449
4556
|
const entry = {
|
|
4450
4557
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4451
4558
|
kind: "translate",
|
|
4452
4559
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
4453
4560
|
model: aiCfg.model,
|
|
4454
|
-
system: buildSystemPrompt(),
|
|
4561
|
+
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
4455
4562
|
// Log the screenshot PATH only — never the image bytes.
|
|
4456
4563
|
items: toTranslate.map((r) => ({
|
|
4457
4564
|
id: r.id,
|
|
@@ -4460,13 +4567,13 @@ function createApi(deps) {
|
|
|
4460
4567
|
targetLocale: r.targetLocale,
|
|
4461
4568
|
context: r.context,
|
|
4462
4569
|
glossary: r.glossary,
|
|
4463
|
-
screenshot:
|
|
4570
|
+
screenshot: latest.keys[r.key]?.screenshot
|
|
4464
4571
|
})),
|
|
4465
4572
|
results
|
|
4466
4573
|
};
|
|
4467
4574
|
appendLog(projectRoot, entry);
|
|
4575
|
+
persist(latest);
|
|
4468
4576
|
}
|
|
4469
|
-
persist(s);
|
|
4470
4577
|
return c.json({ requested: reqs.length, written, errors });
|
|
4471
4578
|
}));
|
|
4472
4579
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
@@ -4522,55 +4629,95 @@ function createApi(deps) {
|
|
|
4522
4629
|
return c.json({ indexed: true, scannedAt: cache2.scannedAt, used: computeUsedKeys(load(), cache2) });
|
|
4523
4630
|
});
|
|
4524
4631
|
app.post("/context/build", async (c) => {
|
|
4632
|
+
const signal = c.req.raw.signal;
|
|
4525
4633
|
const body = await c.req.json().catch(() => ({}));
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
)
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4634
|
+
return streamSSE(c, async (stream) => {
|
|
4635
|
+
const s = load();
|
|
4636
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
4637
|
+
if (!cache2) {
|
|
4638
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: "No usage index found. Run 'glotfile scan' first." }) });
|
|
4639
|
+
return;
|
|
4640
|
+
}
|
|
4641
|
+
const targets = selectContextTargets(s, {
|
|
4642
|
+
all: body.all,
|
|
4643
|
+
keyGlob: body.keyGlob,
|
|
4644
|
+
limit: body.limit,
|
|
4645
|
+
since: body.since,
|
|
4646
|
+
keys: body.keys
|
|
4647
|
+
}, cache2, body.lastRunAt);
|
|
4648
|
+
if (!targets.length) {
|
|
4649
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: 0, written: 0, errors: [] }) });
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
4653
|
+
let provider;
|
|
4654
|
+
try {
|
|
4655
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
4656
|
+
} catch (e) {
|
|
4657
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4660
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
|
|
4661
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
4662
|
+
for (const target of targets) {
|
|
4663
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
4664
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
4665
|
+
key: r.key,
|
|
4666
|
+
file,
|
|
4667
|
+
line: r.line,
|
|
4668
|
+
col: r.col,
|
|
4669
|
+
scanner: r.scanner
|
|
4670
|
+
}))
|
|
4671
|
+
);
|
|
4672
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
4673
|
+
}
|
|
4674
|
+
const system = buildContextSystemPrompt();
|
|
4675
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
4676
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
4677
|
+
const chunks = [];
|
|
4678
|
+
for (let i = 0; i < targets.length; i += batchSize) chunks.push(targets.slice(i, i + batchSize));
|
|
4679
|
+
let totalWritten = 0;
|
|
4680
|
+
let totalDone = 0;
|
|
4681
|
+
const allErrors = [];
|
|
4682
|
+
let next = 0;
|
|
4683
|
+
async function worker() {
|
|
4684
|
+
while (next < chunks.length) {
|
|
4685
|
+
if (signal?.aborted) break;
|
|
4686
|
+
const chunk2 = chunks[next++];
|
|
4687
|
+
let raw;
|
|
4688
|
+
try {
|
|
4689
|
+
raw = await provider.complete({ system, content: [{ type: "text", text: buildContextBatchPrompt(chunk2) }], schema: CONTEXT_BATCH_SCHEMA });
|
|
4690
|
+
} catch (e) {
|
|
4691
|
+
totalDone += chunk2.length;
|
|
4692
|
+
allErrors.push(...chunk2.map((t) => ({ key: t.key, error: e.message })));
|
|
4693
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done: totalDone, total: targets.length, written: totalWritten }) });
|
|
4694
|
+
continue;
|
|
4695
|
+
}
|
|
4696
|
+
if (signal?.aborted) break;
|
|
4697
|
+
const batch = raw;
|
|
4698
|
+
const fresh = load();
|
|
4699
|
+
const { written, errors } = applyContext(fresh, chunk2, batch.items ?? []);
|
|
4700
|
+
appendLog(projectRoot, {
|
|
4701
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4702
|
+
kind: "context",
|
|
4703
|
+
summary: `Generated context for ${chunk2.length} key(s)`,
|
|
4704
|
+
model: aiCfg.model,
|
|
4705
|
+
system,
|
|
4706
|
+
items: chunk2.map((t) => ({ id: t.id, key: t.key, source: t.source })),
|
|
4707
|
+
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
|
|
4708
|
+
});
|
|
4709
|
+
persist(fresh);
|
|
4710
|
+
totalWritten += written;
|
|
4711
|
+
totalDone += chunk2.length;
|
|
4712
|
+
allErrors.push(...errors);
|
|
4713
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done: totalDone, total: targets.length, written: totalWritten }) });
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
4717
|
+
if (signal?.aborted) return;
|
|
4718
|
+
console.log(`[context] ${totalWritten} context(s) written${allErrors.length ? `, ${allErrors.length} error(s)` : ""}`);
|
|
4719
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
4570
4720
|
});
|
|
4571
|
-
persist(s);
|
|
4572
|
-
console.log(`[context] ${written} context(s) written${errors.length ? `, ${errors.length} error(s)` : ""}`);
|
|
4573
|
-
return c.json({ requested: targets.length, written, errors });
|
|
4574
4721
|
});
|
|
4575
4722
|
app.onError(
|
|
4576
4723
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|
|
@@ -5297,7 +5444,7 @@ async function runTranslate(args) {
|
|
|
5297
5444
|
kind: "translate",
|
|
5298
5445
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
5299
5446
|
model: ai.model,
|
|
5300
|
-
system: buildSystemPrompt(),
|
|
5447
|
+
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
5301
5448
|
items: toTranslate.map((r) => ({
|
|
5302
5449
|
id: r.id,
|
|
5303
5450
|
key: r.key,
|
|
@@ -5420,10 +5567,32 @@ async function runBuildContext(args) {
|
|
|
5420
5567
|
target.usageSnippets = extractSnippets(refs, projectRoot, fileCache);
|
|
5421
5568
|
}
|
|
5422
5569
|
const system = buildContextSystemPrompt();
|
|
5423
|
-
const
|
|
5424
|
-
const
|
|
5425
|
-
const
|
|
5426
|
-
const
|
|
5570
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
5571
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
5572
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
5573
|
+
const chunks = [];
|
|
5574
|
+
for (let i = 0; i < targets.length; i += batchSize) chunks.push(targets.slice(i, i + batchSize));
|
|
5575
|
+
let written = 0;
|
|
5576
|
+
const errors = [];
|
|
5577
|
+
let next = 0;
|
|
5578
|
+
async function worker() {
|
|
5579
|
+
while (next < chunks.length) {
|
|
5580
|
+
const chunk2 = chunks[next++];
|
|
5581
|
+
let raw;
|
|
5582
|
+
try {
|
|
5583
|
+
raw = await provider.complete({ system, content: [{ type: "text", text: buildContextBatchPrompt(chunk2) }], schema: CONTEXT_BATCH_SCHEMA });
|
|
5584
|
+
} catch (e2) {
|
|
5585
|
+
errors.push(...chunk2.map((t) => ({ key: t.key, error: e2.message })));
|
|
5586
|
+
continue;
|
|
5587
|
+
}
|
|
5588
|
+
const batch = raw;
|
|
5589
|
+
const { written: w, errors: e } = applyContext(state, chunk2, batch.items ?? []);
|
|
5590
|
+
written += w;
|
|
5591
|
+
errors.push(...e);
|
|
5592
|
+
console.log(`[${next * batchSize}/${targets.length}] wrote ${w}`);
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
5427
5596
|
saveState(args.statePath, state);
|
|
5428
5597
|
console.log(`Wrote context for ${written} key(s).`);
|
|
5429
5598
|
for (const e of errors) console.warn(`skip ${e.key}: ${e.error}`);
|