glotfile 0.4.6 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli.js +239 -40
- package/dist/server/server.js +269 -113
- package/dist/ui/assets/index-DC89onXX.js +1904 -0
- package/dist/ui/assets/index-DPfAS4pJ.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-3IIAIpZW.css +0 -1
- package/dist/ui/assets/index-CrR0eUwT.js +0 -1891
package/dist/server/server.js
CHANGED
|
@@ -202,9 +202,6 @@ function validate(raw) {
|
|
|
202
202
|
if (lint.ignore !== void 0 && (!Array.isArray(lint.ignore) || !lint.ignore.every((g) => typeof g === "string"))) {
|
|
203
203
|
fail("config.lint.ignore must be an array of strings");
|
|
204
204
|
}
|
|
205
|
-
if (lint.dictionary !== void 0 && (!Array.isArray(lint.dictionary) || !lint.dictionary.every((w) => typeof w === "string"))) {
|
|
206
|
-
fail("config.lint.dictionary must be an array of strings");
|
|
207
|
-
}
|
|
208
205
|
if (lint.spelling !== void 0) {
|
|
209
206
|
if (!isObject(lint.spelling)) fail("config.lint.spelling must be an object");
|
|
210
207
|
if (lint.spelling.locales !== void 0 && (!isObject(lint.spelling.locales) || !Object.values(lint.spelling.locales).every((v) => typeof v === "string"))) {
|
|
@@ -269,6 +266,20 @@ function validate(raw) {
|
|
|
269
266
|
lv.value = lv.value.trim();
|
|
270
267
|
}
|
|
271
268
|
}
|
|
269
|
+
if (entry.suppressions !== void 0) {
|
|
270
|
+
if (!Array.isArray(entry.suppressions)) fail(`key "${key}" suppressions must be an array`);
|
|
271
|
+
for (const s of entry.suppressions) {
|
|
272
|
+
if (!isObject(s) || typeof s.locale !== "string" || typeof s.source !== "string") {
|
|
273
|
+
fail(`key "${key}" has an invalid suppression (needs string rule, locale, source)`);
|
|
274
|
+
}
|
|
275
|
+
if (!RULE_IDS.includes(s.rule)) {
|
|
276
|
+
fail(`key "${key}" suppression has unknown rule id "${String(s.rule)}"`);
|
|
277
|
+
}
|
|
278
|
+
if (s.at !== void 0 && typeof s.at !== "string") {
|
|
279
|
+
fail(`key "${key}" suppression "at" must be a string`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
272
283
|
if (entry.notes !== void 0) {
|
|
273
284
|
if (!Array.isArray(entry.notes)) fail(`key "${key}" notes must be an array`);
|
|
274
285
|
for (const n of entry.notes) {
|
|
@@ -512,6 +523,30 @@ function normalizeSource(value) {
|
|
|
512
523
|
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
513
524
|
}
|
|
514
525
|
|
|
526
|
+
// src/server/lint/suppress.ts
|
|
527
|
+
import { createHash } from "crypto";
|
|
528
|
+
function sourceSignature(entry, sourceLocale) {
|
|
529
|
+
const lv = entry.values[sourceLocale];
|
|
530
|
+
if (entry.plural) {
|
|
531
|
+
return Object.entries(lv?.forms ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([cat, body]) => `${cat}:${normalizeSource(body ?? "")}`).join("|");
|
|
532
|
+
}
|
|
533
|
+
return normalizeSource(lv?.value ?? "");
|
|
534
|
+
}
|
|
535
|
+
function sourceHash(entry, sourceLocale) {
|
|
536
|
+
return createHash("sha256").update(sourceSignature(entry, sourceLocale)).digest("hex").slice(0, 12);
|
|
537
|
+
}
|
|
538
|
+
function findSuppression(entry, sourceLocale, ruleId, locale) {
|
|
539
|
+
if (!entry.suppressions?.length) return void 0;
|
|
540
|
+
const current = sourceHash(entry, sourceLocale);
|
|
541
|
+
return entry.suppressions.find((s) => s.rule === ruleId && s.locale === locale && s.source === current);
|
|
542
|
+
}
|
|
543
|
+
function pruneStaleSuppressions(entry, sourceLocale) {
|
|
544
|
+
if (!entry.suppressions?.length) return;
|
|
545
|
+
const current = sourceHash(entry, sourceLocale);
|
|
546
|
+
entry.suppressions = entry.suppressions.filter((s) => s.source === current);
|
|
547
|
+
if (!entry.suppressions.length) delete entry.suppressions;
|
|
548
|
+
}
|
|
549
|
+
|
|
515
550
|
// src/server/state.ts
|
|
516
551
|
var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
517
552
|
function canonLocale(locale) {
|
|
@@ -532,6 +567,7 @@ function normalizeState(state) {
|
|
|
532
567
|
const remapped = {};
|
|
533
568
|
for (const [loc, lv] of Object.entries(entry.values)) remapped[canonLocale(loc)] = lv;
|
|
534
569
|
entry.values = remapped;
|
|
570
|
+
for (const s of entry.suppressions ?? []) s.locale = canonLocale(s.locale);
|
|
535
571
|
}
|
|
536
572
|
for (const output of state.config.outputs) {
|
|
537
573
|
if (!output.localeMap) continue;
|
|
@@ -623,6 +659,7 @@ function setSourceValue(state, key, value) {
|
|
|
623
659
|
lv.state = "needs-review";
|
|
624
660
|
}
|
|
625
661
|
}
|
|
662
|
+
pruneStaleSuppressions(entry, state.config.sourceLocale);
|
|
626
663
|
}
|
|
627
664
|
}
|
|
628
665
|
function setTargetValue(state, key, locale, value, clock = systemClock) {
|
|
@@ -646,6 +683,7 @@ function setSourcePluralForms(state, key, forms) {
|
|
|
646
683
|
lv.state = "needs-review";
|
|
647
684
|
}
|
|
648
685
|
}
|
|
686
|
+
pruneStaleSuppressions(entry, state.config.sourceLocale);
|
|
649
687
|
}
|
|
650
688
|
}
|
|
651
689
|
function setPluralForms(state, key, locale, forms, clock = systemClock) {
|
|
@@ -719,6 +757,19 @@ function deleteNote(state, key, id) {
|
|
|
719
757
|
if (!entry.notes) return;
|
|
720
758
|
entry.notes = entry.notes.filter((n) => n.id !== id);
|
|
721
759
|
}
|
|
760
|
+
function addSuppression(state, key, rule, locale, clock = systemClock) {
|
|
761
|
+
const entry = requireKey(state, key);
|
|
762
|
+
if (!RULE_IDS.includes(rule)) throw new GlotfileError(`Unknown lint rule: ${rule}`);
|
|
763
|
+
const list = (entry.suppressions ?? []).filter((s) => !(s.rule === rule && s.locale === locale));
|
|
764
|
+
list.push({ rule, locale, source: sourceHash(entry, state.config.sourceLocale), at: clock() });
|
|
765
|
+
entry.suppressions = list;
|
|
766
|
+
}
|
|
767
|
+
function removeSuppression(state, key, rule, locale) {
|
|
768
|
+
const entry = requireKey(state, key);
|
|
769
|
+
if (!entry.suppressions) return;
|
|
770
|
+
entry.suppressions = entry.suppressions.filter((s) => !(s.rule === rule && s.locale === locale));
|
|
771
|
+
if (!entry.suppressions.length) delete entry.suppressions;
|
|
772
|
+
}
|
|
722
773
|
function upsertGlossaryEntry(state, entry) {
|
|
723
774
|
const i = state.glossary.findIndex((e) => e.term === entry.term);
|
|
724
775
|
if (i === -1) state.glossary.push(entry);
|
|
@@ -755,6 +806,23 @@ function applyMachineTranslationForms(state, key, locale, forms, clock = systemC
|
|
|
755
806
|
return true;
|
|
756
807
|
}
|
|
757
808
|
|
|
809
|
+
// src/server/lint/accept.ts
|
|
810
|
+
function acceptFindings(state, findings, opts = {}, clock = systemClock) {
|
|
811
|
+
const byRule = {};
|
|
812
|
+
let accepted = 0;
|
|
813
|
+
for (const f of findings) {
|
|
814
|
+
if (f.locale === "" || f.suppressed) continue;
|
|
815
|
+
if (f.severity === "error" && !opts.includeErrors) continue;
|
|
816
|
+
if (opts.rules && !opts.rules.includes(f.ruleId)) continue;
|
|
817
|
+
if (opts.locales && !opts.locales.includes(f.locale)) continue;
|
|
818
|
+
if (!state.keys[f.key]) continue;
|
|
819
|
+
addSuppression(state, f.key, f.ruleId, f.locale, clock);
|
|
820
|
+
byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1;
|
|
821
|
+
accepted++;
|
|
822
|
+
}
|
|
823
|
+
return { accepted, byRule };
|
|
824
|
+
}
|
|
825
|
+
|
|
758
826
|
// src/server/scan.ts
|
|
759
827
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
760
828
|
import { resolve as resolve2 } from "path";
|
|
@@ -1425,6 +1493,88 @@ function globToRegExp2(glob) {
|
|
|
1425
1493
|
return new RegExp(`^${escaped}$`);
|
|
1426
1494
|
}
|
|
1427
1495
|
|
|
1496
|
+
// src/server/ai/batch.ts
|
|
1497
|
+
var MalformedReplyError = class extends Error {
|
|
1498
|
+
constructor(raw) {
|
|
1499
|
+
super("Model reply was not valid translation JSON.");
|
|
1500
|
+
this.raw = raw;
|
|
1501
|
+
this.name = "MalformedReplyError";
|
|
1502
|
+
}
|
|
1503
|
+
raw;
|
|
1504
|
+
};
|
|
1505
|
+
function parseReplyItems(text) {
|
|
1506
|
+
let parsed;
|
|
1507
|
+
try {
|
|
1508
|
+
parsed = JSON.parse(text);
|
|
1509
|
+
} catch {
|
|
1510
|
+
throw new MalformedReplyError(text);
|
|
1511
|
+
}
|
|
1512
|
+
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
1513
|
+
return parsed.items;
|
|
1514
|
+
}
|
|
1515
|
+
function chunk(items, size) {
|
|
1516
|
+
const out = [];
|
|
1517
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
1518
|
+
return out;
|
|
1519
|
+
}
|
|
1520
|
+
function validateTranslation(req, translation) {
|
|
1521
|
+
if (translation === void 0) return { id: req.id, error: "No translation returned." };
|
|
1522
|
+
if (!placeholdersMatch(req.source, translation)) {
|
|
1523
|
+
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
1524
|
+
}
|
|
1525
|
+
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
1526
|
+
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
1527
|
+
}
|
|
1528
|
+
return { id: req.id, translation };
|
|
1529
|
+
}
|
|
1530
|
+
function validatePlural(req, forms) {
|
|
1531
|
+
if (!forms) return { id: req.id, error: "No translation returned." };
|
|
1532
|
+
const plural = req.plural;
|
|
1533
|
+
if (!plural) return { id: req.id, error: "validatePlural called on a non-plural request." };
|
|
1534
|
+
const cats = plural.categories;
|
|
1535
|
+
const missing = cats.filter((c) => typeof forms[c] !== "string");
|
|
1536
|
+
if (missing.length) return { id: req.id, error: `Missing plural categories: ${missing.join(", ")}.` };
|
|
1537
|
+
const badPh = cats.find((c) => !pluralFormPlaceholdersMatch(c, req.source, forms[c]));
|
|
1538
|
+
if (badPh) return { id: req.id, error: `Placeholder mismatch in plural form "${badPh}".` };
|
|
1539
|
+
if (req.maxLength !== void 0) {
|
|
1540
|
+
const over = cats.find((c) => forms[c].length > req.maxLength);
|
|
1541
|
+
if (over) return { id: req.id, error: `Plural form "${over}" exceeds maxLength (${forms[over].length} > ${req.maxLength}).` };
|
|
1542
|
+
}
|
|
1543
|
+
const out = {};
|
|
1544
|
+
for (const c of cats) out[c] = forms[c];
|
|
1545
|
+
return { id: req.id, forms: out };
|
|
1546
|
+
}
|
|
1547
|
+
function validateReply(req, item) {
|
|
1548
|
+
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
1549
|
+
}
|
|
1550
|
+
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
1551
|
+
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
1552
|
+
async function resolveBatch(batch, isRetry = false) {
|
|
1553
|
+
let reply;
|
|
1554
|
+
try {
|
|
1555
|
+
reply = await callBatch(batch, signal);
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
if (!(err instanceof MalformedReplyError)) throw err;
|
|
1558
|
+
onMalformedReply?.(err.raw, batch.length);
|
|
1559
|
+
if (signal?.aborted) return failBatch(batch);
|
|
1560
|
+
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
1561
|
+
const mid = Math.ceil(batch.length / 2);
|
|
1562
|
+
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
1563
|
+
}
|
|
1564
|
+
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
1565
|
+
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
1566
|
+
}
|
|
1567
|
+
const results = [];
|
|
1568
|
+
const total = reqs.length;
|
|
1569
|
+
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
1570
|
+
if (signal?.aborted) break;
|
|
1571
|
+
const batchResults = await resolveBatch(batch);
|
|
1572
|
+
results.push(...batchResults);
|
|
1573
|
+
onBatchComplete?.(results.length, total, batchResults);
|
|
1574
|
+
}
|
|
1575
|
+
return results;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1428
1578
|
// src/server/ai/run.ts
|
|
1429
1579
|
function selectRequests(state, opts) {
|
|
1430
1580
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
@@ -1535,7 +1685,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
1535
1685
|
return { skipped: keys.size };
|
|
1536
1686
|
}
|
|
1537
1687
|
var DEFAULT_LOCALE_CONCURRENCY = 3;
|
|
1538
|
-
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal) {
|
|
1688
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity) {
|
|
1539
1689
|
if (!reqs.length) return [];
|
|
1540
1690
|
const byLocale = /* @__PURE__ */ new Map();
|
|
1541
1691
|
for (const req of reqs) {
|
|
@@ -1546,26 +1696,42 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
1546
1696
|
}
|
|
1547
1697
|
group.push(req);
|
|
1548
1698
|
}
|
|
1699
|
+
const localeBatches = [...byLocale.entries()].map(([locale, group]) => ({
|
|
1700
|
+
locale,
|
|
1701
|
+
batches: chunk(group, Math.max(1, batchSize))
|
|
1702
|
+
}));
|
|
1703
|
+
const jobs = [];
|
|
1704
|
+
const maxBatches = Math.max(...localeBatches.map((g) => g.batches.length));
|
|
1705
|
+
for (let i = 0; i < maxBatches; i++) {
|
|
1706
|
+
for (const g of localeBatches) {
|
|
1707
|
+
if (i < g.batches.length) jobs.push({ locale: g.locale, batch: g.batches[i] });
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
|
|
1711
|
+
const started = /* @__PURE__ */ new Set();
|
|
1549
1712
|
const total = reqs.length;
|
|
1550
1713
|
let done = 0;
|
|
1551
1714
|
const allResults = [];
|
|
1552
|
-
const groups = [...byLocale.values()];
|
|
1553
1715
|
let next = 0;
|
|
1554
1716
|
async function worker() {
|
|
1555
|
-
while (next <
|
|
1717
|
+
while (next < jobs.length) {
|
|
1556
1718
|
if (signal?.aborted) break;
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1719
|
+
const { locale, batch } = jobs[next++];
|
|
1720
|
+
if (!started.has(locale)) {
|
|
1721
|
+
started.add(locale);
|
|
1722
|
+
hooks.onLocaleStart?.(locale);
|
|
1723
|
+
}
|
|
1724
|
+
const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
1725
|
+
done += results.length;
|
|
1726
|
+
hooks.onBatchComplete?.(done, total, results, locale);
|
|
1727
|
+
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
1728
|
+
allResults.push(...batchResults);
|
|
1729
|
+
const left = remaining.get(locale) - 1;
|
|
1730
|
+
remaining.set(locale, left);
|
|
1731
|
+
if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
|
|
1569
1735
|
await Promise.all(workers);
|
|
1570
1736
|
return allResults;
|
|
1571
1737
|
}
|
|
@@ -1653,11 +1819,19 @@ function spellValue(locale, value, ignore) {
|
|
|
1653
1819
|
|
|
1654
1820
|
// src/server/checks.ts
|
|
1655
1821
|
var CHECK_IDS = ["untranslated", "placeholder", "spelling", "length", "glossary"];
|
|
1822
|
+
var CHECK_RULE = {
|
|
1823
|
+
untranslated: "empty-translation",
|
|
1824
|
+
placeholder: "placeholder-mismatch",
|
|
1825
|
+
spelling: "spelling",
|
|
1826
|
+
length: "max-length",
|
|
1827
|
+
glossary: "glossary-violation"
|
|
1828
|
+
};
|
|
1656
1829
|
function contains(haystack, needle, caseSensitive) {
|
|
1657
1830
|
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1658
1831
|
}
|
|
1659
1832
|
function runChecks(state, opts = {}) {
|
|
1660
|
-
const
|
|
1833
|
+
const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
|
|
1834
|
+
const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
|
|
1661
1835
|
const issues = [];
|
|
1662
1836
|
let spellPending = false;
|
|
1663
1837
|
const { sourceLocale } = state.config;
|
|
@@ -1785,7 +1959,11 @@ function runChecks(state, opts = {}) {
|
|
|
1785
1959
|
}
|
|
1786
1960
|
}
|
|
1787
1961
|
}
|
|
1788
|
-
|
|
1962
|
+
const visible = issues.filter((i) => {
|
|
1963
|
+
const entry = state.keys[i.key];
|
|
1964
|
+
return !entry || !findSuppression(entry, sourceLocale, CHECK_RULE[i.check], i.locale);
|
|
1965
|
+
});
|
|
1966
|
+
return { issues: visible, spellPending };
|
|
1789
1967
|
}
|
|
1790
1968
|
|
|
1791
1969
|
// src/server/lint/spelling.ts
|
|
@@ -2027,7 +2205,10 @@ function sortFindings(findings) {
|
|
|
2027
2205
|
}
|
|
2028
2206
|
function countSeverities(findings) {
|
|
2029
2207
|
let error = 0, warn = 0;
|
|
2030
|
-
for (const f of findings)
|
|
2208
|
+
for (const f of findings) {
|
|
2209
|
+
if (f.suppressed) continue;
|
|
2210
|
+
f.severity === "error" ? error++ : warn++;
|
|
2211
|
+
}
|
|
2031
2212
|
return { error, warn };
|
|
2032
2213
|
}
|
|
2033
2214
|
async function loadSpellers(locales, config, load, warn) {
|
|
@@ -2053,7 +2234,7 @@ async function runLint(state, options = {}) {
|
|
|
2053
2234
|
const active = rules.filter(isActive);
|
|
2054
2235
|
const spellingOn = active.some((r) => r.id === "spelling");
|
|
2055
2236
|
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
2056
|
-
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.
|
|
2237
|
+
const allowWords = spellingOn ? buildAllowWords(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
|
|
2057
2238
|
const ctx = {
|
|
2058
2239
|
config,
|
|
2059
2240
|
sourceLocale: state.config.sourceLocale,
|
|
@@ -2065,16 +2246,23 @@ async function runLint(state, options = {}) {
|
|
|
2065
2246
|
const ignoreRes = (config.ignore ?? []).map(globToRegExp2);
|
|
2066
2247
|
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
2067
2248
|
const findings = [];
|
|
2249
|
+
let suppressed = 0;
|
|
2068
2250
|
for (const rule of active) {
|
|
2069
2251
|
const severity = resolveSeverity(rule.id, config);
|
|
2070
2252
|
for (const raw of rule.run(state, ctx)) {
|
|
2071
2253
|
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
2072
2254
|
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
2255
|
+
const entry = state.keys[raw.key];
|
|
2256
|
+
if (raw.locale !== "" && entry && findSuppression(entry, state.config.sourceLocale, rule.id, raw.locale)) {
|
|
2257
|
+
suppressed++;
|
|
2258
|
+
if (options.includeSuppressed) findings.push({ ...raw, severity, suppressed: true });
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2073
2261
|
findings.push({ ...raw, severity });
|
|
2074
2262
|
}
|
|
2075
2263
|
}
|
|
2076
2264
|
const sorted = sortFindings(findings);
|
|
2077
|
-
const counts = countSeverities(sorted);
|
|
2265
|
+
const counts = { ...countSeverities(sorted), suppressed };
|
|
2078
2266
|
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
2079
2267
|
}
|
|
2080
2268
|
|
|
@@ -2952,88 +3140,6 @@ var BATCH_SCHEMA = {
|
|
|
2952
3140
|
additionalProperties: false
|
|
2953
3141
|
};
|
|
2954
3142
|
|
|
2955
|
-
// src/server/ai/batch.ts
|
|
2956
|
-
var MalformedReplyError = class extends Error {
|
|
2957
|
-
constructor(raw) {
|
|
2958
|
-
super("Model reply was not valid translation JSON.");
|
|
2959
|
-
this.raw = raw;
|
|
2960
|
-
this.name = "MalformedReplyError";
|
|
2961
|
-
}
|
|
2962
|
-
raw;
|
|
2963
|
-
};
|
|
2964
|
-
function parseReplyItems(text) {
|
|
2965
|
-
let parsed;
|
|
2966
|
-
try {
|
|
2967
|
-
parsed = JSON.parse(text);
|
|
2968
|
-
} catch {
|
|
2969
|
-
throw new MalformedReplyError(text);
|
|
2970
|
-
}
|
|
2971
|
-
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
2972
|
-
return parsed.items;
|
|
2973
|
-
}
|
|
2974
|
-
function chunk(items, size) {
|
|
2975
|
-
const out = [];
|
|
2976
|
-
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
2977
|
-
return out;
|
|
2978
|
-
}
|
|
2979
|
-
function validateTranslation(req, translation) {
|
|
2980
|
-
if (translation === void 0) return { id: req.id, error: "No translation returned." };
|
|
2981
|
-
if (!placeholdersMatch(req.source, translation)) {
|
|
2982
|
-
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
2983
|
-
}
|
|
2984
|
-
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
2985
|
-
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
2986
|
-
}
|
|
2987
|
-
return { id: req.id, translation };
|
|
2988
|
-
}
|
|
2989
|
-
function validatePlural(req, forms) {
|
|
2990
|
-
if (!forms) return { id: req.id, error: "No translation returned." };
|
|
2991
|
-
const plural = req.plural;
|
|
2992
|
-
if (!plural) return { id: req.id, error: "validatePlural called on a non-plural request." };
|
|
2993
|
-
const cats = plural.categories;
|
|
2994
|
-
const missing = cats.filter((c) => typeof forms[c] !== "string");
|
|
2995
|
-
if (missing.length) return { id: req.id, error: `Missing plural categories: ${missing.join(", ")}.` };
|
|
2996
|
-
const badPh = cats.find((c) => !pluralFormPlaceholdersMatch(c, req.source, forms[c]));
|
|
2997
|
-
if (badPh) return { id: req.id, error: `Placeholder mismatch in plural form "${badPh}".` };
|
|
2998
|
-
if (req.maxLength !== void 0) {
|
|
2999
|
-
const over = cats.find((c) => forms[c].length > req.maxLength);
|
|
3000
|
-
if (over) return { id: req.id, error: `Plural form "${over}" exceeds maxLength (${forms[over].length} > ${req.maxLength}).` };
|
|
3001
|
-
}
|
|
3002
|
-
const out = {};
|
|
3003
|
-
for (const c of cats) out[c] = forms[c];
|
|
3004
|
-
return { id: req.id, forms: out };
|
|
3005
|
-
}
|
|
3006
|
-
function validateReply(req, item) {
|
|
3007
|
-
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
3008
|
-
}
|
|
3009
|
-
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
3010
|
-
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
3011
|
-
async function resolveBatch(batch, isRetry = false) {
|
|
3012
|
-
let reply;
|
|
3013
|
-
try {
|
|
3014
|
-
reply = await callBatch(batch, signal);
|
|
3015
|
-
} catch (err) {
|
|
3016
|
-
if (!(err instanceof MalformedReplyError)) throw err;
|
|
3017
|
-
onMalformedReply?.(err.raw, batch.length);
|
|
3018
|
-
if (signal?.aborted) return failBatch(batch);
|
|
3019
|
-
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
3020
|
-
const mid = Math.ceil(batch.length / 2);
|
|
3021
|
-
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
3022
|
-
}
|
|
3023
|
-
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
3024
|
-
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
3025
|
-
}
|
|
3026
|
-
const results = [];
|
|
3027
|
-
const total = reqs.length;
|
|
3028
|
-
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
3029
|
-
if (signal?.aborted) break;
|
|
3030
|
-
const batchResults = await resolveBatch(batch);
|
|
3031
|
-
results.push(...batchResults);
|
|
3032
|
-
onBatchComplete?.(results.length, total, batchResults);
|
|
3033
|
-
}
|
|
3034
|
-
return results;
|
|
3035
|
-
}
|
|
3036
|
-
|
|
3037
3143
|
// src/server/ai/anthropic.ts
|
|
3038
3144
|
var AnthropicProvider = class {
|
|
3039
3145
|
constructor(config, client) {
|
|
@@ -4108,6 +4214,7 @@ import { homedir } from "os";
|
|
|
4108
4214
|
import { join as join8 } from "path";
|
|
4109
4215
|
var THEMES = ["system", "light", "dark"];
|
|
4110
4216
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
4217
|
+
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
4111
4218
|
var defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
|
|
4112
4219
|
var DEFAULTS = { theme: "system" };
|
|
4113
4220
|
function readJson(path) {
|
|
@@ -4120,7 +4227,10 @@ function readJson(path) {
|
|
|
4120
4227
|
}
|
|
4121
4228
|
function loadUiPrefs(path) {
|
|
4122
4229
|
const raw = readJson(path);
|
|
4123
|
-
|
|
4230
|
+
const prefs = { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
|
|
4231
|
+
if (isPanelWidth(raw.keyColumnWidth)) prefs.keyColumnWidth = Math.round(raw.keyColumnWidth);
|
|
4232
|
+
if (isPanelWidth(raw.detailPanelWidth)) prefs.detailPanelWidth = Math.round(raw.detailPanelWidth);
|
|
4233
|
+
return prefs;
|
|
4124
4234
|
}
|
|
4125
4235
|
function saveUiPrefs(path, prefs) {
|
|
4126
4236
|
const merged = { ...readJson(path), ...prefs };
|
|
@@ -4256,9 +4366,20 @@ function createApi(deps) {
|
|
|
4256
4366
|
app.get("/state", (c) => c.json(load()));
|
|
4257
4367
|
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
4258
4368
|
app.put("/ui-prefs", async (c) => {
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4261
|
-
|
|
4369
|
+
const body = await c.req.json();
|
|
4370
|
+
const patch = {};
|
|
4371
|
+
if ("theme" in body) {
|
|
4372
|
+
if (!isThemeMode(body.theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
|
|
4373
|
+
patch.theme = body.theme;
|
|
4374
|
+
}
|
|
4375
|
+
for (const field of ["keyColumnWidth", "detailPanelWidth"]) {
|
|
4376
|
+
if (field in body) {
|
|
4377
|
+
if (!isPanelWidth(body[field])) return c.json({ error: `${field} must be a number between 120 and 1200` }, 400);
|
|
4378
|
+
patch[field] = Math.round(body[field]);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
if (Object.keys(patch).length === 0) return c.json({ error: "no recognized preferences in body" }, 400);
|
|
4382
|
+
saveUiPrefs(uiPrefsPath, patch);
|
|
4262
4383
|
return c.json({ ok: true });
|
|
4263
4384
|
});
|
|
4264
4385
|
app.get("/local-settings", (c) => c.json(loadLocalSettings(projectRoot)));
|
|
@@ -4732,12 +4853,47 @@ function createApi(deps) {
|
|
|
4732
4853
|
};
|
|
4733
4854
|
app.get("/lint", async (c) => {
|
|
4734
4855
|
const state = load();
|
|
4856
|
+
const includeSuppressed = c.req.query("includeSuppressed") === "1";
|
|
4735
4857
|
const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
|
|
4736
|
-
} });
|
|
4858
|
+
}, includeSuppressed });
|
|
4737
4859
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
|
|
4738
|
-
const counts = countSeverities(findings);
|
|
4860
|
+
const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
|
|
4739
4861
|
return c.json({ findings, counts, ok: counts.error === 0 });
|
|
4740
4862
|
});
|
|
4863
|
+
app.post("/keys/:key/suppressions", async (c) => {
|
|
4864
|
+
const key = c.req.param("key");
|
|
4865
|
+
const { rule, locale } = await c.req.json();
|
|
4866
|
+
if (typeof rule !== "string" || !rule) return c.json({ error: "rule is required" }, 400);
|
|
4867
|
+
if (typeof locale !== "string" || !locale) return c.json({ error: "locale is required" }, 400);
|
|
4868
|
+
const s = load();
|
|
4869
|
+
addSuppression(s, key, rule, locale);
|
|
4870
|
+
persist(s);
|
|
4871
|
+
logChange({ kind: "suppression", summary: `Suppressed ${rule} for ${key} [${locale}]`, key, locale, after: rule });
|
|
4872
|
+
return c.json({ ok: true });
|
|
4873
|
+
});
|
|
4874
|
+
app.delete("/keys/:key/suppressions", (c) => {
|
|
4875
|
+
const key = c.req.param("key");
|
|
4876
|
+
const rule = c.req.query("rule") ?? "";
|
|
4877
|
+
const locale = c.req.query("locale") ?? "";
|
|
4878
|
+
if (!rule || !locale) return c.json({ error: "rule and locale are required" }, 400);
|
|
4879
|
+
const s = load();
|
|
4880
|
+
removeSuppression(s, key, rule, locale);
|
|
4881
|
+
persist(s);
|
|
4882
|
+
logChange({ kind: "suppression", summary: `Unsuppressed ${rule} for ${key} [${locale}]`, key, locale, before: rule });
|
|
4883
|
+
return c.json({ ok: true });
|
|
4884
|
+
});
|
|
4885
|
+
app.post("/lint/accept", async (c) => {
|
|
4886
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4887
|
+
const s = load();
|
|
4888
|
+
const lint = await runLint(s, { loadSpeller: cachedLoader, warn: () => {
|
|
4889
|
+
} });
|
|
4890
|
+
const result = acceptFindings(s, lint.findings, { rules: body.rules, locales: body.locales });
|
|
4891
|
+
if (result.accepted > 0) {
|
|
4892
|
+
persist(s);
|
|
4893
|
+
logChange({ kind: "suppression", summary: `Suppressed ${result.accepted} finding(s)`, after: result.byRule });
|
|
4894
|
+
}
|
|
4895
|
+
return c.json({ ok: true, ...result });
|
|
4896
|
+
});
|
|
4741
4897
|
app.get("/checks", (c) => {
|
|
4742
4898
|
const param = c.req.query("checks");
|
|
4743
4899
|
const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
|
|
@@ -4862,7 +5018,7 @@ function createApi(deps) {
|
|
|
4862
5018
|
raw
|
|
4863
5019
|
});
|
|
4864
5020
|
}
|
|
4865
|
-
}, aiCfg.concurrency, signal);
|
|
5021
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
4866
5022
|
if (!signal?.aborted) {
|
|
4867
5023
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
4868
5024
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -4905,7 +5061,7 @@ function createApi(deps) {
|
|
|
4905
5061
|
raw
|
|
4906
5062
|
});
|
|
4907
5063
|
}
|
|
4908
|
-
}, aiCfg.concurrency);
|
|
5064
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
4909
5065
|
const latest = load();
|
|
4910
5066
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
4911
5067
|
const entry = {
|