glotfile 0.5.0 → 0.5.2

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.
@@ -29,7 +29,7 @@ var init_dictionary_en = __esm({
29
29
  import { Hono as Hono2 } from "hono";
30
30
  import { serve } from "@hono/node-server";
31
31
  import { fileURLToPath } from "url";
32
- import { dirname as dirname4, join as join9, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
32
+ import { dirname as dirname4, join as join10, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
33
33
  import { readFile, stat } from "fs/promises";
34
34
  import { createServer } from "net";
35
35
  import open from "open";
@@ -916,7 +916,11 @@ var PATTERNS = {
916
916
  apple: [
917
917
  /NSLocalizedString\s*\(\s*@?"([^"]+)"/g,
918
918
  /String\s*\(\s*localized:\s*"([^"]+)"/g,
919
- /localizedString\s*\(\s*forKey:\s*"([^"]+)"/g
919
+ /localizedString\s*\(\s*forKey:\s*"([^"]+)"/g,
920
+ // The "key".localized / "key".localised String-extension idiom, where the
921
+ // literal IS the key (common when keys are natural-language source text).
922
+ /"([^"]+)"\s*\.\s*localized\b/g,
923
+ /"([^"]+)"\s*\.\s*localised\b/g
920
924
  ]
921
925
  };
922
926
  var PREFIX_PATTERNS = {
@@ -934,7 +938,7 @@ var PREFIX_PATTERNS = {
934
938
  /(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
935
939
  ]
936
940
  };
937
- var CACHE_VERSION = 4;
941
+ var CACHE_VERSION = 5;
938
942
  var EXT_SCANNER = {
939
943
  ".php": "laravel",
940
944
  ".vue": "js-i18n",
@@ -1493,6 +1497,88 @@ function globToRegExp2(glob) {
1493
1497
  return new RegExp(`^${escaped}$`);
1494
1498
  }
1495
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
+
1496
1582
  // src/server/ai/run.ts
1497
1583
  function selectRequests(state, opts) {
1498
1584
  const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
@@ -1603,7 +1689,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
1603
1689
  return { skipped: keys.size };
1604
1690
  }
1605
1691
  var DEFAULT_LOCALE_CONCURRENCY = 3;
1606
- async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal) {
1692
+ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity) {
1607
1693
  if (!reqs.length) return [];
1608
1694
  const byLocale = /* @__PURE__ */ new Map();
1609
1695
  for (const req of reqs) {
@@ -1614,26 +1700,42 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
1614
1700
  }
1615
1701
  group.push(req);
1616
1702
  }
1703
+ const localeBatches = [...byLocale.entries()].map(([locale, group]) => ({
1704
+ locale,
1705
+ batches: chunk(group, Math.max(1, batchSize))
1706
+ }));
1707
+ const jobs = [];
1708
+ const maxBatches = Math.max(...localeBatches.map((g) => g.batches.length));
1709
+ for (let i = 0; i < maxBatches; i++) {
1710
+ for (const g of localeBatches) {
1711
+ if (i < g.batches.length) jobs.push({ locale: g.locale, batch: g.batches[i] });
1712
+ }
1713
+ }
1714
+ const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
1715
+ const started = /* @__PURE__ */ new Set();
1617
1716
  const total = reqs.length;
1618
1717
  let done = 0;
1619
1718
  const allResults = [];
1620
- const groups = [...byLocale.values()];
1621
1719
  let next = 0;
1622
1720
  async function worker() {
1623
- while (next < groups.length) {
1721
+ while (next < jobs.length) {
1624
1722
  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);
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);
1637
1739
  await Promise.all(workers);
1638
1740
  return allResults;
1639
1741
  }
@@ -2639,8 +2741,50 @@ var appleStringsdict = {
2639
2741
  }
2640
2742
  };
2641
2743
 
