glotfile 0.5.0 → 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 +33 -16
- package/dist/server/server.js +115 -99
- package/package.json +1 -1
package/dist/server/cli.js
CHANGED
|
@@ -2806,7 +2806,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
2806
2806
|
const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
|
|
2807
2807
|
return { skipped: keys.size };
|
|
2808
2808
|
}
|
|
2809
|
-
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal) {
|
|
2809
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity) {
|
|
2810
2810
|
if (!reqs.length) return [];
|
|
2811
2811
|
const byLocale = /* @__PURE__ */ new Map();
|
|
2812
2812
|
for (const req of reqs) {
|
|
@@ -2817,26 +2817,42 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
2817
2817
|
}
|
|
2818
2818
|
group.push(req);
|
|
2819
2819
|
}
|
|
2820
|
+
const localeBatches = [...byLocale.entries()].map(([locale, group]) => ({
|
|
2821
|
+
locale,
|
|
2822
|
+
batches: chunk(group, Math.max(1, batchSize))
|
|
2823
|
+
}));
|
|
2824
|
+
const jobs = [];
|
|
2825
|
+
const maxBatches = Math.max(...localeBatches.map((g) => g.batches.length));
|
|
2826
|
+
for (let i = 0; i < maxBatches; i++) {
|
|
2827
|
+
for (const g of localeBatches) {
|
|
2828
|
+
if (i < g.batches.length) jobs.push({ locale: g.locale, batch: g.batches[i] });
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
|
|
2832
|
+
const started = /* @__PURE__ */ new Set();
|
|
2820
2833
|
const total = reqs.length;
|
|
2821
2834
|
let done = 0;
|
|
2822
2835
|
const allResults = [];
|
|
2823
|
-
const groups = [...byLocale.values()];
|
|
2824
2836
|
let next = 0;
|
|
2825
2837
|
async function worker() {
|
|
2826
|
-
while (next <
|
|
2838
|
+
while (next < jobs.length) {
|
|
2827
2839
|
if (signal?.aborted) break;
|
|
2828
|
-
const
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2840
|
+
const { locale, batch } = jobs[next++];
|
|
2841
|
+
if (!started.has(locale)) {
|
|
2842
|
+
started.add(locale);
|
|
2843
|
+
hooks.onLocaleStart?.(locale);
|
|
2844
|
+
}
|
|
2845
|
+
const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
2846
|
+
done += results.length;
|
|
2847
|
+
hooks.onBatchComplete?.(done, total, results, locale);
|
|
2848
|
+
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
2849
|
+
allResults.push(...batchResults);
|
|
2850
|
+
const left = remaining.get(locale) - 1;
|
|
2851
|
+
remaining.set(locale, left);
|
|
2852
|
+
if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
2837
2853
|
}
|
|
2838
2854
|
}
|
|
2839
|
-
const workers = Array.from({ length: Math.min(concurrency,
|
|
2855
|
+
const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
|
|
2840
2856
|
await Promise.all(workers);
|
|
2841
2857
|
return allResults;
|
|
2842
2858
|
}
|
|
@@ -2872,6 +2888,7 @@ var init_run = __esm({
|
|
|
2872
2888
|
init_plurals();
|
|
2873
2889
|
init_state();
|
|
2874
2890
|
init_glob();
|
|
2891
|
+
init_batch();
|
|
2875
2892
|
MEDIA_TYPES = {
|
|
2876
2893
|
".png": "image/png",
|
|
2877
2894
|
".jpg": "image/jpeg",
|
|
@@ -5453,7 +5470,7 @@ function createApi(deps) {
|
|
|
5453
5470
|
raw
|
|
5454
5471
|
});
|
|
5455
5472
|
}
|
|
5456
|
-
}, aiCfg.concurrency, signal);
|
|
5473
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
5457
5474
|
if (!signal?.aborted) {
|
|
5458
5475
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
5459
5476
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -5496,7 +5513,7 @@ function createApi(deps) {
|
|
|
5496
5513
|
raw
|
|
5497
5514
|
});
|
|
5498
5515
|
}
|
|
5499
|
-
}, aiCfg.concurrency);
|
|
5516
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
5500
5517
|
const latest = load();
|
|
5501
5518
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
5502
5519
|
const entry = {
|
|
@@ -6123,7 +6140,7 @@ async function runTranslate(args) {
|
|
|
6123
6140
|
raw
|
|
6124
6141
|
});
|
|
6125
6142
|
}
|
|
6126
|
-
});
|
|
6143
|
+
}, ai.concurrency, void 0, ai.batchSize);
|
|
6127
6144
|
process.stdout.write("\n");
|
|
6128
6145
|
if (!batchCallbackFired) {
|
|
6129
6146
|
({ written, errors } = applyResults(state, toTranslate, results));
|
package/dist/server/server.js
CHANGED
|
@@ -1493,6 +1493,88 @@ function globToRegExp2(glob) {
|
|
|
1493
1493
|
return new RegExp(`^${escaped}$`);
|
|
1494
1494
|
}
|
|
1495
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
|
+
|
|
1496
1578
|
// src/server/ai/run.ts
|
|
1497
1579
|
function selectRequests(state, opts) {
|
|
1498
1580
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
@@ -1603,7 +1685,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
1603
1685
|
return { skipped: keys.size };
|
|
1604
1686
|
}
|
|
1605
1687
|
var DEFAULT_LOCALE_CONCURRENCY = 3;
|
|
1606
|
-
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) {
|
|
1607
1689
|
if (!reqs.length) return [];
|
|
1608
1690
|
const byLocale = /* @__PURE__ */ new Map();
|
|
1609
1691
|
for (const req of reqs) {
|
|
@@ -1614,26 +1696,42 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
1614
1696
|
}
|
|
1615
1697
|
group.push(req);
|
|
1616
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();
|
|
1617
1712
|
const total = reqs.length;
|
|
1618
1713
|
let done = 0;
|
|
1619
1714
|
const allResults = [];
|
|
1620
|
-
const groups = [...byLocale.values()];
|
|
1621
1715
|
let next = 0;
|
|
1622
1716
|
async function worker() {
|
|
1623
|
-
while (next <
|
|
1717
|
+
while (next < jobs.length) {
|
|
1624
1718
|
if (signal?.aborted) break;
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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);
|
|
1637
1735
|
await Promise.all(workers);
|
|
1638
1736
|
return allResults;
|
|
1639
1737
|
}
|
|
@@ -3042,88 +3140,6 @@ var BATCH_SCHEMA = {
|
|
|
3042
3140
|
additionalProperties: false
|
|
3043
3141
|
};
|
|
3044
3142
|
|
|
3045
|
-
// src/server/ai/batch.ts
|
|
3046
|
-
var MalformedReplyError = class extends Error {
|
|
3047
|
-
constructor(raw) {
|
|
3048
|
-
super("Model reply was not valid translation JSON.");
|
|
3049
|
-
this.raw = raw;
|
|
3050
|
-
this.name = "MalformedReplyError";
|
|
3051
|
-
}
|
|
3052
|
-
raw;
|
|
3053
|
-
};
|
|
3054
|
-
function parseReplyItems(text) {
|
|
3055
|
-
let parsed;
|
|
3056
|
-
try {
|
|
3057
|
-
parsed = JSON.parse(text);
|
|
3058
|
-
} catch {
|
|
3059
|
-
throw new MalformedReplyError(text);
|
|
3060
|
-
}
|
|
3061
|
-
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
3062
|
-
return parsed.items;
|
|
3063
|
-
}
|
|
3064
|
-
function chunk(items, size) {
|
|
3065
|
-
const out = [];
|
|
3066
|
-
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
3067
|
-
return out;
|
|
3068
|
-
}
|
|
3069
|
-
function validateTranslation(req, translation) {
|
|
3070
|
-
if (translation === void 0) return { id: req.id, error: "No translation returned." };
|
|
3071
|
-
if (!placeholdersMatch(req.source, translation)) {
|
|
3072
|
-
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
3073
|
-
}
|
|
3074
|
-
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
3075
|
-
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
3076
|
-
}
|
|
3077
|
-
return { id: req.id, translation };
|
|
3078
|
-
}
|
|
3079
|
-
function validatePlural(req, forms) {
|
|
3080
|
-
if (!forms) return { id: req.id, error: "No translation returned." };
|
|
3081
|
-
const plural = req.plural;
|
|
3082
|
-
if (!plural) return { id: req.id, error: "validatePlural called on a non-plural request." };
|
|
3083
|
-
const cats = plural.categories;
|
|
3084
|
-
const missing = cats.filter((c) => typeof forms[c] !== "string");
|
|
3085
|
-
if (missing.length) return { id: req.id, error: `Missing plural categories: ${missing.join(", ")}.` };
|
|
3086
|
-
const badPh = cats.find((c) => !pluralFormPlaceholdersMatch(c, req.source, forms[c]));
|
|
3087
|
-
if (badPh) return { id: req.id, error: `Placeholder mismatch in plural form "${badPh}".` };
|
|
3088
|
-
if (req.maxLength !== void 0) {
|
|
3089
|
-
const over = cats.find((c) => forms[c].length > req.maxLength);
|
|
3090
|
-
if (over) return { id: req.id, error: `Plural form "${over}" exceeds maxLength (${forms[over].length} > ${req.maxLength}).` };
|
|
3091
|
-
}
|
|
3092
|
-
const out = {};
|
|
3093
|
-
for (const c of cats) out[c] = forms[c];
|
|
3094
|
-
return { id: req.id, forms: out };
|
|
3095
|
-
}
|
|
3096
|
-
function validateReply(req, item) {
|
|
3097
|
-
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
3098
|
-
}
|
|
3099
|
-
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
3100
|
-
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
3101
|
-
async function resolveBatch(batch, isRetry = false) {
|
|
3102
|
-
let reply;
|
|
3103
|
-
try {
|
|
3104
|
-
reply = await callBatch(batch, signal);
|
|
3105
|
-
} catch (err) {
|
|
3106
|
-
if (!(err instanceof MalformedReplyError)) throw err;
|
|
3107
|
-
onMalformedReply?.(err.raw, batch.length);
|
|
3108
|
-
if (signal?.aborted) return failBatch(batch);
|
|
3109
|
-
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
3110
|
-
const mid = Math.ceil(batch.length / 2);
|
|
3111
|
-
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
3112
|
-
}
|
|
3113
|
-
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
3114
|
-
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
3115
|
-
}
|
|
3116
|
-
const results = [];
|
|
3117
|
-
const total = reqs.length;
|
|
3118
|
-
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
3119
|
-
if (signal?.aborted) break;
|
|
3120
|
-
const batchResults = await resolveBatch(batch);
|
|
3121
|
-
results.push(...batchResults);
|
|
3122
|
-
onBatchComplete?.(results.length, total, batchResults);
|
|
3123
|
-
}
|
|
3124
|
-
return results;
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
3143
|
// src/server/ai/anthropic.ts
|
|
3128
3144
|
var AnthropicProvider = class {
|
|
3129
3145
|
constructor(config, client) {
|
|
@@ -5002,7 +5018,7 @@ function createApi(deps) {
|
|
|
5002
5018
|
raw
|
|
5003
5019
|
});
|
|
5004
5020
|
}
|
|
5005
|
-
}, aiCfg.concurrency, signal);
|
|
5021
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
5006
5022
|
if (!signal?.aborted) {
|
|
5007
5023
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
5008
5024
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -5045,7 +5061,7 @@ function createApi(deps) {
|
|
|
5045
5061
|
raw
|
|
5046
5062
|
});
|
|
5047
5063
|
}
|
|
5048
|
-
}, aiCfg.concurrency);
|
|
5064
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
5049
5065
|
const latest = load();
|
|
5050
5066
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
5051
5067
|
const entry = {
|