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/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 {
|
|
@@ -2050,7 +2074,27 @@ var init_ollama = __esm({
|
|
|
2050
2074
|
super(config, client ?? loadOpenAIClient(ollamaClientOptions(config)));
|
|
2051
2075
|
}
|
|
2052
2076
|
supportsVision() {
|
|
2053
|
-
return
|
|
2077
|
+
return this.config.vision === true;
|
|
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;
|
|
2054
2098
|
}
|
|
2055
2099
|
};
|
|
2056
2100
|
}
|
|
@@ -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;
|
|
@@ -2225,18 +2269,37 @@ function coerceAi(raw) {
|
|
|
2225
2269
|
model: typeof a.model === "string" && a.model ? a.model : DEFAULT_AI.model,
|
|
2226
2270
|
endpoint: typeof a.endpoint === "string" ? a.endpoint : null,
|
|
2227
2271
|
region: typeof a.region === "string" ? a.region : null,
|
|
2228
|
-
batchSize: typeof a.batchSize === "number" && a.batchSize > 0 ? a.batchSize : DEFAULT_AI.batchSize
|
|
2272
|
+
batchSize: typeof a.batchSize === "number" && a.batchSize > 0 ? a.batchSize : DEFAULT_AI.batchSize,
|
|
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
|
|
2229
2278
|
};
|
|
2230
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
|
+
}
|
|
2231
2288
|
function loadLocalSettings(projectRoot) {
|
|
2232
2289
|
const raw = readJson(settingsPath(projectRoot));
|
|
2233
|
-
|
|
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 };
|
|
2234
2295
|
}
|
|
2235
2296
|
function saveLocalSettings(projectRoot, patch) {
|
|
2236
2297
|
const path = settingsPath(projectRoot);
|
|
2237
2298
|
const merged = { ...readJson(path) };
|
|
2238
2299
|
if (patch.ai !== void 0) merged.ai = patch.ai;
|
|
2239
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;
|
|
2240
2303
|
ensureGlotfileDir(projectRoot);
|
|
2241
2304
|
writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
|
|
2242
2305
|
}
|
|
@@ -2312,6 +2375,7 @@ function selectRequests(state, opts) {
|
|
|
2312
2375
|
id: String(id++),
|
|
2313
2376
|
key,
|
|
2314
2377
|
source: other,
|
|
2378
|
+
sourceLocale: state.config.sourceLocale,
|
|
2315
2379
|
context: entry.context,
|
|
2316
2380
|
targetLocale: locale,
|
|
2317
2381
|
maxLength: entry.maxLength,
|
|
@@ -2332,6 +2396,7 @@ function selectRequests(state, opts) {
|
|
|
2332
2396
|
id: String(id++),
|
|
2333
2397
|
key,
|
|
2334
2398
|
source,
|
|
2399
|
+
sourceLocale: state.config.sourceLocale,
|
|
2335
2400
|
context: entry.context,
|
|
2336
2401
|
targetLocale: locale,
|
|
2337
2402
|
maxLength: entry.maxLength,
|
|
@@ -2908,7 +2973,7 @@ function buildContextBatchPrompt(reqs) {
|
|
|
2908
2973
|
${s.lines}
|
|
2909
2974
|
\`\`\``;
|
|
2910
2975
|
}).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
|
|
2911
|
-
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 };
|
|
2912
2977
|
});
|
|
2913
2978
|
return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
|
|
2914
2979
|
}
|
|
@@ -3822,7 +3887,7 @@ var init_ui_prefs = __esm({
|
|
|
3822
3887
|
// src/server/api.ts
|
|
3823
3888
|
import { Hono } from "hono";
|
|
3824
3889
|
import { streamSSE } from "hono/streaming";
|
|
3825
|
-
import { readFileSync as readFileSync14, existsSync as existsSync10, readdirSync as readdirSync7, rmSync as rmSync4 } from "fs";
|
|
3890
|
+
import { readFileSync as readFileSync14, existsSync as existsSync10, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "fs";
|
|
3826
3891
|
import { dirname as dirname2, resolve as resolve9, basename, relative as relative3, sep } from "path";
|
|
3827
3892
|
function projectName(root) {
|
|
3828
3893
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
@@ -3892,32 +3957,90 @@ function createApi(deps) {
|
|
|
3892
3957
|
saveLocalSettings(projectRoot, patch);
|
|
3893
3958
|
return c.json({ ok: true });
|
|
3894
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
|
+
});
|
|
3895
3997
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
3896
3998
|
app.get("/files", (c) => {
|
|
3897
3999
|
const found = /* @__PURE__ */ new Map();
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
abs = resolve9(projectRoot, `${name}.json`);
|
|
3908
|
-
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3909
|
-
abs = resolve9(projectRoot, name);
|
|
3910
|
-
} else {
|
|
3911
|
-
continue;
|
|
3912
|
-
}
|
|
3913
|
-
if (found.has(abs)) continue;
|
|
4000
|
+
const activeRel = relative3(projectRoot, deps.statePath);
|
|
4001
|
+
found.set(deps.statePath, {
|
|
4002
|
+
name: basename(deps.statePath),
|
|
4003
|
+
path: deps.statePath,
|
|
4004
|
+
relDir: activeRel !== basename(activeRel) ? dirname2(activeRel) : void 0
|
|
4005
|
+
});
|
|
4006
|
+
function walk(dir, depth) {
|
|
4007
|
+
if (depth > 4) return;
|
|
4008
|
+
let entries = [];
|
|
3914
4009
|
try {
|
|
3915
|
-
|
|
3916
|
-
found.set(abs, { name: basename(abs), path: abs });
|
|
4010
|
+
entries = readdirSync7(dir);
|
|
3917
4011
|
} catch {
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
for (const name of entries) {
|
|
4015
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4016
|
+
const abs = resolve9(dir, name);
|
|
4017
|
+
let filePath = null;
|
|
4018
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync10(resolve9(abs, "config.json"))) {
|
|
4019
|
+
filePath = resolve9(dir, `${name}.json`);
|
|
4020
|
+
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
4021
|
+
filePath = abs;
|
|
4022
|
+
} else {
|
|
4023
|
+
try {
|
|
4024
|
+
if (statSync4(abs).isDirectory()) walk(abs, depth + 1);
|
|
4025
|
+
} catch {
|
|
4026
|
+
}
|
|
4027
|
+
continue;
|
|
4028
|
+
}
|
|
4029
|
+
if (found.has(filePath)) continue;
|
|
4030
|
+
try {
|
|
4031
|
+
loadState(filePath);
|
|
4032
|
+
const rel = relative3(projectRoot, filePath);
|
|
4033
|
+
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname2(rel) : void 0 });
|
|
4034
|
+
} catch {
|
|
4035
|
+
}
|
|
3918
4036
|
}
|
|
3919
4037
|
}
|
|
3920
|
-
|
|
4038
|
+
walk(projectRoot, 0);
|
|
4039
|
+
const files = [...found.values()].sort((a, b) => {
|
|
4040
|
+
const ka = a.relDir ? `${a.relDir}/${a.name}` : a.name;
|
|
4041
|
+
const kb = b.relDir ? `${b.relDir}/${b.name}` : b.name;
|
|
4042
|
+
return ka.localeCompare(kb);
|
|
4043
|
+
});
|
|
3921
4044
|
return c.json(files);
|
|
3922
4045
|
});
|
|
3923
4046
|
app.post("/file", async (c) => {
|
|
@@ -4049,7 +4172,7 @@ function createApi(deps) {
|
|
|
4049
4172
|
return c.json({ removed });
|
|
4050
4173
|
});
|
|
4051
4174
|
app.post("/keys/bulk-meta", async (c) => {
|
|
4052
|
-
const { keys, addTags, removeTags, skipTranslate } = await c.req.json();
|
|
4175
|
+
const { keys, addTags, removeTags, skipTranslate, clearContext } = await c.req.json();
|
|
4053
4176
|
if (!Array.isArray(keys) || keys.length === 0) return c.json({ error: "keys must be a non-empty array" }, 400);
|
|
4054
4177
|
const s = load();
|
|
4055
4178
|
let updated = 0;
|
|
@@ -4067,6 +4190,11 @@ function createApi(deps) {
|
|
|
4067
4190
|
if (skipTranslate) setMetadata(s, key, { skipTranslate: true });
|
|
4068
4191
|
else delete entry.skipTranslate;
|
|
4069
4192
|
}
|
|
4193
|
+
if (clearContext === true) {
|
|
4194
|
+
delete entry.context;
|
|
4195
|
+
delete entry.contextSource;
|
|
4196
|
+
delete entry.contextAt;
|
|
4197
|
+
}
|
|
4070
4198
|
updated++;
|
|
4071
4199
|
}
|
|
4072
4200
|
persist(s);
|
|
@@ -4347,7 +4475,7 @@ function createApi(deps) {
|
|
|
4347
4475
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4348
4476
|
let totalWritten = 0;
|
|
4349
4477
|
const allErrors = [];
|
|
4350
|
-
const system = buildSystemPrompt();
|
|
4478
|
+
const system = buildSystemPrompt(reqs.some((r) => r.plural !== void 0));
|
|
4351
4479
|
const reqById = new Map(reqs.map((r) => [r.id, r]));
|
|
4352
4480
|
const localeTotals = /* @__PURE__ */ new Map();
|
|
4353
4481
|
for (const r of reqs) localeTotals.set(r.targetLocale, (localeTotals.get(r.targetLocale) ?? 0) + 1);
|
|
@@ -4363,8 +4491,9 @@ function createApi(deps) {
|
|
|
4363
4491
|
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
4364
4492
|
},
|
|
4365
4493
|
onBatchComplete: (done, total, batchResults, locale) => {
|
|
4366
|
-
const
|
|
4367
|
-
|
|
4494
|
+
const fresh = load();
|
|
4495
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
4496
|
+
persist(fresh);
|
|
4368
4497
|
totalWritten += written;
|
|
4369
4498
|
allErrors.push(...errors);
|
|
4370
4499
|
appendLog(projectRoot, {
|
|
@@ -4375,7 +4504,7 @@ function createApi(deps) {
|
|
|
4375
4504
|
system,
|
|
4376
4505
|
items: batchResults.map((r) => {
|
|
4377
4506
|
const req = reqById.get(r.id);
|
|
4378
|
-
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 };
|
|
4379
4508
|
}),
|
|
4380
4509
|
results: batchResults
|
|
4381
4510
|
});
|
|
@@ -4390,7 +4519,7 @@ function createApi(deps) {
|
|
|
4390
4519
|
onLocaleDone: (locale) => {
|
|
4391
4520
|
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
4392
4521
|
}
|
|
4393
|
-
},
|
|
4522
|
+
}, aiCfg.concurrency, signal);
|
|
4394
4523
|
if (!signal?.aborted) {
|
|
4395
4524
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
4396
4525
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -4421,14 +4550,15 @@ function createApi(deps) {
|
|
|
4421
4550
|
}
|
|
4422
4551
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, projectRoot, provider.supportsVision());
|
|
4423
4552
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4424
|
-
const results = await runLocaleParallel(toTranslate, provider);
|
|
4425
|
-
|
|
4553
|
+
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4554
|
+
const latest = load();
|
|
4555
|
+
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
4426
4556
|
const entry = {
|
|
4427
4557
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4428
4558
|
kind: "translate",
|
|
4429
4559
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
4430
4560
|
model: aiCfg.model,
|
|
4431
|
-
system: buildSystemPrompt(),
|
|
4561
|
+
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
4432
4562
|
// Log the screenshot PATH only — never the image bytes.
|
|
4433
4563
|
items: toTranslate.map((r) => ({
|
|
4434
4564
|
id: r.id,
|
|
@@ -4437,13 +4567,13 @@ function createApi(deps) {
|
|
|
4437
4567
|
targetLocale: r.targetLocale,
|
|
4438
4568
|
context: r.context,
|
|
4439
4569
|
glossary: r.glossary,
|
|
4440
|
-
screenshot:
|
|
4570
|
+
screenshot: latest.keys[r.key]?.screenshot
|
|
4441
4571
|
})),
|
|
4442
4572
|
results
|
|
4443
4573
|
};
|
|
4444
4574
|
appendLog(projectRoot, entry);
|
|
4575
|
+
persist(latest);
|
|
4445
4576
|
}
|
|
4446
|
-
persist(s);
|
|
4447
4577
|
return c.json({ requested: reqs.length, written, errors });
|
|
4448
4578
|
}));
|
|
4449
4579
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
@@ -4499,55 +4629,95 @@ function createApi(deps) {
|
|
|
4499
4629
|
return c.json({ indexed: true, scannedAt: cache2.scannedAt, used: computeUsedKeys(load(), cache2) });
|
|
4500
4630
|
});
|
|
4501
4631
|
app.post("/context/build", async (c) => {
|
|
4632
|
+
const signal = c.req.raw.signal;
|
|
4502
4633
|
const body = await c.req.json().catch(() => ({}));
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
)
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
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 }) });
|
|
4547
4720
|
});
|
|
4548
|
-
persist(s);
|
|
4549
|
-
console.log(`[context] ${written} context(s) written${errors.length ? `, ${errors.length} error(s)` : ""}`);
|
|
4550
|
-
return c.json({ requested: targets.length, written, errors });
|
|
4551
4721
|
});
|
|
4552
4722
|
app.onError(
|
|
4553
4723
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|
|
@@ -5274,7 +5444,7 @@ async function runTranslate(args) {
|
|
|
5274
5444
|
kind: "translate",
|
|
5275
5445
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
5276
5446
|
model: ai.model,
|
|
5277
|
-
system: buildSystemPrompt(),
|
|
5447
|
+
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
5278
5448
|
items: toTranslate.map((r) => ({
|
|
5279
5449
|
id: r.id,
|
|
5280
5450
|
key: r.key,
|
|
@@ -5397,10 +5567,32 @@ async function runBuildContext(args) {
|
|
|
5397
5567
|
target.usageSnippets = extractSnippets(refs, projectRoot, fileCache);
|
|
5398
5568
|
}
|
|
5399
5569
|
const system = buildContextSystemPrompt();
|
|
5400
|
-
const
|
|
5401
|
-
const
|
|
5402
|
-
const
|
|
5403
|
-
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));
|
|
5404
5596
|
saveState(args.statePath, state);
|
|
5405
5597
|
console.log(`Wrote context for ${written} key(s).`);
|
|
5406
5598
|
for (const e of errors) console.warn(`skip ${e.key}: ${e.error}`);
|