2744
+ // src/server/adapters/apple-strings.ts
2745
+ var DEFAULT_LOCALE_CASE6 = "bcp47-hyphen";
2746
+ function escape(s) {
2747
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
2748
+ }
2749
+ var appleStrings = {
2750
+ name: "apple-strings",
2751
+ capabilities: {
2752
+ // Plurals belong in .stringsdict (apple-stringsdict), not the scalar table.
2753
+ plural: "none",
2754
+ select: "none",
2755
+ nesting: "flat",
2756
+ metadata: false,
2757
+ placeholderStyle: "printf",
2758
+ fileGrouping: "per-locale"
2759
+ },
2760
+ defaultLocaleCase: DEFAULT_LOCALE_CASE6,
2761
+ export(state, output) {
2762
+ const files = [];
2763
+ const warnings = [];
2764
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE6));
2765
+ const emptyAs = resolveEmptyAs(output, "source");
2766
+ const keys = Object.keys(state.keys).sort();
2767
+ for (const locale of state.config.locales) {
2768
+ const lines = [];
2769
+ for (const key of keys) {
2770
+ const entry = state.keys[key];
2771
+ if (entry.plural) continue;
2772
+ const value = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
2773
+ if (value === null) continue;
2774
+ lines.push(`"${escape(key)}" = "${escape(value)}";`);
2775
+ }
2776
+ const contents = lines.length ? lines.join("\n") + "\n" : "";
2777
+ files.push({
2778
+ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE6)),
2779
+ contents
2780
+ });
2781
+ }
2782
+ return { files, warnings };
2783
+ }
2784
+ };
2785
+
2642
2786
  // src/server/adapters/vue-i18n-json.ts
2643
- var DEFAULT_LOCALE_CASE6 = "lower-hyphen";
2787
+ var DEFAULT_LOCALE_CASE7 = "lower-hyphen";
2644
2788
  var vueI18nJson = {
2645
2789
  name: "vue-i18n-json",
2646
2790
  capabilities: {
@@ -2651,11 +2795,11 @@ var vueI18nJson = {
2651
2795
  placeholderStyle: "named",
2652
2796
  fileGrouping: "per-locale"
2653
2797
  },
2654
- defaultLocaleCase: DEFAULT_LOCALE_CASE6,
2798
+ defaultLocaleCase: DEFAULT_LOCALE_CASE7,
2655
2799
  export(state, output) {
2656
2800
  const files = [];
2657
2801
  const warnings = [];
2658
- warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE6));
2802
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
2659
2803
  const { indent, finalNewline } = resolveFormat(state, output);
2660
2804
  const fmt = { indent, sortKeys: true, finalNewline };
2661
2805
  const emptyAs = resolveEmptyAs(output, "omit");
@@ -2695,7 +2839,7 @@ var vueI18nJson = {
2695
2839
  }
2696
2840
  payload = tree;
2697
2841
  }
2698
- files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE6)), contents: serializeJson(payload, fmt) });
2842
+ files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7)), contents: serializeJson(payload, fmt) });
2699
2843
  }
2700
2844
  files.sort((a, b) => a.path.localeCompare(b.path));
2701
2845
  return { files, warnings };
