glotfile 0.5.3 → 0.5.4
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 +143 -184
- package/dist/server/server.js +364 -425
- package/dist/ui/assets/{index-BjN06DHD.js → index-BGtqXjLQ.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
package/dist/server/server.js
CHANGED
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res) => function __init() {
|
|
4
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
-
};
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// node_modules/dictionary-en/index.js
|
|
12
|
-
var dictionary_en_exports = {};
|
|
13
|
-
__export(dictionary_en_exports, {
|
|
14
|
-
default: () => dictionary_en_default
|
|
15
|
-
});
|
|
16
|
-
import fs from "fs/promises";
|
|
17
|
-
var aff, dic, dictionary, dictionary_en_default;
|
|
18
|
-
var init_dictionary_en = __esm({
|
|
19
|
-
async "node_modules/dictionary-en/index.js"() {
|
|
20
|
-
"use strict";
|
|
21
|
-
aff = await fs.readFile(new URL("index.aff", import.meta.url));
|
|
22
|
-
dic = await fs.readFile(new URL("index.dic", import.meta.url));
|
|
23
|
-
dictionary = { aff, dic };
|
|
24
|
-
dictionary_en_default = dictionary;
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
1
|
// src/server/server.ts
|
|
29
2
|
import { Hono as Hono2 } from "hono";
|
|
30
3
|
import { serve } from "@hono/node-server";
|
|
@@ -865,7 +838,7 @@ function findMissing(state) {
|
|
|
865
838
|
const entry = state.keys[key];
|
|
866
839
|
if (entry.skipTranslate) continue;
|
|
867
840
|
for (const locale of targets) {
|
|
868
|
-
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
|
|
841
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value?.trim();
|
|
869
842
|
if (!v) out.push({ key, locale });
|
|
870
843
|
}
|
|
871
844
|
}
|
|
@@ -1487,162 +1460,14 @@ function pluralFormPlaceholdersMatch(category, source, form) {
|
|
|
1487
1460
|
return COUNT_OPTIONAL.has(category) ? placeholdersSubset(source, form) : placeholdersMatch(source, form);
|
|
1488
1461
|
}
|
|
1489
1462
|
|
|
1490
|
-
// src/server/
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
// src/server/glob.ts
|
|
1495
|
-
function globToRegExp2(glob) {
|
|
1496
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1497
|
-
return new RegExp(`^${escaped}$`);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
// src/server/ai/batch.ts
|
|
1501
|
-
var MalformedReplyError = class extends Error {
|
|
1502
|
-
constructor(raw) {
|
|
1503
|
-
super("Model reply was not valid translation JSON.");
|
|
1504
|
-
this.raw = raw;
|
|
1505
|
-
this.name = "MalformedReplyError";
|
|
1506
|
-
}
|
|
1507
|
-
raw;
|
|
1508
|
-
};
|
|
1509
|
-
function parseReplyItems(text) {
|
|
1510
|
-
let parsed;
|
|
1511
|
-
try {
|
|
1512
|
-
parsed = JSON.parse(text);
|
|
1513
|
-
} catch {
|
|
1514
|
-
throw new MalformedReplyError(text);
|
|
1515
|
-
}
|
|
1516
|
-
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
1517
|
-
return parsed.items;
|
|
1518
|
-
}
|
|
1519
|
-
function chunk(items, size) {
|
|
1520
|
-
const out = [];
|
|
1521
|
-
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
1522
|
-
return out;
|
|
1523
|
-
}
|
|
1524
|
-
function validateTranslation(req, translation) {
|
|
1525
|
-
if (translation === void 0) return { id: req.id, error: "No translation returned." };
|
|
1526
|
-
if (!placeholdersMatch(req.source, translation)) {
|
|
1527
|
-
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
1528
|
-
}
|
|
1529
|
-
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
1530
|
-
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
1531
|
-
}
|
|
1532
|
-
return { id: req.id, translation };
|
|
1533
|
-
}
|
|
1534
|
-
function validatePlural(req, forms) {
|
|
1535
|
-
if (!forms) return { id: req.id, error: "No translation returned." };
|
|
1536
|
-
const plural = req.plural;
|
|
1537
|
-
if (!plural) return { id: req.id, error: "validatePlural called on a non-plural request." };
|
|
1538
|
-
const cats = plural.categories;
|
|
1539
|
-
const missing = cats.filter((c) => typeof forms[c] !== "string");
|
|
1540
|
-
if (missing.length) return { id: req.id, error: `Missing plural categories: ${missing.join(", ")}.` };
|
|
1541
|
-
const badPh = cats.find((c) => !pluralFormPlaceholdersMatch(c, req.source, forms[c]));
|
|
1542
|
-
if (badPh) return { id: req.id, error: `Placeholder mismatch in plural form "${badPh}".` };
|
|
1543
|
-
if (req.maxLength !== void 0) {
|
|
1544
|
-
const over = cats.find((c) => forms[c].length > req.maxLength);
|
|
1545
|
-
if (over) return { id: req.id, error: `Plural form "${over}" exceeds maxLength (${forms[over].length} > ${req.maxLength}).` };
|
|
1546
|
-
}
|
|
1547
|
-
const out = {};
|
|
1548
|
-
for (const c of cats) out[c] = forms[c];
|
|
1549
|
-
return { id: req.id, forms: out };
|
|
1550
|
-
}
|
|
1551
|
-
function validateReply(req, item) {
|
|
1552
|
-
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
1553
|
-
}
|
|
1554
|
-
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
1555
|
-
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
1556
|
-
async function resolveBatch(batch, isRetry = false) {
|
|
1557
|
-
let reply;
|
|
1558
|
-
try {
|
|
1559
|
-
reply = await callBatch(batch, signal);
|
|
1560
|
-
} catch (err) {
|
|
1561
|
-
if (!(err instanceof MalformedReplyError)) throw err;
|
|
1562
|
-
onMalformedReply?.(err.raw, batch.length);
|
|
1563
|
-
if (signal?.aborted) return failBatch(batch);
|
|
1564
|
-
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
1565
|
-
const mid = Math.ceil(batch.length / 2);
|
|
1566
|
-
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
1567
|
-
}
|
|
1568
|
-
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
1569
|
-
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
1570
|
-
}
|
|
1571
|
-
const results = [];
|
|
1572
|
-
const total = reqs.length;
|
|
1573
|
-
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
1574
|
-
if (signal?.aborted) break;
|
|
1575
|
-
const batchResults = await resolveBatch(batch);
|
|
1576
|
-
results.push(...batchResults);
|
|
1577
|
-
onBatchComplete?.(results.length, total, batchResults);
|
|
1578
|
-
}
|
|
1579
|
-
return results;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// src/server/ai/run.ts
|
|
1583
|
-
function selectRequests(state, opts) {
|
|
1584
|
-
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
1585
|
-
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
1586
|
-
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
1587
|
-
const reqs = [];
|
|
1588
|
-
let id = 0;
|
|
1589
|
-
for (const key of Object.keys(state.keys).sort()) {
|
|
1590
|
-
const entry = state.keys[key];
|
|
1591
|
-
if (entry.skipTranslate) continue;
|
|
1592
|
-
if (keyRe && !keyRe.test(key)) continue;
|
|
1593
|
-
if (keySet && !keySet.has(key)) continue;
|
|
1594
|
-
const sourceLv = entry.values[state.config.sourceLocale];
|
|
1595
|
-
if (entry.plural) {
|
|
1596
|
-
const sourceForms = sourceLv?.forms;
|
|
1597
|
-
const other = sourceForms?.other;
|
|
1598
|
-
if (!sourceForms || !other) continue;
|
|
1599
|
-
for (const locale of targets) {
|
|
1600
|
-
const have = entry.values[locale]?.forms ?? {};
|
|
1601
|
-
const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
|
|
1602
|
-
if (opts.onlyMissing && complete) continue;
|
|
1603
|
-
const glossary = relevantGlossary(other, locale, state.glossary);
|
|
1604
|
-
reqs.push({
|
|
1605
|
-
id: String(id++),
|
|
1606
|
-
key,
|
|
1607
|
-
source: other,
|
|
1608
|
-
sourceLocale: state.config.sourceLocale,
|
|
1609
|
-
context: entry.context,
|
|
1610
|
-
targetLocale: locale,
|
|
1611
|
-
maxLength: entry.maxLength,
|
|
1612
|
-
placeholders: extractPlaceholders(other),
|
|
1613
|
-
...glossary.length ? { glossary } : {},
|
|
1614
|
-
plural: { arg: entry.plural.arg, categories: categoriesFor(locale), sourceForms }
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
continue;
|
|
1618
|
-
}
|
|
1619
|
-
const source = sourceLv?.value;
|
|
1620
|
-
if (!source) continue;
|
|
1621
|
-
for (const locale of targets) {
|
|
1622
|
-
const existing = entry.values[locale]?.value;
|
|
1623
|
-
if (opts.onlyMissing && existing) continue;
|
|
1624
|
-
const glossary = relevantGlossary(source, locale, state.glossary);
|
|
1625
|
-
reqs.push({
|
|
1626
|
-
id: String(id++),
|
|
1627
|
-
key,
|
|
1628
|
-
source,
|
|
1629
|
-
sourceLocale: state.config.sourceLocale,
|
|
1630
|
-
context: entry.context,
|
|
1631
|
-
targetLocale: locale,
|
|
1632
|
-
maxLength: entry.maxLength,
|
|
1633
|
-
placeholders: extractPlaceholders(source),
|
|
1634
|
-
...glossary.length ? { glossary } : {}
|
|
1635
|
-
});
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
return reqs;
|
|
1463
|
+
// src/server/glossary.ts
|
|
1464
|
+
function contains(haystack, needle, caseSensitive) {
|
|
1465
|
+
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1639
1466
|
}
|
|
1640
1467
|
function relevantGlossary(source, targetLocale, glossary) {
|
|
1641
1468
|
const hints = [];
|
|
1642
1469
|
for (const entry of glossary) {
|
|
1643
|
-
|
|
1644
|
-
const needle = entry.caseSensitive ? entry.term : entry.term.toLowerCase();
|
|
1645
|
-
if (!haystack.includes(needle)) continue;
|
|
1470
|
+
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1646
1471
|
hints.push({
|
|
1647
1472
|
term: entry.term,
|
|
1648
1473
|
doNotTranslate: entry.doNotTranslate,
|
|
@@ -1652,170 +1477,84 @@ function relevantGlossary(source, targetLocale, glossary) {
|
|
|
1652
1477
|
}
|
|
1653
1478
|
return hints;
|
|
1654
1479
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
};
|
|
1662
|
-
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
1663
|
-
function attachScreenshots(reqs, state, projectRoot) {
|
|
1664
|
-
const cache2 = /* @__PURE__ */ new Map();
|
|
1665
|
-
for (const req of reqs) {
|
|
1666
|
-
const screenshot = state.keys[req.key]?.screenshot;
|
|
1667
|
-
if (!screenshot) continue;
|
|
1668
|
-
const mediaType = MEDIA_TYPES[extname2(screenshot).toLowerCase()];
|
|
1669
|
-
if (!mediaType) continue;
|
|
1670
|
-
if (!cache2.has(screenshot)) {
|
|
1671
|
-
const abs = resolve4(projectRoot, screenshot);
|
|
1672
|
-
if (!existsSync6(abs)) {
|
|
1673
|
-
cache2.set(screenshot, null);
|
|
1674
|
-
} else {
|
|
1675
|
-
const buf = readFileSync6(abs);
|
|
1676
|
-
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
1480
|
+
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
1481
|
+
const out = [];
|
|
1482
|
+
for (const entry of glossary) {
|
|
1483
|
+
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1484
|
+
if (entry.doNotTranslate) {
|
|
1485
|
+
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
1486
|
+
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
1677
1487
|
}
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
const forced = entry.translations?.[targetLocale];
|
|
1491
|
+
if (forced && !contains(value, forced, entry.caseSensitive)) {
|
|
1492
|
+
out.push({ term: entry.term, expected: forced, kind: "forced" });
|
|
1678
1493
|
}
|
|
1679
|
-
const image = cache2.get(screenshot);
|
|
1680
|
-
if (image) req.image = image;
|
|
1681
1494
|
}
|
|
1495
|
+
return out;
|
|
1682
1496
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1497
|
+
|
|
1498
|
+
// src/server/spell.ts
|
|
1499
|
+
var instances = /* @__PURE__ */ new Map();
|
|
1500
|
+
var loading = /* @__PURE__ */ new Set();
|
|
1501
|
+
var unavailable = /* @__PURE__ */ new Set();
|
|
1502
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1503
|
+
var norm = (dictId) => dictId.toLowerCase();
|
|
1504
|
+
var ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
1505
|
+
var MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
1506
|
+
var WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
1507
|
+
function spellTokens(value) {
|
|
1508
|
+
return value.replace(ICU_BLOCK, " ").replace(MASK, " ").match(WORD) ?? [];
|
|
1509
|
+
}
|
|
1510
|
+
function ignoreWordsFor(glossary, customWords = []) {
|
|
1511
|
+
const set = /* @__PURE__ */ new Set();
|
|
1512
|
+
const add = (text) => {
|
|
1513
|
+
for (const w of text.match(WORD) ?? []) set.add(w.toLowerCase());
|
|
1514
|
+
};
|
|
1515
|
+
for (const e of glossary) {
|
|
1516
|
+
add(e.term);
|
|
1517
|
+
for (const t of Object.values(e.translations ?? {})) add(t);
|
|
1687
1518
|
}
|
|
1688
|
-
const
|
|
1689
|
-
return
|
|
1519
|
+
for (const w of customWords) add(w);
|
|
1520
|
+
return set;
|
|
1690
1521
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1522
|
+
async function getSpeller(dictId) {
|
|
1523
|
+
const key = norm(dictId);
|
|
1524
|
+
const existing = instances.get(key);
|
|
1525
|
+
if (existing) return existing;
|
|
1526
|
+
if (unavailable.has(key)) return null;
|
|
1527
|
+
try {
|
|
1528
|
+
const nspellMod = await import("nspell");
|
|
1529
|
+
const nspell = nspellMod.default ?? nspellMod;
|
|
1530
|
+
const dictMod = await import(`dictionary-${key}`);
|
|
1531
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
1532
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
1533
|
+
const speller = nspell(dict);
|
|
1534
|
+
instances.set(key, speller);
|
|
1535
|
+
return speller;
|
|
1536
|
+
} catch {
|
|
1537
|
+
unavailable.add(key);
|
|
1538
|
+
return null;
|
|
1539
|
+
} finally {
|
|
1540
|
+
loading.delete(key);
|
|
1702
1541
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1542
|
+
}
|
|
1543
|
+
function spellValue(dictId, value, ignore) {
|
|
1544
|
+
const key = norm(dictId);
|
|
1545
|
+
if (unavailable.has(key)) return [];
|
|
1546
|
+
const spell = instances.get(key);
|
|
1547
|
+
if (!spell) {
|
|
1548
|
+
if (!loading.has(key)) {
|
|
1549
|
+
loading.add(key);
|
|
1550
|
+
void getSpeller(key);
|
|
1712
1551
|
}
|
|
1713
|
-
|
|
1714
|
-
const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
|
|
1715
|
-
const started = /* @__PURE__ */ new Set();
|
|
1716
|
-
const total = reqs.length;
|
|
1717
|
-
let done = 0;
|
|
1718
|
-
const allResults = [];
|
|
1719
|
-
let next = 0;
|
|
1720
|
-
async function worker() {
|
|
1721
|
-
while (next < jobs.length) {
|
|
1722
|
-
if (signal?.aborted) break;
|
|
1723
|
-
const { locale, batch } = jobs[next++];
|
|
1724
|
-
if (!started.has(locale)) {
|
|
1725
|
-
started.add(locale);
|
|
1726
|
-
hooks.onLocaleStart?.(locale);
|
|
1727
|
-
}
|
|
1728
|
-
const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
1729
|
-
done += results.length;
|
|
1730
|
-
hooks.onBatchComplete?.(done, total, results, locale);
|
|
1731
|
-
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
1732
|
-
allResults.push(...batchResults);
|
|
1733
|
-
const left = remaining.get(locale) - 1;
|
|
1734
|
-
remaining.set(locale, left);
|
|
1735
|
-
if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
|
|
1739
|
-
await Promise.all(workers);
|
|
1740
|
-
return allResults;
|
|
1741
|
-
}
|
|
1742
|
-
function applyResults(state, reqs, results, clock = systemClock, force = false) {
|
|
1743
|
-
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
1744
|
-
let written = 0;
|
|
1745
|
-
const errors = [];
|
|
1746
|
-
for (const res of results) {
|
|
1747
|
-
const req = byId.get(res.id);
|
|
1748
|
-
if (!req) continue;
|
|
1749
|
-
if (req.plural) {
|
|
1750
|
-
if (res.error || res.forms === void 0) {
|
|
1751
|
-
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
1752
|
-
continue;
|
|
1753
|
-
}
|
|
1754
|
-
if (applyMachineTranslationForms(state, req.key, req.targetLocale, res.forms, clock, force)) written++;
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
if (res.translation === void 0) {
|
|
1758
|
-
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
1759
|
-
continue;
|
|
1760
|
-
}
|
|
1761
|
-
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
1762
|
-
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
1763
|
-
}
|
|
1764
|
-
return { written, errors };
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// src/server/spell.ts
|
|
1768
|
-
import nspell from "nspell";
|
|
1769
|
-
var LOADERS = {
|
|
1770
|
-
en: () => init_dictionary_en().then(() => dictionary_en_exports),
|
|
1771
|
-
es: () => import("dictionary-es"),
|
|
1772
|
-
fr: () => import("dictionary-fr")
|
|
1773
|
-
};
|
|
1774
|
-
var instances = /* @__PURE__ */ new Map();
|
|
1775
|
-
var loading = /* @__PURE__ */ new Set();
|
|
1776
|
-
var unavailable = /* @__PURE__ */ new Set();
|
|
1777
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1778
|
-
var norm = (locale) => locale.toLowerCase();
|
|
1779
|
-
var ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
1780
|
-
var MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
1781
|
-
var WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
1782
|
-
async function loadDictionary(locale) {
|
|
1783
|
-
const key = norm(locale);
|
|
1784
|
-
if (instances.has(key) || unavailable.has(key)) return;
|
|
1785
|
-
const loader = LOADERS[key];
|
|
1786
|
-
if (!loader) {
|
|
1787
|
-
unavailable.add(key);
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
try {
|
|
1791
|
-
const { default: dict } = await loader();
|
|
1792
|
-
instances.set(key, nspell(dict));
|
|
1793
|
-
} catch {
|
|
1794
|
-
unavailable.add(key);
|
|
1795
|
-
} finally {
|
|
1796
|
-
loading.delete(key);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
function spellValue(locale, value, ignore) {
|
|
1800
|
-
const key = norm(locale);
|
|
1801
|
-
if (unavailable.has(key)) return [];
|
|
1802
|
-
const spell = instances.get(key);
|
|
1803
|
-
if (!spell) {
|
|
1804
|
-
if (!LOADERS[key]) {
|
|
1805
|
-
unavailable.add(key);
|
|
1806
|
-
return [];
|
|
1807
|
-
}
|
|
1808
|
-
if (!loading.has(key)) {
|
|
1809
|
-
loading.add(key);
|
|
1810
|
-
void loadDictionary(key);
|
|
1811
|
-
}
|
|
1812
|
-
return null;
|
|
1552
|
+
return null;
|
|
1813
1553
|
}
|
|
1814
1554
|
const cacheKey = key + " " + value;
|
|
1815
1555
|
let allBad = cache.get(cacheKey);
|
|
1816
1556
|
if (!allBad) {
|
|
1817
|
-
|
|
1818
|
-
allBad = words.filter((w) => !spell.correct(w));
|
|
1557
|
+
allBad = spellTokens(value).filter((w) => !spell.correct(w));
|
|
1819
1558
|
cache.set(cacheKey, allBad);
|
|
1820
1559
|
}
|
|
1821
1560
|
return allBad.filter((w) => !ignore.has(w.toLowerCase()));
|
|
@@ -1830,39 +1569,21 @@ var CHECK_RULE = {
|
|
|
1830
1569
|
length: "max-length",
|
|
1831
1570
|
glossary: "glossary-violation"
|
|
1832
1571
|
};
|
|
1833
|
-
function contains(haystack, needle, caseSensitive) {
|
|
1834
|
-
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1835
|
-
}
|
|
1836
1572
|
function runChecks(state, opts = {}) {
|
|
1837
1573
|
const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
|
|
1838
1574
|
const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
|
|
1839
1575
|
const issues = [];
|
|
1840
1576
|
let spellPending = false;
|
|
1841
1577
|
const { sourceLocale } = state.config;
|
|
1842
|
-
const
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
for (const t of Object.values(e.translations ?? {})) {
|
|
1847
|
-
for (const w of t.toLowerCase().split(/\s+/)) if (w) ignore.add(w);
|
|
1578
|
+
const ignore = ignoreWordsFor(state.glossary, state.config.spelling?.customWords);
|
|
1579
|
+
if (on("untranslated")) {
|
|
1580
|
+
for (const m of findMissing(state)) {
|
|
1581
|
+
issues.push({ key: m.key, locale: m.locale, check: "untranslated", message: "Not translated yet" });
|
|
1848
1582
|
}
|
|
1849
1583
|
}
|
|
1850
|
-
for (const word of state.config.spelling?.customWords ?? []) {
|
|
1851
|
-
const w = word.trim().toLowerCase();
|
|
1852
|
-
if (w) ignore.add(w);
|
|
1853
|
-
}
|
|
1854
|
-
const targetLocales = state.config.locales.filter((l) => l !== sourceLocale);
|
|
1855
1584
|
for (const key of Object.keys(state.keys).sort()) {
|
|
1856
1585
|
const entry = state.keys[key];
|
|
1857
1586
|
const source = entry.values[sourceLocale]?.value ?? "";
|
|
1858
|
-
if (on("untranslated") && !entry.skipTranslate) {
|
|
1859
|
-
for (const locale of targetLocales) {
|
|
1860
|
-
const translated = entry.plural ? (entry.values[locale]?.forms?.other ?? "").trim() !== "" : (entry.values[locale]?.value ?? "").trim() !== "";
|
|
1861
|
-
if (!translated) {
|
|
1862
|
-
issues.push({ key, locale, check: "untranslated", message: "Not translated yet" });
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
1587
|
if (entry.plural) {
|
|
1867
1588
|
if (on("placeholder")) {
|
|
1868
1589
|
const sourceForm = entry.values[sourceLocale]?.forms?.other ?? "";
|
|
@@ -1906,7 +1627,8 @@ function runChecks(state, opts = {}) {
|
|
|
1906
1627
|
});
|
|
1907
1628
|
}
|
|
1908
1629
|
if (on("spelling") && !blank) {
|
|
1909
|
-
const
|
|
1630
|
+
const dictId = state.config.lint?.spelling?.locales?.[locale] ?? locale;
|
|
1631
|
+
const bad = spellValue(dictId, value, ignore);
|
|
1910
1632
|
if (bad === null) spellPending = true;
|
|
1911
1633
|
else if (bad.length) {
|
|
1912
1634
|
issues.push({
|
|
@@ -1936,29 +1658,14 @@ function runChecks(state, opts = {}) {
|
|
|
1936
1658
|
});
|
|
1937
1659
|
}
|
|
1938
1660
|
if (on("glossary") && source) {
|
|
1939
|
-
for (const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
message: `Do-not-translate term "${hint.term}" is missing from the translation`,
|
|
1948
|
-
detail: [hint.term]
|
|
1949
|
-
});
|
|
1950
|
-
}
|
|
1951
|
-
} else if (hint.forced) {
|
|
1952
|
-
if (!contains(value, hint.forced, cs)) {
|
|
1953
|
-
issues.push({
|
|
1954
|
-
key,
|
|
1955
|
-
locale,
|
|
1956
|
-
check: "glossary",
|
|
1957
|
-
message: `Should use "${hint.forced}" for "${hint.term}"`,
|
|
1958
|
-
detail: [hint.forced]
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1661
|
+
for (const viol of glossaryViolations(source, value, locale, state.glossary)) {
|
|
1662
|
+
issues.push({
|
|
1663
|
+
key,
|
|
1664
|
+
locale,
|
|
1665
|
+
check: "glossary",
|
|
1666
|
+
message: viol.kind === "do-not-translate" ? `Do-not-translate term "${viol.term}" is missing from the translation` : `Should use "${viol.expected}" for "${viol.term}"`,
|
|
1667
|
+
detail: [viol.expected]
|
|
1668
|
+
});
|
|
1962
1669
|
}
|
|
1963
1670
|
}
|
|
1964
1671
|
}
|
|
@@ -1970,19 +1677,13 @@ function runChecks(state, opts = {}) {
|
|
|
1970
1677
|
return { issues: visible, spellPending };
|
|
1971
1678
|
}
|
|
1972
1679
|
|
|
1973
|
-
// src/server/
|
|
1974
|
-
function
|
|
1975
|
-
|
|
1976
|
-
}
|
|
1977
|
-
function buildAllowWords(glossary, dictionary2 = []) {
|
|
1978
|
-
const set = /* @__PURE__ */ new Set();
|
|
1979
|
-
const add = (s) => {
|
|
1980
|
-
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
1981
|
-
};
|
|
1982
|
-
for (const g of glossary) add(g.term);
|
|
1983
|
-
for (const w of dictionary2) add(w);
|
|
1984
|
-
return set;
|
|
1680
|
+
// src/server/glob.ts
|
|
1681
|
+
function globToRegExp2(glob) {
|
|
1682
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1683
|
+
return new RegExp(`^${escaped}$`);
|
|
1985
1684
|
}
|
|
1685
|
+
|
|
1686
|
+
// src/server/lint/spelling.ts
|
|
1986
1687
|
var spellingRule = {
|
|
1987
1688
|
id: "spelling",
|
|
1988
1689
|
run(state, ctx) {
|
|
@@ -1994,10 +1695,8 @@ var spellingRule = {
|
|
|
1994
1695
|
if (!speller) continue;
|
|
1995
1696
|
const value = entry.values[locale]?.value;
|
|
1996
1697
|
if (!value) continue;
|
|
1997
|
-
const
|
|
1998
|
-
|
|
1999
|
-
const lower = word.toLowerCase();
|
|
2000
|
-
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
1698
|
+
for (const word of spellTokens(value)) {
|
|
1699
|
+
if (ctx.allowWords.has(word.toLowerCase())) continue;
|
|
2001
1700
|
if (!speller.correct(word)) {
|
|
2002
1701
|
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
2003
1702
|
}
|
|
@@ -2007,18 +1706,7 @@ var spellingRule = {
|
|
|
2007
1706
|
return out;
|
|
2008
1707
|
}
|
|
2009
1708
|
};
|
|
2010
|
-
var defaultLoader =
|
|
2011
|
-
try {
|
|
2012
|
-
const nspellMod = await import("nspell");
|
|
2013
|
-
const nspell2 = nspellMod.default ?? nspellMod;
|
|
2014
|
-
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
2015
|
-
const dictExport = dictMod.default ?? dictMod;
|
|
2016
|
-
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
2017
|
-
return nspell2(dict);
|
|
2018
|
-
} catch {
|
|
2019
|
-
return null;
|
|
2020
|
-
}
|
|
2021
|
-
};
|
|
1709
|
+
var defaultLoader = (dictId) => getSpeller(dictId);
|
|
2022
1710
|
|
|
2023
1711
|
// src/server/lint/rules.ts
|
|
2024
1712
|
var emptySourceRule = {
|
|
@@ -2035,17 +1723,14 @@ var emptySourceRule = {
|
|
|
2035
1723
|
};
|
|
2036
1724
|
var emptyTranslationRule = {
|
|
2037
1725
|
id: "empty-translation",
|
|
2038
|
-
|
|
1726
|
+
// findMissing is the shared "untranslated" walk (also behind the editor's
|
|
1727
|
+
// untranslated check and /scan/missing); a whitespace-only value counts as
|
|
1728
|
+
// missing there, so no separate whitespace pass is needed.
|
|
1729
|
+
run(state) {
|
|
2039
1730
|
const out = [];
|
|
2040
1731
|
for (const m of findMissing(state)) {
|
|
2041
1732
|
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
2042
1733
|
}
|
|
2043
|
-
for (const key of Object.keys(state.keys)) {
|
|
2044
|
-
for (const locale of ctx.targetLocales) {
|
|
2045
|
-
const v = state.keys[key].values[locale]?.value;
|
|
2046
|
-
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
1734
|
return out;
|
|
2050
1735
|
}
|
|
2051
1736
|
};
|
|
@@ -2173,13 +1858,13 @@ var glossaryViolationRule = {
|
|
|
2173
1858
|
for (const locale of ctx.targetLocales) {
|
|
2174
1859
|
const v = entry.values[locale]?.value;
|
|
2175
1860
|
if (!v) continue;
|
|
2176
|
-
for (const
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
}
|
|
1861
|
+
for (const viol of glossaryViolations(src, v, locale, ctx.glossary)) {
|
|
1862
|
+
out.push({
|
|
1863
|
+
ruleId: "glossary-violation",
|
|
1864
|
+
key,
|
|
1865
|
+
locale,
|
|
1866
|
+
message: viol.kind === "do-not-translate" ? `do-not-translate term "${viol.term}" is missing or altered` : `expected glossary translation "${viol.expected}" for "${viol.term}"`
|
|
1867
|
+
});
|
|
2183
1868
|
}
|
|
2184
1869
|
}
|
|
2185
1870
|
}
|
|
@@ -2238,7 +1923,7 @@ async function runLint(state, options = {}) {
|
|
|
2238
1923
|
const active = rules.filter(isActive);
|
|
2239
1924
|
const spellingOn = active.some((r) => r.id === "spelling");
|
|
2240
1925
|
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
2241
|
-
const allowWords = spellingOn ?
|
|
1926
|
+
const allowWords = spellingOn ? ignoreWordsFor(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
|
|
2242
1927
|
const ctx = {
|
|
2243
1928
|
config,
|
|
2244
1929
|
sourceLocale: state.config.sourceLocale,
|
|
@@ -2271,8 +1956,8 @@ async function runLint(state, options = {}) {
|
|
|
2271
1956
|
}
|
|
2272
1957
|
|
|
2273
1958
|
// src/server/lint/outputs.ts
|
|
2274
|
-
import { readFileSync as
|
|
2275
|
-
import { resolve as
|
|
1959
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
1960
|
+
import { resolve as resolve4 } from "path";
|
|
2276
1961
|
|
|
2277
1962
|
// src/server/adapters/options.ts
|
|
2278
1963
|
function applyCase(canonical, style) {
|
|
@@ -3072,8 +2757,8 @@ function checkOutputs(state, root) {
|
|
|
3072
2757
|
for (const output of state.config.outputs) {
|
|
3073
2758
|
const result = getAdapter(output.adapter).export(state, output);
|
|
3074
2759
|
for (const file of result.files) {
|
|
3075
|
-
const abs =
|
|
3076
|
-
const current =
|
|
2760
|
+
const abs = resolve4(root, file.path);
|
|
2761
|
+
const current = existsSync6(abs) ? readFileSync6(abs, "utf8") : null;
|
|
3077
2762
|
if (current === null) {
|
|
3078
2763
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
3079
2764
|
} else if (current !== file.contents) {
|
|
@@ -3187,6 +2872,88 @@ var BATCH_SCHEMA = {
|
|
|
3187
2872
|
additionalProperties: false
|
|
3188
2873
|
};
|
|
3189
2874
|
|
|
2875
|
+
// src/server/ai/batch.ts
|
|
2876
|
+
var MalformedReplyError = class extends Error {
|
|
2877
|
+
constructor(raw) {
|
|
2878
|
+
super("Model reply was not valid translation JSON.");
|
|
2879
|
+
this.raw = raw;
|
|
2880
|
+
this.name = "MalformedReplyError";
|
|
2881
|
+
}
|
|
2882
|
+
raw;
|
|
2883
|
+
};
|
|
2884
|
+
function parseReplyItems(text) {
|
|
2885
|
+
let parsed;
|
|
2886
|
+
try {
|
|
2887
|
+
parsed = JSON.parse(text);
|
|
2888
|
+
} catch {
|
|
2889
|
+
throw new MalformedReplyError(text);
|
|
2890
|
+
}
|
|
2891
|
+
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
2892
|
+
return parsed.items;
|
|
2893
|
+
}
|
|
2894
|
+
function chunk(items, size) {
|
|
2895
|
+
const out = [];
|
|
2896
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
2897
|
+
return out;
|
|
2898
|
+
}
|
|
2899
|
+
function validateTranslation(req, translation) {
|
|
2900
|
+
if (translation === void 0) return { id: req.id, error: "No translation returned." };
|
|
2901
|
+
if (!placeholdersMatch(req.source, translation)) {
|
|
2902
|
+
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
2903
|
+
}
|
|
2904
|
+
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
2905
|
+
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
2906
|
+
}
|
|
2907
|
+
return { id: req.id, translation };
|
|
2908
|
+
}
|
|
2909
|
+
function validatePlural(req, forms) {
|
|
2910
|
+
if (!forms) return { id: req.id, error: "No translation returned." };
|
|
2911
|
+
const plural = req.plural;
|
|
2912
|
+
if (!plural) return { id: req.id, error: "validatePlural called on a non-plural request." };
|
|
2913
|
+
const cats = plural.categories;
|
|
2914
|
+
const missing = cats.filter((c) => typeof forms[c] !== "string");
|
|
2915
|
+
if (missing.length) return { id: req.id, error: `Missing plural categories: ${missing.join(", ")}.` };
|
|
2916
|
+
const badPh = cats.find((c) => !pluralFormPlaceholdersMatch(c, req.source, forms[c]));
|
|
2917
|
+
if (badPh) return { id: req.id, error: `Placeholder mismatch in plural form "${badPh}".` };
|
|
2918
|
+
if (req.maxLength !== void 0) {
|
|
2919
|
+
const over = cats.find((c) => forms[c].length > req.maxLength);
|
|
2920
|
+
if (over) return { id: req.id, error: `Plural form "${over}" exceeds maxLength (${forms[over].length} > ${req.maxLength}).` };
|
|
2921
|
+
}
|
|
2922
|
+
const out = {};
|
|
2923
|
+
for (const c of cats) out[c] = forms[c];
|
|
2924
|
+
return { id: req.id, forms: out };
|
|
2925
|
+
}
|
|
2926
|
+
function validateReply(req, item) {
|
|
2927
|
+
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
2928
|
+
}
|
|
2929
|
+
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
2930
|
+
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
2931
|
+
async function resolveBatch(batch, isRetry = false) {
|
|
2932
|
+
let reply;
|
|
2933
|
+
try {
|
|
2934
|
+
reply = await callBatch(batch, signal);
|
|
2935
|
+
} catch (err) {
|
|
2936
|
+
if (!(err instanceof MalformedReplyError)) throw err;
|
|
2937
|
+
onMalformedReply?.(err.raw, batch.length);
|
|
2938
|
+
if (signal?.aborted) return failBatch(batch);
|
|
2939
|
+
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
2940
|
+
const mid = Math.ceil(batch.length / 2);
|
|
2941
|
+
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
2942
|
+
}
|
|
2943
|
+
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
2944
|
+
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
2945
|
+
}
|
|
2946
|
+
const results = [];
|
|
2947
|
+
const total = reqs.length;
|
|
2948
|
+
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
2949
|
+
if (signal?.aborted) break;
|
|
2950
|
+
const batchResults = await resolveBatch(batch);
|
|
2951
|
+
results.push(...batchResults);
|
|
2952
|
+
onBatchComplete?.(results.length, total, batchResults);
|
|
2953
|
+
}
|
|
2954
|
+
return results;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
3190
2957
|
// src/server/ai/anthropic.ts
|
|
3191
2958
|
var AnthropicProvider = class {
|
|
3192
2959
|
constructor(config, client) {
|
|
@@ -3618,6 +3385,178 @@ function makeProvider(ai) {
|
|
|
3618
3385
|
}
|
|
3619
3386
|
}
|
|
3620
3387
|
|
|
3388
|
+
// src/server/ai/run.ts
|
|
3389
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
3390
|
+
import { resolve as resolve5, extname as extname2 } from "path";
|
|
3391
|
+
function selectRequests(state, opts) {
|
|
3392
|
+
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
3393
|
+
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
3394
|
+
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3395
|
+
const reqs = [];
|
|
3396
|
+
let id = 0;
|
|
3397
|
+
for (const key of Object.keys(state.keys).sort()) {
|
|
3398
|
+
const entry = state.keys[key];
|
|
3399
|
+
if (entry.skipTranslate) continue;
|
|
3400
|
+
if (keyRe && !keyRe.test(key)) continue;
|
|
3401
|
+
if (keySet && !keySet.has(key)) continue;
|
|
3402
|
+
const sourceLv = entry.values[state.config.sourceLocale];
|
|
3403
|
+
if (entry.plural) {
|
|
3404
|
+
const sourceForms = sourceLv?.forms;
|
|
3405
|
+
const other = sourceForms?.other;
|
|
3406
|
+
if (!sourceForms || !other) continue;
|
|
3407
|
+
for (const locale of targets) {
|
|
3408
|
+
const have = entry.values[locale]?.forms ?? {};
|
|
3409
|
+
const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
|
|
3410
|
+
if (opts.onlyMissing && complete) continue;
|
|
3411
|
+
const glossary = relevantGlossary(other, locale, state.glossary);
|
|
3412
|
+
reqs.push({
|
|
3413
|
+
id: String(id++),
|
|
3414
|
+
key,
|
|
3415
|
+
source: other,
|
|
3416
|
+
sourceLocale: state.config.sourceLocale,
|
|
3417
|
+
context: entry.context,
|
|
3418
|
+
targetLocale: locale,
|
|
3419
|
+
maxLength: entry.maxLength,
|
|
3420
|
+
placeholders: extractPlaceholders(other),
|
|
3421
|
+
...glossary.length ? { glossary } : {},
|
|
3422
|
+
plural: { arg: entry.plural.arg, categories: categoriesFor(locale), sourceForms }
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
const source = sourceLv?.value;
|
|
3428
|
+
if (!source) continue;
|
|
3429
|
+
for (const locale of targets) {
|
|
3430
|
+
const existing = entry.values[locale]?.value;
|
|
3431
|
+
if (opts.onlyMissing && existing) continue;
|
|
3432
|
+
const glossary = relevantGlossary(source, locale, state.glossary);
|
|
3433
|
+
reqs.push({
|
|
3434
|
+
id: String(id++),
|
|
3435
|
+
key,
|
|
3436
|
+
source,
|
|
3437
|
+
sourceLocale: state.config.sourceLocale,
|
|
3438
|
+
context: entry.context,
|
|
3439
|
+
targetLocale: locale,
|
|
3440
|
+
maxLength: entry.maxLength,
|
|
3441
|
+
placeholders: extractPlaceholders(source),
|
|
3442
|
+
...glossary.length ? { glossary } : {}
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
return reqs;
|
|
3447
|
+
}
|
|
3448
|
+
var MEDIA_TYPES = {
|
|
3449
|
+
".png": "image/png",
|
|
3450
|
+
".jpg": "image/jpeg",
|
|
3451
|
+
".jpeg": "image/jpeg",
|
|
3452
|
+
".webp": "image/webp",
|
|
3453
|
+
".gif": "image/gif"
|
|
3454
|
+
};
|
|
3455
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
3456
|
+
function attachScreenshots(reqs, state, projectRoot) {
|
|
3457
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
3458
|
+
for (const req of reqs) {
|
|
3459
|
+
const screenshot = state.keys[req.key]?.screenshot;
|
|
3460
|
+
if (!screenshot) continue;
|
|
3461
|
+
const mediaType = MEDIA_TYPES[extname2(screenshot).toLowerCase()];
|
|
3462
|
+
if (!mediaType) continue;
|
|
3463
|
+
if (!cache2.has(screenshot)) {
|
|
3464
|
+
const abs = resolve5(projectRoot, screenshot);
|
|
3465
|
+
if (!existsSync7(abs)) {
|
|
3466
|
+
cache2.set(screenshot, null);
|
|
3467
|
+
} else {
|
|
3468
|
+
const buf = readFileSync7(abs);
|
|
3469
|
+
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
const image = cache2.get(screenshot);
|
|
3473
|
+
if (image) req.image = image;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision) {
|
|
3477
|
+
if (supportsVision) {
|
|
3478
|
+
attachScreenshots(reqs, state, projectRoot);
|
|
3479
|
+
return { skipped: 0 };
|
|
3480
|
+
}
|
|
3481
|
+
const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
|
|
3482
|
+
return { skipped: keys.size };
|
|
3483
|
+
}
|
|
3484
|
+
var DEFAULT_LOCALE_CONCURRENCY = 3;
|
|
3485
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity) {
|
|
3486
|
+
if (!reqs.length) return [];
|
|
3487
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
3488
|
+
for (const req of reqs) {
|
|
3489
|
+
let group = byLocale.get(req.targetLocale);
|
|
3490
|
+
if (!group) {
|
|
3491
|
+
group = [];
|
|
3492
|
+
byLocale.set(req.targetLocale, group);
|
|
3493
|
+
}
|
|
3494
|
+
group.push(req);
|
|
3495
|
+
}
|
|
3496
|
+
const localeBatches = [...byLocale.entries()].map(([locale, group]) => ({
|
|
3497
|
+
locale,
|
|
3498
|
+
batches: chunk(group, Math.max(1, batchSize))
|
|
3499
|
+
}));
|
|
3500
|
+
const jobs = [];
|
|
3501
|
+
const maxBatches = Math.max(...localeBatches.map((g) => g.batches.length));
|
|
3502
|
+
for (let i = 0; i < maxBatches; i++) {
|
|
3503
|
+
for (const g of localeBatches) {
|
|
3504
|
+
if (i < g.batches.length) jobs.push({ locale: g.locale, batch: g.batches[i] });
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
|
|
3508
|
+
const started = /* @__PURE__ */ new Set();
|
|
3509
|
+
const total = reqs.length;
|
|
3510
|
+
let done = 0;
|
|
3511
|
+
const allResults = [];
|
|
3512
|
+
let next = 0;
|
|
3513
|
+
async function worker() {
|
|
3514
|
+
while (next < jobs.length) {
|
|
3515
|
+
if (signal?.aborted) break;
|
|
3516
|
+
const { locale, batch } = jobs[next++];
|
|
3517
|
+
if (!started.has(locale)) {
|
|
3518
|
+
started.add(locale);
|
|
3519
|
+
hooks.onLocaleStart?.(locale);
|
|
3520
|
+
}
|
|
3521
|
+
const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
3522
|
+
done += results.length;
|
|
3523
|
+
hooks.onBatchComplete?.(done, total, results, locale);
|
|
3524
|
+
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
3525
|
+
allResults.push(...batchResults);
|
|
3526
|
+
const left = remaining.get(locale) - 1;
|
|
3527
|
+
remaining.set(locale, left);
|
|
3528
|
+
if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
|
|
3532
|
+
await Promise.all(workers);
|
|
3533
|
+
return allResults;
|
|
3534
|
+
}
|
|
3535
|
+
function applyResults(state, reqs, results, clock = systemClock, force = false) {
|
|
3536
|
+
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
3537
|
+
let written = 0;
|
|
3538
|
+
const errors = [];
|
|
3539
|
+
for (const res of results) {
|
|
3540
|
+
const req = byId.get(res.id);
|
|
3541
|
+
if (!req) continue;
|
|
3542
|
+
if (req.plural) {
|
|
3543
|
+
if (res.error || res.forms === void 0) {
|
|
3544
|
+
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
3545
|
+
continue;
|
|
3546
|
+
}
|
|
3547
|
+
if (applyMachineTranslationForms(state, req.key, req.targetLocale, res.forms, clock, force)) written++;
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
if (res.translation === void 0) {
|
|
3551
|
+
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
3555
|
+
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
3556
|
+
}
|
|
3557
|
+
return { written, errors };
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3621
3560
|
// src/server/ai/pricing.ts
|
|
3622
3561
|
var PRICE_TABLE = [
|
|
3623
3562
|
["claude-fable-5", 10, 50],
|