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.
@@ -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 < groups.length) {
2838
+ while (next < jobs.length) {
2827
2839
  if (signal?.aborted) break;
2828
- const group = groups[next++];
2829
- const locale = group[0].targetLocale;
2830
- hooks.onLocaleStart?.(locale);
2831
- const localeResults = await provider.translate(group, (_localeDone, _localeTotal, batchResults) => {
2832
- done += batchResults.length;
2833
- hooks.onBatchComplete?.(done, total, batchResults, locale);
2834
- }, signal, (raw, batchSize) => hooks.onMalformedReply?.(raw, batchSize, locale));
2835
- allResults.push(...localeResults);
2836
- if (!signal?.aborted) hooks.onLocaleDone?.(locale);
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, groups.length) }, worker);
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));
@@ -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 < groups.length) {
1717
+ while (next < jobs.length) {
1624
1718
  if (signal?.aborted) break;
1625
- const group = groups[next++];
1626
- const locale = group[0].targetLocale;
1627
- hooks.onLocaleStart?.(locale);
1628
- const localeResults = await provider.translate(group, (_localeDone, _localeTotal, batchResults) => {
1629
- done += batchResults.length;
1630
- hooks.onBatchComplete?.(done, total, batchResults, locale);
1631
- }, signal, (raw, batchSize) => hooks.onMalformedReply?.(raw, batchSize, locale));
1632
- allResults.push(...localeResults);
1633
- if (!signal?.aborted) hooks.onLocaleDone?.(locale);
1634
- }
1635
- }
1636
- const workers = Array.from({ length: Math.min(concurrency, groups.length) }, worker);
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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glotfile",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local-first, git-native translation management.",
5
5
  "type": "module",
6
6
  "bin": {