@@ -2739,7 +2883,7 @@ function renderEmbeddedIcu(value) {
2739
2883
  function renderScalar(value, ids) {
2740
2884
  return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
2741
2885
  }
2742
- var DEFAULT_LOCALE_CASE7 = "bcp47-hyphen";
2886
+ var DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
2743
2887
  var angularXliff = {
2744
2888
  name: "angular-xliff",
2745
2889
  capabilities: {
@@ -2750,17 +2894,17 @@ var angularXliff = {
2750
2894
  placeholderStyle: "icu",
2751
2895
  fileGrouping: "per-locale"
2752
2896
  },
2753
- defaultLocaleCase: DEFAULT_LOCALE_CASE7,
2897
+ defaultLocaleCase: DEFAULT_LOCALE_CASE8,
2754
2898
  export(state, output) {
2755
2899
  const files = [];
2756
2900
  const warnings = [];
2757
- warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
2901
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
2758
2902
  const sourceLocale = state.config.sourceLocale;
2759
- const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE7);
2903
+ const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE8);
2760
2904
  const emptyAs = resolveEmptyAs(output, "source");
2761
2905
  const keys = Object.keys(state.keys).sort();
2762
2906
  for (const locale of state.config.locales) {
2763
- const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7);
2907
+ const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
2764
2908
  const units = [];
2765
2909
  for (const key of keys) {
2766
2910
  const entry = state.keys[key];
@@ -2823,7 +2967,7 @@ function yamlMap(node, indent, level) {
2823
2967
  }
2824
2968
  return lines;
2825
2969
  }
2826
- var DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
2970
+ var DEFAULT_LOCALE_CASE9 = "bcp47-hyphen";
2827
2971
  var railsYaml = {
2828
2972
  name: "rails-yaml",
2829
2973
  capabilities: {
@@ -2834,10 +2978,10 @@ var railsYaml = {
2834
2978
  placeholderStyle: "named",
2835
2979
  fileGrouping: "per-locale"
2836
2980
  },
2837
- defaultLocaleCase: DEFAULT_LOCALE_CASE8,
2981
+ defaultLocaleCase: DEFAULT_LOCALE_CASE9,
2838
2982
  export(state, output) {
2839
2983
  const warnings = [];
2840
- warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
2984
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE9));
2841
2985
  const { indent, finalNewline } = resolveFormat(state, output);
2842
2986
  const emptyAs = resolveEmptyAs(output, "omit");
2843
2987
  const files = [];
@@ -2869,7 +3013,7 @@ var railsYaml = {
2869
3013
  for (const c of collisions) {
2870
3014
  warnings.push({ code: "key-collision", key: c, locale, message: "key is both a leaf and a parent; dropped from nested output" });
2871
3015
  }
2872
- const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
3016
+ const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE9);
2873
3017
  const body = [`${yamlKey(token)}:`, ...yamlMap(nested, indent, 1)].join("\n");
2874
3018
  files.push({ path: resolvePath(output.path, token), contents: finalNewline ? body + "\n" : body });
2875
3019
  }
@@ -2910,6 +3054,7 @@ function getRegistry() {
2910
3054
  [i18nextJson.name]: i18nextJson,
2911
3055
  [gettextPo.name]: gettextPo,
2912
3056
  [appleStringsdict.name]: appleStringsdict,
3057
+ [appleStrings.name]: appleStrings,
2913
3058
  [vueI18nJson.name]: vueI18nJson,
2914
3059
  [angularXliff.name]: angularXliff,
2915
3060
  [railsYaml.name]: railsYaml
@@ -2940,8 +3085,8 @@ function checkOutputs(state, root) {
2940
3085
  }
2941
3086
 
2942
3087
  // src/server/api.ts
2943
- import { readFileSync as readFileSync14, existsSync as existsSync11, readdirSync as readdirSync8, statSync as statSync5, rmSync as rmSync4 } from "fs";
2944
- import { dirname as dirname3, resolve as resolve9, basename, relative as relative3, sep as sep2 } from "path";
3088
+ import { readFileSync as readFileSync15, existsSync as existsSync11, readdirSync as readdirSync9, statSync as statSync6, rmSync as rmSync4 } from "fs";
3089
+ import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
2945
3090
 
2946
3091
  // src/server/ai/anthropic.ts
2947
3092
  import Anthropic from "@anthropic-ai/sdk";
@@ -3042,88 +3187,6 @@ var BATCH_SCHEMA = {
3042
3187
  additionalProperties: false
3043
3188
  };
3044
3189
 
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
3190
  // src/server/ai/anthropic.ts
3128
3191
  var AnthropicProvider = class {
3129
3192
  constructor(config, client) {
@@ -3669,6 +3732,9 @@ function readLog(projectRoot, limit = 100) {
3669
3732
  return entries.reverse().slice(0, limit);
3670
3733
  }
3671
3734
 
3735
+ // src/server/import/run.ts
3736
+ import { relative as relative3 } from "path";
3737
+
3672
3738
  // src/server/import/detect.ts
3673
3739
  import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
3674
3740
  import { join as join4 } from "path";
@@ -3733,11 +3799,38 @@ function detectArb(root) {
3733
3799
  }
3734
3800
  return null;
3735
3801
  }
3736
- var DETECTORS = [detectLaravel, detectVue, detectArb];
3802
+ function lprojLocales(dir) {
3803
+ return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync9(join4(dir, `${l}.lproj`, "Localizable.strings")));
3804
+ }
3805
+ function detectApple(root) {
3806
+ const candidates = [root, ...listDirs(root).map((d) => join4(root, d))];
3807
+ let best = null;
3808
+ for (const dir of candidates) {
3809
+ const locales = lprojLocales(dir);
3810
+ if (locales.length === 0) continue;
3811
+ if (!best || locales.length > best.locales.length) {
3812
+ best = {
3813
+ format: "apple-strings",
3814
+ localeRoot: dir,
3815
+ locales,
3816
+ sourceLocale: pickSource(locales, (loc) => {
3817
+ try {
3818
+ return statSync2(join4(dir, `${loc}.lproj`, "Localizable.strings")).size;
3819
+ } catch {
3820
+ return 0;
3821
+ }
3822
+ })
3823
+ };
3824
+ }
3825
+ }
3826
+ return best;
3827
+ }
3828
+ var DETECTORS = [detectLaravel, detectVue, detectArb, detectApple];
3737
3829
  var BY_FORMAT = {
3738
3830
  "laravel-php": detectLaravel,
3739
3831
  "vue-i18n-json": (root) => detectVue(root, true),
3740
- "flutter-arb": detectArb
3832
+ "flutter-arb": detectArb,
3833
+ "apple-strings": detectApple
3741
3834
  };
3742
3835
  function detect(root, formatOverride) {
3743
3836
  if (!existsSync9(root)) return null;
@@ -3947,11 +4040,139 @@ var flutterArb2 = {
3947
4040
  }
3948
4041
  };
3949
4042
 
4043
+ // src/server/import/parsers/apple-strings.ts
4044
+ import { readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync4 } from "fs";
4045
+ import { join as join8 } from "path";
4046
+ var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4047
+ var TABLE = "Localizable.strings";
4048
+ function localeFromLproj(dir) {
4049
+ const m = dir.match(/^(.+)\.lproj$/);
4050
+ if (!m) return null;
4051
+ return LOCALE_RE4.test(m[1]) ? m[1] : null;
4052
+ }
4053
+ function unescape(body) {
4054
+ return body.replace(/\\(U[0-9a-fA-F]{4}|u[0-9a-fA-F]{4}|.)/g, (_m, esc) => {
4055
+ const c = esc[0];
4056
+ if (c === "U" || c === "u") return String.fromCharCode(parseInt(esc.slice(1), 16));
4057
+ if (c === "n") return "\n";
4058
+ if (c === "t") return " ";
4059
+ if (c === "r") return "\r";
4060
+ return esc;
4061
+ });
4062
+ }
4063
+ function parseStrings(text, file, warnings) {
4064
+ const pairs = [];
4065
+ let i = 0;
4066
+ const n = text.length;
4067
+ const skipTrivia = () => {
4068
+ while (i < n) {
4069
+ const c = text[i];
4070
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
4071
+ i++;
4072
+ continue;
4073
+ }
4074
+ if (c === "/" && text[i + 1] === "/") {
4075
+ i += 2;
4076
+ while (i < n && text[i] !== "\n") i++;
4077
+ continue;
4078
+ }
4079
+ if (c === "/" && text[i + 1] === "*") {
4080
+ i += 2;
4081
+ while (i < n && !(text[i] === "*" && text[i + 1] === "/")) i++;
4082
+ i += 2;
4083
+ continue;
4084
+ }
4085
+ break;
4086
+ }
4087
+ };
4088
+ const readToken = () => {
4089
+ if (i >= n) return null;
4090
+ if (text[i] === '"') {
4091
+ i++;
4092
+ let raw2 = "";
4093
+ while (i < n) {
4094
+ const c = text[i];
4095
+ if (c === "\\") {
4096
+ raw2 += c + (text[i + 1] ?? "");
4097
+ i += 2;
4098
+ continue;
4099
+ }
4100
+ if (c === '"') {
4101
+ i++;
4102
+ return unescape(raw2);
4103
+ }
4104
+ raw2 += c;
4105
+ i++;
4106
+ }
4107
+ return null;
4108
+ }
4109
+ let raw = "";
4110
+ while (i < n && !/[\s=;]/.test(text[i])) raw += text[i++];
4111
+ return raw.length ? raw : null;
4112
+ };
4113
+ while (true) {
4114
+ skipTrivia();
4115
+ if (i >= n) break;
4116
+ const key = readToken();
4117
+ if (key === null) {
4118
+ warnings.push(`apple-strings: malformed entry in ${file} near offset ${i}`);
4119
+ break;
4120
+ }
4121
+ skipTrivia();
4122
+ if (text[i] !== "=") {
4123
+ warnings.push(`apple-strings: expected '=' after key "${key}" in ${file}`);
4124
+ break;
4125
+ }
4126
+ i++;
4127
+ skipTrivia();
4128
+ const value = readToken();
4129
+ if (value === null) {
4130
+ warnings.push(`apple-strings: missing value for key "${key}" in ${file}`);
4131
+ break;
4132
+ }
4133
+ skipTrivia();
4134
+ if (text[i] === ";") i++;
4135
+ pairs.push({ key, value });
4136
+ }
4137
+ return pairs;
4138
+ }
4139
+ var appleStrings2 = {
4140
+ name: "apple-strings",
4141
+ parse(localeRoot, opts) {
4142
+ const warnings = [];
4143
+ const keys = {};
4144
+ const locales = [];
4145
+ for (const dir of readdirSync7(localeRoot).sort()) {
4146
+ const locale = localeFromLproj(dir);
4147
+ if (!locale) continue;
4148
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4149
+ const file = join8(localeRoot, dir, TABLE);
4150
+ let text;
4151
+ try {
4152
+ if (!statSync4(file).isFile()) continue;
4153
+ text = readFileSync11(file, "utf8");
4154
+ } catch {
4155
+ continue;
4156
+ }
4157
+ locales.push(locale);
4158
+ const others = readdirSync7(join8(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
4159
+ if (others.length) {
4160
+ warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
4161
+ }
4162
+ for (const { key, value } of parseStrings(text, file, warnings)) {
4163
+ (keys[key] ??= { values: {} }).values[locale] = value;
4164
+ }
4165
+ }
4166
+ return { locales, keys, warnings };
4167
+ }
4168
+ };
4169
+
3950
4170
  // src/server/import/parsers/index.ts
3951
4171
  var REGISTRY = {
3952
4172
  [vueI18nJson2.name]: vueI18nJson2,
3953
4173
  [laravelPhp2.name]: laravelPhp2,
3954
- [flutterArb2.name]: flutterArb2
4174
+ [flutterArb2.name]: flutterArb2,
4175
+ [appleStrings2.name]: appleStrings2
3955
4176
  };
3956
4177
  function getParser(name) {
3957
4178
  const p = REGISTRY[name];
@@ -3963,16 +4184,20 @@ function getParser(name) {
3963
4184
  var OUTPUT_BY_FORMAT = {
3964
4185
  "laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
3965
4186
  "vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
3966
- "flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" }
4187
+ "flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
4188
+ "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true }
3967
4189
  };
3968
4190
  function assemble2(parsed, opts) {
3969
4191
  const warnings = [...parsed.warnings];
3970
4192
  const base = OUTPUT_BY_FORMAT[opts.format];
3971
4193
  if (!base) throw new Error(`No output mapping for format "${opts.format}"`);
4194
+ const prefix = (opts.localeRootRel ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
4195
+ const path = base.rootRelative && prefix ? `${prefix}/${base.path}` : base.path;
3972
4196
  const rawLocales = [.../* @__PURE__ */ new Set([opts.sourceLocale, ...parsed.locales])];
3973
4197
  const pairs = rawLocales.map((obs) => [canonLocale(obs), obs]);
3974
4198
  const inferred = inferLocaleStyle(pairs, getAdapter(base.adapter).defaultLocaleCase);
3975
- const output = { ...base, ...inferred };
4199
+ const { rootRelative: _rootRelative, ...baseOutput } = base;
4200
+ const output = { ...baseOutput, path, ...inferred };
3976
4201
  const sourceLocale = canonLocale(opts.sourceLocale);
3977
4202
  const locales = [...new Set(rawLocales.map(canonLocale))].sort();
3978
4203
  const keys = {};
@@ -4064,7 +4289,8 @@ function runImport(opts) {
4064
4289
  const assembled = assemble2(parsed, {
4065
4290
  sourceLocale: opts.sourceLocale ?? det.sourceLocale,
4066
4291
  format: det.format,
4067
- cldr: opts.cldr
4292
+ cldr: opts.cldr,
4293
+ localeRootRel: relative3(opts.projectRoot, det.localeRoot)
4068
4294
  });
4069
4295
  const { warnings, ...rest } = assembled;
4070
4296
  const state = validate(rest);
@@ -4077,7 +4303,7 @@ function runImport(opts) {
4077
4303
  }
4078
4304
 
4079
4305
  // src/server/export-run.ts
4080
- import { existsSync as existsSync10, readFileSync as readFileSync11, readdirSync as readdirSync7, rmdirSync, statSync as statSync4, unlinkSync } from "fs";
4306
+ import { existsSync as existsSync10, readFileSync as readFileSync12, readdirSync as readdirSync8, rmdirSync, statSync as statSync5, unlinkSync } from "fs";
4081
4307
  import { dirname as dirname2, resolve as resolve7, sep } from "path";
4082
4308
  function effectiveLocales(config) {
4083
4309
  const limit = config.exportLocales;
@@ -4120,7 +4346,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
4120
4346
  if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
4121
4347
  const next = resolve7(dir, segment);
4122
4348
  if (isLast) {
4123
- if (stale(locale) && existsSync10(next) && statSync4(next).isFile()) {
4349
+ if (stale(locale) && existsSync10(next) && statSync5(next).isFile()) {
4124
4350
  unlinkSync(next);
4125
4351
  deleted++;
4126
4352
  removeEmptyDirs(dir, root);
@@ -4133,7 +4359,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
4133
4359
  const re = segmentRegExp(segment);
4134
4360
  let entries;
4135
4361
  try {
4136
- entries = readdirSync7(dir, { withFileTypes: true });
4362
+ entries = readdirSync8(dir, { withFileTypes: true });
4137
4363
  } catch {
4138
4364
  return;
4139
4365
  }
@@ -4176,7 +4402,7 @@ function exportToDisk(state, projectRoot, opts) {
4176
4402
  writtenPaths.add(abs);
4177
4403
  let current = null;
4178
4404
  try {
4179
- current = readFileSync11(abs, "utf8");
4405
+ current = readFileSync12(abs, "utf8");
4180
4406
  } catch {
4181
4407
  }
4182
4408
  if (current === f.contents) {
@@ -4193,17 +4419,17 @@ function exportToDisk(state, projectRoot, opts) {
4193
4419
  }
4194
4420
 
4195
4421
  // src/server/ui-prefs.ts
4196
- import { readFileSync as readFileSync12 } from "fs";
4422
+ import { readFileSync as readFileSync13 } from "fs";
4197
4423
  import { homedir } from "os";
4198
- import { join as join8 } from "path";
4424
+ import { join as join9 } from "path";
4199
4425
  var THEMES = ["system", "light", "dark"];
4200
4426
  var isThemeMode = (v) => THEMES.includes(v);
4201
4427
  var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4202
- var defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
4428
+ var defaultUiPrefsPath = () => join9(homedir(), ".glotfile", "ui.json");
4203
4429
  var DEFAULTS = { theme: "system" };
4204
4430
  function readJson(path) {
4205
4431
  try {
4206
- const parsed = JSON.parse(readFileSync12(path, "utf8"));
4432
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
4207
4433
  return parsed && typeof parsed === "object" ? parsed : {};
4208
4434
  } catch {
4209
4435
  return {};
@@ -4222,7 +4448,7 @@ function saveUiPrefs(path, prefs) {
4222
4448
  }
4223
4449
 
4224
4450
  // src/server/local-settings.ts
4225
- import { readFileSync as readFileSync13 } from "fs";
4451
+ import { readFileSync as readFileSync14 } from "fs";
4226
4452
  import { resolve as resolve8 } from "path";
4227
4453
  var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
4228
4454
  var isEditorId = (v) => EDITOR_IDS.includes(v);
@@ -4237,7 +4463,7 @@ var DEFAULT_EDITOR = "vscode";
4237
4463
  var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
4238
4464
  function readJson2(path) {
4239
4465
  try {
4240
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
4466
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
4241
4467
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4242
4468
  } catch {
4243
4469
  return {};
@@ -4310,7 +4536,7 @@ function projectName(root) {
4310
4536
  const nameFile = resolve9(root, ".idea", ".name");
4311
4537
  if (existsSync11(nameFile)) {
4312
4538
  try {
4313
- const name = readFileSync14(nameFile, "utf8").trim();
4539
+ const name = readFileSync15(nameFile, "utf8").trim();
4314
4540
  if (name) return name;
4315
4541
  } catch {
4316
4542
  }
@@ -4425,7 +4651,7 @@ function createApi(deps) {
4425
4651
  app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
4426
4652
  app.get("/files", (c) => {
4427
4653
  const found = /* @__PURE__ */ new Map();
4428
- const activeRel = relative3(projectRoot, deps.statePath);
4654
+ const activeRel = relative4(projectRoot, deps.statePath);
4429
4655
  found.set(deps.statePath, {
4430
4656
  name: basename(deps.statePath),
4431
4657
  path: deps.statePath,
@@ -4435,7 +4661,7 @@ function createApi(deps) {
4435
4661
  if (depth > 4) return;
4436
4662
  let entries = [];
4437
4663
  try {
4438
- entries = readdirSync8(dir);
4664
+ entries = readdirSync9(dir);
4439
4665
  } catch {
4440
4666
  return;
4441
4667
  }
@@ -4449,7 +4675,7 @@ function createApi(deps) {
4449
4675
  filePath = abs;
4450
4676
  } else {
4451
4677
  try {
4452
- if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
4678
+ if (statSync6(abs).isDirectory()) walk(abs, depth + 1);
4453
4679
  } catch {
4454
4680
  }
4455
4681
  continue;
@@ -4457,7 +4683,7 @@ function createApi(deps) {
4457
4683
  if (found.has(filePath)) continue;
4458
4684
  try {
4459
4685
  loadState(filePath);
4460
- const rel = relative3(projectRoot, filePath);
4686
+ const rel = relative4(projectRoot, filePath);
4461
4687
  found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
4462
4688
  } catch {
4463
4689
  }
@@ -4536,7 +4762,7 @@ function createApi(deps) {
4536
4762
  for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
4537
4763
  const root = dirname3(resolve9(deps.statePath));
4538
4764
  const abs = resolve9(root, screenshot);
4539
- const rel = relative3(root, abs);
4765
+ const rel = relative4(root, abs);
4540
4766
  const seg0 = rel.split(sep2)[0] ?? "";
4541
4767
  if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
4542
4768
  try {
@@ -5002,7 +5228,7 @@ function createApi(deps) {
5002
5228
  raw
5003
5229
  });
5004
5230
  }
5005
- }, aiCfg.concurrency, signal);
5231
+ }, aiCfg.concurrency, signal, aiCfg.batchSize);
5006
5232
  if (!signal?.aborted) {
5007
5233
  console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
5008
5234
  await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
@@ -5045,7 +5271,7 @@ function createApi(deps) {
5045
5271
  raw
5046
5272
  });
5047
5273
  }
5048
- }, aiCfg.concurrency);
5274
+ }, aiCfg.concurrency, void 0, aiCfg.batchSize);
5049
5275
  const latest = load();
5050
5276
  ({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
5051
5277
  const entry = {
@@ -5230,7 +5456,7 @@ function createApi(deps) {
5230
5456
 
5231
5457
  // src/server/server.ts
5232
5458
  var here = dirname4(fileURLToPath(import.meta.url));
5233
- var DEFAULT_UI_DIR = join9(here, "..", "ui");
5459
+ var DEFAULT_UI_DIR = join10(here, "..", "ui");
5234
5460
  var MIME = {
5235
5461
  ".html": "text/html; charset=utf-8",
5236
5462
  ".js": "text/javascript; charset=utf-8",
@@ -5284,7 +5510,7 @@ function buildApp(opts) {
5284
5510
  const file = await readFileResponse(target);
5285
5511
  if (file) return file;
5286
5512
  }
5287
- const index = await readFileResponse(join9(root, "index.html"));
5513
+ const index = await readFileResponse(join10(root, "index.html"));
5288
5514
  if (index) return index;
5289
5515
  return c.notFound();
5290
5516
  });