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.
@@ -1445,8 +1445,58 @@ var init_apple_stringsdict = __esm({
1445
1445
  }
1446
1446
  });
1447
1447
 
1448
+ // src/server/adapters/apple-strings.ts
1449
+ function escape(s) {
1450
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
1451
+ }
1452
+ var DEFAULT_LOCALE_CASE6, appleStrings;
1453
+ var init_apple_strings = __esm({
1454
+ "src/server/adapters/apple-strings.ts"() {
1455
+ "use strict";
1456
+ init_adapters();
1457
+ init_options();
1458
+ DEFAULT_LOCALE_CASE6 = "bcp47-hyphen";
1459
+ appleStrings = {
1460
+ name: "apple-strings",
1461
+ capabilities: {
1462
+ // Plurals belong in .stringsdict (apple-stringsdict), not the scalar table.
1463
+ plural: "none",
1464
+ select: "none",
1465
+ nesting: "flat",
1466
+ metadata: false,
1467
+ placeholderStyle: "printf",
1468
+ fileGrouping: "per-locale"
1469
+ },
1470
+ defaultLocaleCase: DEFAULT_LOCALE_CASE6,
1471
+ export(state, output) {
1472
+ const files = [];
1473
+ const warnings = [];
1474
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE6));
1475
+ const emptyAs = resolveEmptyAs(output, "source");
1476
+ const keys = Object.keys(state.keys).sort();
1477
+ for (const locale of state.config.locales) {
1478
+ const lines = [];
1479
+ for (const key of keys) {
1480
+ const entry = state.keys[key];
1481
+ if (entry.plural) continue;
1482
+ const value = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
1483
+ if (value === null) continue;
1484
+ lines.push(`"${escape(key)}" = "${escape(value)}";`);
1485
+ }
1486
+ const contents = lines.length ? lines.join("\n") + "\n" : "";
1487
+ files.push({
1488
+ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE6)),
1489
+ contents
1490
+ });
1491
+ }
1492
+ return { files, warnings };
1493
+ }
1494
+ };
1495
+ }
1496
+ });
1497
+
1448
1498
  // src/server/adapters/vue-i18n-json.ts
1449
- var DEFAULT_LOCALE_CASE6, vueI18nJson;
1499
+ var DEFAULT_LOCALE_CASE7, vueI18nJson;
1450
1500
  var init_vue_i18n_json = __esm({
1451
1501
  "src/server/adapters/vue-i18n-json.ts"() {
1452
1502
  "use strict";
@@ -1456,7 +1506,7 @@ var init_vue_i18n_json = __esm({
1456
1506
  init_format();
1457
1507
  init_placeholders();
1458
1508
  init_schema();
1459
- DEFAULT_LOCALE_CASE6 = "lower-hyphen";
1509
+ DEFAULT_LOCALE_CASE7 = "lower-hyphen";
1460
1510
  vueI18nJson = {
1461
1511
  name: "vue-i18n-json",
1462
1512
  capabilities: {
@@ -1467,11 +1517,11 @@ var init_vue_i18n_json = __esm({
1467
1517
  placeholderStyle: "named",
1468
1518
  fileGrouping: "per-locale"
1469
1519
  },
1470
- defaultLocaleCase: DEFAULT_LOCALE_CASE6,
1520
+ defaultLocaleCase: DEFAULT_LOCALE_CASE7,
1471
1521
  export(state, output) {
1472
1522
  const files = [];
1473
1523
  const warnings = [];
1474
- warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE6));
1524
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
1475
1525
  const { indent, finalNewline } = resolveFormat(state, output);
1476
1526
  const fmt = { indent, sortKeys: true, finalNewline };
1477
1527
  const emptyAs = resolveEmptyAs(output, "omit");
@@ -1511,7 +1561,7 @@ var init_vue_i18n_json = __esm({
1511
1561
  }
1512
1562
  payload = tree;
1513
1563
  }
1514
- files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE6)), contents: serializeJson(payload, fmt) });
1564
+ files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7)), contents: serializeJson(payload, fmt) });
1515
1565
  }
1516
1566
  files.sort((a, b) => a.path.localeCompare(b.path));
1517
1567
  return { files, warnings };
@@ -1557,7 +1607,7 @@ function renderEmbeddedIcu(value) {
1557
1607
  function renderScalar(value, ids) {
1558
1608
  return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
1559
1609
  }
1560
- var DEFAULT_LOCALE_CASE7, angularXliff;
1610
+ var DEFAULT_LOCALE_CASE8, angularXliff;
1561
1611
  var init_angular_xliff = __esm({
1562
1612
  "src/server/adapters/angular-xliff.ts"() {
1563
1613
  "use strict";
@@ -1565,7 +1615,7 @@ var init_angular_xliff = __esm({
1565
1615
  init_options();
1566
1616
  init_placeholders();
1567
1617
  init_schema();
1568
- DEFAULT_LOCALE_CASE7 = "bcp47-hyphen";
1618
+ DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
1569
1619
  angularXliff = {
1570
1620
  name: "angular-xliff",
1571
1621
  capabilities: {
@@ -1576,17 +1626,17 @@ var init_angular_xliff = __esm({
1576
1626
  placeholderStyle: "icu",
1577
1627
  fileGrouping: "per-locale"
1578
1628
  },
1579
- defaultLocaleCase: DEFAULT_LOCALE_CASE7,
1629
+ defaultLocaleCase: DEFAULT_LOCALE_CASE8,
1580
1630
  export(state, output) {
1581
1631
  const files = [];
1582
1632
  const warnings = [];
1583
- warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
1633
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
1584
1634
  const sourceLocale = state.config.sourceLocale;
1585
- const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE7);
1635
+ const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE8);
1586
1636
  const emptyAs = resolveEmptyAs(output, "source");
1587
1637
  const keys = Object.keys(state.keys).sort();
1588
1638
  for (const locale of state.config.locales) {
1589
- const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7);
1639
+ const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
1590
1640
  const units = [];
1591
1641
  for (const key of keys) {
1592
1642
  const entry = state.keys[key];
@@ -1650,7 +1700,7 @@ function yamlMap(node, indent, level) {
1650
1700
  }
1651
1701
  return lines;
1652
1702
  }
1653
- var RESERVED_KEYS, DEFAULT_LOCALE_CASE8, railsYaml;
1703
+ var RESERVED_KEYS, DEFAULT_LOCALE_CASE9, railsYaml;
1654
1704
  var init_rails_yaml = __esm({
1655
1705
  "src/server/adapters/rails-yaml.ts"() {
1656
1706
  "use strict";
@@ -1660,7 +1710,7 @@ var init_rails_yaml = __esm({
1660
1710
  init_placeholders();
1661
1711
  init_schema();
1662
1712
  RESERVED_KEYS = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "on", "off", "null", "y", "n"]);
1663
- DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
1713
+ DEFAULT_LOCALE_CASE9 = "bcp47-hyphen";
1664
1714
  railsYaml = {
1665
1715
  name: "rails-yaml",
1666
1716
  capabilities: {
@@ -1671,10 +1721,10 @@ var init_rails_yaml = __esm({
1671
1721
  placeholderStyle: "named",
1672
1722
  fileGrouping: "per-locale"
1673
1723
  },
1674
- defaultLocaleCase: DEFAULT_LOCALE_CASE8,
1724
+ defaultLocaleCase: DEFAULT_LOCALE_CASE9,
1675
1725
  export(state, output) {
1676
1726
  const warnings = [];
1677
- warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
1727
+ warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE9));
1678
1728
  const { indent, finalNewline } = resolveFormat(state, output);
1679
1729
  const emptyAs = resolveEmptyAs(output, "omit");
1680
1730
  const files = [];
@@ -1706,7 +1756,7 @@ var init_rails_yaml = __esm({
1706
1756
  for (const c of collisions) {
1707
1757
  warnings.push({ code: "key-collision", key: c, locale, message: "key is both a leaf and a parent; dropped from nested output" });
1708
1758
  }
1709
- const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
1759
+ const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE9);
1710
1760
  const body = [`${yamlKey(token)}:`, ...yamlMap(nested, indent, 1)].join("\n");
1711
1761
  files.push({ path: resolvePath(output.path, token), contents: finalNewline ? body + "\n" : body });
1712
1762
  }
@@ -1748,6 +1798,7 @@ function getRegistry() {
1748
1798
  [i18nextJson.name]: i18nextJson,
1749
1799
  [gettextPo.name]: gettextPo,
1750
1800
  [appleStringsdict.name]: appleStringsdict,
1801
+ [appleStrings.name]: appleStrings,
1751
1802
  [vueI18nJson.name]: vueI18nJson,
1752
1803
  [angularXliff.name]: angularXliff,
1753
1804
  [railsYaml.name]: railsYaml
@@ -1768,6 +1819,7 @@ var init_adapters = __esm({
1768
1819
  init_i18next_json();
1769
1820
  init_gettext_po();
1770
1821
  init_apple_stringsdict();
1822
+ init_apple_strings();
1771
1823
  init_vue_i18n_json();
1772
1824
  init_angular_xliff();
1773
1825
  init_rails_yaml();
@@ -2806,7 +2858,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
2806
2858
  const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
2807
2859
  return { skipped: keys.size };
2808
2860
  }
2809
- async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal) {
2861
+ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity) {
2810
2862
  if (!reqs.length) return [];
2811
2863
  const byLocale = /* @__PURE__ */ new Map();
2812
2864
  for (const req of reqs) {
@@ -2817,26 +2869,42 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
2817
2869
  }
2818
2870
  group.push(req);
2819
2871
  }
2872
+ const localeBatches = [...byLocale.entries()].map(([locale, group]) => ({
2873
+ locale,
2874
+ batches: chunk(group, Math.max(1, batchSize))
2875
+ }));
2876
+ const jobs = [];
2877
+ const maxBatches = Math.max(...localeBatches.map((g) => g.batches.length));
2878
+ for (let i = 0; i < maxBatches; i++) {
2879
+ for (const g of localeBatches) {
2880
+ if (i < g.batches.length) jobs.push({ locale: g.locale, batch: g.batches[i] });
2881
+ }
2882
+ }
2883
+ const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
2884
+ const started = /* @__PURE__ */ new Set();
2820
2885
  const total = reqs.length;
2821
2886
  let done = 0;
2822
2887
  const allResults = [];
2823
- const groups = [...byLocale.values()];
2824
2888
  let next = 0;
2825
2889
  async function worker() {
2826
- while (next < groups.length) {
2890
+ while (next < jobs.length) {
2827
2891
  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);
2837
- }
2838
- }
2839
- const workers = Array.from({ length: Math.min(concurrency, groups.length) }, worker);
2892
+ const { locale, batch } = jobs[next++];
2893
+ if (!started.has(locale)) {
2894
+ started.add(locale);
2895
+ hooks.onLocaleStart?.(locale);
2896
+ }
2897
+ const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
2898
+ done += results.length;
2899
+ hooks.onBatchComplete?.(done, total, results, locale);
2900
+ }, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
2901
+ allResults.push(...batchResults);
2902
+ const left = remaining.get(locale) - 1;
2903
+ remaining.set(locale, left);
2904
+ if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
2905
+ }
2906
+ }
2907
+ const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
2840
2908
  await Promise.all(workers);
2841
2909
  return allResults;
2842
2910
  }
@@ -2872,6 +2940,7 @@ var init_run = __esm({
2872
2940
  init_plurals();
2873
2941
  init_state();
2874
2942
  init_glob();
2943
+ init_batch();
2875
2944
  MEDIA_TYPES = {
2876
2945
  ".png": "image/png",
2877
2946
  ".jpg": "image/jpeg",
@@ -3292,7 +3361,11 @@ var init_scanner = __esm({
3292
3361
  apple: [
3293
3362
  /NSLocalizedString\s*\(\s*@?"([^"]+)"/g,
3294
3363
  /String\s*\(\s*localized:\s*"([^"]+)"/g,
3295
- /localizedString\s*\(\s*forKey:\s*"([^"]+)"/g
3364
+ /localizedString\s*\(\s*forKey:\s*"([^"]+)"/g,
3365
+ // The "key".localized / "key".localised String-extension idiom, where the
3366
+ // literal IS the key (common when keys are natural-language source text).
3367
+ /"([^"]+)"\s*\.\s*localized\b/g,
3368
+ /"([^"]+)"\s*\.\s*localised\b/g
3296
3369
  ]
3297
3370
  };
3298
3371
  PREFIX_PATTERNS = {
@@ -3310,7 +3383,7 @@ var init_scanner = __esm({
3310
3383
  /(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
3311
3384
  ]
3312
3385
  };
3313
- CACHE_VERSION = 4;
3386
+ CACHE_VERSION = 5;
3314
3387
  EXT_SCANNER = {
3315
3388
  ".php": "laravel",
3316
3389
  ".vue": "js-i18n",
@@ -3950,6 +4023,32 @@ function detectArb(root) {
3950
4023
  }
3951
4024
  return null;
3952
4025
  }
4026
+ function lprojLocales(dir) {
4027
+ return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync10(join4(dir, `${l}.lproj`, "Localizable.strings")));
4028
+ }
4029
+ function detectApple(root) {
4030
+ const candidates = [root, ...listDirs(root).map((d) => join4(root, d))];
4031
+ let best = null;
4032
+ for (const dir of candidates) {
4033
+ const locales = lprojLocales(dir);
4034
+ if (locales.length === 0) continue;
4035
+ if (!best || locales.length > best.locales.length) {
4036
+ best = {
4037
+ format: "apple-strings",
4038
+ localeRoot: dir,
4039
+ locales,
4040
+ sourceLocale: pickSource(locales, (loc) => {
4041
+ try {
4042
+ return statSync3(join4(dir, `${loc}.lproj`, "Localizable.strings")).size;
4043
+ } catch {
4044
+ return 0;
4045
+ }
4046
+ })
4047
+ };
4048
+ }
4049
+ }
4050
+ return best;
4051
+ }
3953
4052
  function detect(root, formatOverride) {
3954
4053
  if (!existsSync10(root)) return null;
3955
4054
  if (formatOverride) {
@@ -3969,11 +4068,12 @@ var init_detect = __esm({
3969
4068
  "use strict";
3970
4069
  LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3971
4070
  VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
3972
- DETECTORS = [detectLaravel, detectVue, detectArb];
4071
+ DETECTORS = [detectLaravel, detectVue, detectArb, detectApple];
3973
4072
  BY_FORMAT = {
3974
4073
  "laravel-php": detectLaravel,
3975
4074
  "vue-i18n-json": (root) => detectVue(root, true),
3976
- "flutter-arb": detectArb
4075
+ "flutter-arb": detectArb,
4076
+ "apple-strings": detectApple
3977
4077
  };
3978
4078
  }
3979
4079
  });
@@ -4199,6 +4299,139 @@ var init_flutter_arb2 = __esm({
4199
4299
  }
4200
4300
  });
4201
4301
 
4302
+ // src/server/import/parsers/apple-strings.ts
4303
+ import { readdirSync as readdirSync8, readFileSync as readFileSync13, statSync as statSync5 } from "fs";
4304
+ import { join as join8 } from "path";
4305
+ function localeFromLproj(dir) {
4306
+ const m = dir.match(/^(.+)\.lproj$/);
4307
+ if (!m) return null;
4308
+ return LOCALE_RE4.test(m[1]) ? m[1] : null;
4309
+ }
4310
+ function unescape(body) {
4311
+ return body.replace(/\\(U[0-9a-fA-F]{4}|u[0-9a-fA-F]{4}|.)/g, (_m, esc) => {
4312
+ const c = esc[0];
4313
+ if (c === "U" || c === "u") return String.fromCharCode(parseInt(esc.slice(1), 16));
4314
+ if (c === "n") return "\n";
4315
+ if (c === "t") return " ";
4316
+ if (c === "r") return "\r";
4317
+ return esc;
4318
+ });
4319
+ }
4320
+ function parseStrings(text, file, warnings) {
4321
+ const pairs = [];
4322
+ let i = 0;
4323
+ const n = text.length;
4324
+ const skipTrivia = () => {
4325
+ while (i < n) {
4326
+ const c = text[i];
4327
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
4328
+ i++;
4329
+ continue;
4330
+ }
4331
+ if (c === "/" && text[i + 1] === "/") {
4332
+ i += 2;
4333
+ while (i < n && text[i] !== "\n") i++;
4334
+ continue;
4335
+ }
4336
+ if (c === "/" && text[i + 1] === "*") {
4337
+ i += 2;
4338
+ while (i < n && !(text[i] === "*" && text[i + 1] === "/")) i++;
4339
+ i += 2;
4340
+ continue;
4341
+ }
4342
+ break;
4343
+ }
4344
+ };
4345
+ const readToken = () => {
4346
+ if (i >= n) return null;
4347
+ if (text[i] === '"') {
4348
+ i++;
4349
+ let raw2 = "";
4350
+ while (i < n) {
4351
+ const c = text[i];
4352
+ if (c === "\\") {
4353
+ raw2 += c + (text[i + 1] ?? "");
4354
+ i += 2;
4355
+ continue;
4356
+ }
4357
+ if (c === '"') {
4358
+ i++;
4359
+ return unescape(raw2);
4360
+ }
4361
+ raw2 += c;
4362
+ i++;
4363
+ }
4364
+ return null;
4365
+ }
4366
+ let raw = "";
4367
+ while (i < n && !/[\s=;]/.test(text[i])) raw += text[i++];
4368
+ return raw.length ? raw : null;
4369
+ };
4370
+ while (true) {
4371
+ skipTrivia();
4372
+ if (i >= n) break;
4373
+ const key = readToken();
4374
+ if (key === null) {
4375
+ warnings.push(`apple-strings: malformed entry in ${file} near offset ${i}`);
4376
+ break;
4377
+ }
4378
+ skipTrivia();
4379
+ if (text[i] !== "=") {
4380
+ warnings.push(`apple-strings: expected '=' after key "${key}" in ${file}`);
4381
+ break;
4382
+ }
4383
+ i++;
4384
+ skipTrivia();
4385
+ const value = readToken();
4386
+ if (value === null) {
4387
+ warnings.push(`apple-strings: missing value for key "${key}" in ${file}`);
4388
+ break;
4389
+ }
4390
+ skipTrivia();
4391
+ if (text[i] === ";") i++;
4392
+ pairs.push({ key, value });
4393
+ }
4394
+ return pairs;
4395
+ }
4396
+ var LOCALE_RE4, TABLE, appleStrings2;
4397
+ var init_apple_strings2 = __esm({
4398
+ "src/server/import/parsers/apple-strings.ts"() {
4399
+ "use strict";
4400
+ LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4401
+ TABLE = "Localizable.strings";
4402
+ appleStrings2 = {
4403
+ name: "apple-strings",
4404
+ parse(localeRoot, opts) {
4405
+ const warnings = [];
4406
+ const keys = {};
4407
+ const locales = [];
4408
+ for (const dir of readdirSync8(localeRoot).sort()) {
4409
+ const locale = localeFromLproj(dir);
4410
+ if (!locale) continue;
4411
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4412
+ const file = join8(localeRoot, dir, TABLE);
4413
+ let text;
4414
+ try {
4415
+ if (!statSync5(file).isFile()) continue;
4416
+ text = readFileSync13(file, "utf8");
4417
+ } catch {
4418
+ continue;
4419
+ }
4420
+ locales.push(locale);
4421
+ const others = readdirSync8(join8(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
4422
+ if (others.length) {
4423
+ warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
4424
+ }
4425
+ for (const { key, value } of parseStrings(text, file, warnings)) {
4426
+ (keys[key] ??= { values: {} }).values[locale] = value;
4427
+ }
4428
+ }
4429
+ return { locales, keys, warnings };
4430
+ }
4431
+ };
4432
+ }
4433
+ });
4434
+
4202
4435
  // src/server/import/parsers/index.ts
4203
4436
  function getParser(name) {
4204
4437
  const p = REGISTRY[name];
@@ -4212,10 +4445,12 @@ var init_parsers = __esm({
4212
4445
  init_vue_i18n_json2();
4213
4446
  init_laravel_php2();
4214
4447
  init_flutter_arb2();
4448
+ init_apple_strings2();
4215
4449
  REGISTRY = {
4216
4450
  [vueI18nJson2.name]: vueI18nJson2,
4217
4451
  [laravelPhp2.name]: laravelPhp2,
4218
- [flutterArb2.name]: flutterArb2
4452
+ [flutterArb2.name]: flutterArb2,
4453
+ [appleStrings2.name]: appleStrings2
4219
4454
  };
4220
4455
  }
4221
4456
  });
@@ -4225,10 +4460,13 @@ function assemble2(parsed, opts) {
4225
4460
  const warnings = [...parsed.warnings];
4226
4461
  const base = OUTPUT_BY_FORMAT[opts.format];
4227
4462
  if (!base) throw new Error(`No output mapping for format "${opts.format}"`);
4463
+ const prefix = (opts.localeRootRel ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
4464
+ const path = base.rootRelative && prefix ? `${prefix}/${base.path}` : base.path;
4228
4465
  const rawLocales = [.../* @__PURE__ */ new Set([opts.sourceLocale, ...parsed.locales])];
4229
4466
  const pairs = rawLocales.map((obs) => [canonLocale(obs), obs]);
4230
4467
  const inferred = inferLocaleStyle(pairs, getAdapter(base.adapter).defaultLocaleCase);
4231
- const output = { ...base, ...inferred };
4468
+ const { rootRelative: _rootRelative, ...baseOutput } = base;
4469
+ const output = { ...baseOutput, path, ...inferred };
4232
4470
  const sourceLocale = canonLocale(opts.sourceLocale);
4233
4471
  const locales = [...new Set(rawLocales.map(canonLocale))].sort();
4234
4472
  const keys = {};
@@ -4297,7 +4535,8 @@ var init_assemble = __esm({
4297
4535
  OUTPUT_BY_FORMAT = {
4298
4536
  "laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
4299
4537
  "vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
4300
- "flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" }
4538
+ "flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
4539
+ "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true }
4301
4540
  };
4302
4541
  }
4303
4542
  });
@@ -4308,6 +4547,7 @@ __export(run_exports, {
4308
4547
  previewImport: () => previewImport,
4309
4548
  runImport: () => runImport
4310
4549
  });
4550
+ import { relative as relative3 } from "path";
4311
4551
  function previewImport(projectRoot, format) {
4312
4552
  const det = detect(projectRoot, format);
4313
4553
  if (!det) return null;
@@ -4341,7 +4581,8 @@ function runImport(opts) {
4341
4581
  const assembled = assemble2(parsed, {
4342
4582
  sourceLocale: opts.sourceLocale ?? det.sourceLocale,
4343
4583
  format: det.format,
4344
- cldr: opts.cldr
4584
+ cldr: opts.cldr,
4585
+ localeRootRel: relative3(opts.projectRoot, det.localeRoot)
4345
4586
  });
4346
4587
  const { warnings, ...rest } = assembled;
4347
4588
  const state = validate(rest);
@@ -4717,12 +4958,12 @@ var init_checks = __esm({
4717
4958
  });
4718
4959
 
4719
4960
  // src/server/ui-prefs.ts
4720
- import { readFileSync as readFileSync13 } from "fs";
4961
+ import { readFileSync as readFileSync14 } from "fs";
4721
4962
  import { homedir } from "os";
4722
- import { join as join8 } from "path";
4963
+ import { join as join9 } from "path";
4723
4964
  function readJson2(path) {
4724
4965
  try {
4725
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
4966
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
4726
4967
  return parsed && typeof parsed === "object" ? parsed : {};
4727
4968
  } catch {
4728
4969
  return {};
@@ -4747,7 +4988,7 @@ var init_ui_prefs = __esm({
4747
4988
  THEMES = ["system", "light", "dark"];
4748
4989
  isThemeMode = (v) => THEMES.includes(v);
4749
4990
  isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4750
- defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
4991
+ defaultUiPrefsPath = () => join9(homedir(), ".glotfile", "ui.json");
4751
4992
  DEFAULTS = { theme: "system" };
4752
4993
  }
4753
4994
  });
@@ -4755,13 +4996,13 @@ var init_ui_prefs = __esm({
4755
4996
  // src/server/api.ts
4756
4997
  import { Hono } from "hono";
4757
4998
  import { streamSSE } from "hono/streaming";
4758
- import { readFileSync as readFileSync14, existsSync as existsSync11, readdirSync as readdirSync8, statSync as statSync5, rmSync as rmSync4 } from "fs";
4759
- import { dirname as dirname3, resolve as resolve9, basename, relative as relative3, sep as sep2 } from "path";
4999
+ import { readFileSync as readFileSync15, existsSync as existsSync11, readdirSync as readdirSync9, statSync as statSync6, rmSync as rmSync4 } from "fs";
5000
+ import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
4760
5001
  function projectName(root) {
4761
5002
  const nameFile = resolve9(root, ".idea", ".name");
4762
5003
  if (existsSync11(nameFile)) {
4763
5004
  try {
4764
- const name = readFileSync14(nameFile, "utf8").trim();
5005
+ const name = readFileSync15(nameFile, "utf8").trim();
4765
5006
  if (name) return name;
4766
5007
  } catch {
4767
5008
  }
@@ -4876,7 +5117,7 @@ function createApi(deps) {
4876
5117
  app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
4877
5118
  app.get("/files", (c) => {
4878
5119
  const found = /* @__PURE__ */ new Map();
4879
- const activeRel = relative3(projectRoot, deps.statePath);
5120
+ const activeRel = relative4(projectRoot, deps.statePath);
4880
5121
  found.set(deps.statePath, {
4881
5122
  name: basename(deps.statePath),
4882
5123
  path: deps.statePath,
@@ -4886,7 +5127,7 @@ function createApi(deps) {
4886
5127
  if (depth > 4) return;
4887
5128
  let entries = [];
4888
5129
  try {
4889
- entries = readdirSync8(dir);
5130
+ entries = readdirSync9(dir);
4890
5131
  } catch {
4891
5132
  return;
4892
5133
  }
@@ -4900,7 +5141,7 @@ function createApi(deps) {
4900
5141
  filePath = abs;
4901
5142
  } else {
4902
5143
  try {
4903
- if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
5144
+ if (statSync6(abs).isDirectory()) walk(abs, depth + 1);
4904
5145
  } catch {
4905
5146
  }
4906
5147
  continue;
@@ -4908,7 +5149,7 @@ function createApi(deps) {
4908
5149
  if (found.has(filePath)) continue;
4909
5150
  try {
4910
5151
  loadState(filePath);
4911
- const rel = relative3(projectRoot, filePath);
5152
+ const rel = relative4(projectRoot, filePath);
4912
5153
  found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
4913
5154
  } catch {
4914
5155
  }
@@ -4987,7 +5228,7 @@ function createApi(deps) {
4987
5228
  for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
4988
5229
  const root = dirname3(resolve9(deps.statePath));
4989
5230
  const abs = resolve9(root, screenshot);
4990
- const rel = relative3(root, abs);
5231
+ const rel = relative4(root, abs);
4991
5232
  const seg0 = rel.split(sep2)[0] ?? "";
4992
5233
  if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
4993
5234
  try {
@@ -5453,7 +5694,7 @@ function createApi(deps) {
5453
5694
  raw
5454
5695
  });
5455
5696
  }
5456
- }, aiCfg.concurrency, signal);
5697
+ }, aiCfg.concurrency, signal, aiCfg.batchSize);
5457
5698
  if (!signal?.aborted) {
5458
5699
  console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
5459
5700
  await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
@@ -5496,7 +5737,7 @@ function createApi(deps) {
5496
5737
  raw
5497
5738
  });
5498
5739
  }
5499
- }, aiCfg.concurrency);
5740
+ }, aiCfg.concurrency, void 0, aiCfg.batchSize);
5500
5741
  const latest = load();
5501
5742
  ({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
5502
5743
  const entry = {
@@ -5718,7 +5959,7 @@ __export(server_exports, {
5718
5959
  import { Hono as Hono2 } from "hono";
5719
5960
  import { serve } from "@hono/node-server";
5720
5961
  import { fileURLToPath } from "url";
5721
- import { dirname as dirname4, join as join9, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
5962
+ import { dirname as dirname4, join as join10, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
5722
5963
  import { readFile, stat } from "fs/promises";
5723
5964
  import { createServer } from "net";
5724
5965
  import open from "open";
@@ -5761,7 +6002,7 @@ function buildApp(opts) {
5761
6002
  const file = await readFileResponse(target);
5762
6003
  if (file) return file;
5763
6004
  }
5764
- const index = await readFileResponse(join9(root, "index.html"));
6005
+ const index = await readFileResponse(join10(root, "index.html"));
5765
6006
  if (index) return index;
5766
6007
  return c.notFound();
5767
6008
  });
@@ -5819,7 +6060,7 @@ var init_server = __esm({
5819
6060
  init_scan();
5820
6061
  init_scanner();
5821
6062
  here = dirname4(fileURLToPath(import.meta.url));
5822
- DEFAULT_UI_DIR = join9(here, "..", "ui");
6063
+ DEFAULT_UI_DIR = join10(here, "..", "ui");
5823
6064
  MIME = {
5824
6065
  ".html": "text/html; charset=utf-8",
5825
6066
  ".js": "text/javascript; charset=utf-8",
@@ -5863,8 +6104,9 @@ init_scanner();
5863
6104
  init_context();
5864
6105
  init_run2();
5865
6106
  init_outputs();
5866
- import { resolve as resolve11, dirname as dirname5 } from "path";
5867
- import { readFileSync as readFileSync15, existsSync as existsSync12 } from "fs";
6107
+ import { resolve as resolve11, dirname as dirname5, join as join11 } from "path";
6108
+ import { readFileSync as readFileSync16, existsSync as existsSync12, mkdirSync as mkdirSync4, cpSync } from "fs";
6109
+ import { fileURLToPath as fileURLToPath2 } from "url";
5868
6110
 
5869
6111
  // src/server/lint/locate.ts
5870
6112
  function locate(rawText, key) {
@@ -5930,8 +6172,7 @@ function formatSarif(report, rawText) {
5930
6172
  }
5931
6173
 
5932
6174
  // src/server/cli.ts
5933
- import { fileURLToPath as fileURLToPath2 } from "url";
5934
- var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "build-context", "scan", "prune", "split"];
6175
+ var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "build-context", "scan", "prune", "split", "skill"];
5935
6176
  var isCommand = (s) => s != null && COMMANDS.includes(s);
5936
6177
  function parseArgs(argv) {
5937
6178
  const statePath = resolve11(process.cwd(), "glotfile.json");
@@ -5998,6 +6239,7 @@ function parseArgs(argv) {
5998
6239
  else if (flag === "--unused") args.unused = true;
5999
6240
  else if (flag === "--write") args.write = true;
6000
6241
  else if (flag === "--estimate") args.estimate = true;
6242
+ else if (flag === "--print") args.print = true;
6001
6243
  }
6002
6244
  return args;
6003
6245
  }
@@ -6123,7 +6365,7 @@ async function runTranslate(args) {
6123
6365
  raw
6124
6366
  });
6125
6367
  }
6126
- });
6368
+ }, ai.concurrency, void 0, ai.batchSize);
6127
6369
  process.stdout.write("\n");
6128
6370
  if (!batchCallbackFired) {
6129
6371
  ({ written, errors } = applyResults(state, toTranslate, results));
@@ -6171,7 +6413,7 @@ async function runLintCmd(args) {
6171
6413
  }
6172
6414
  return;
6173
6415
  }
6174
- const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
6416
+ const rawText = existsSync12(args.statePath) ? readFileSync16(args.statePath, "utf8") : "";
6175
6417
  const report = await runLint(state, {
6176
6418
  locales: args.locales,
6177
6419
  ruleIds: args.ruleIds,
@@ -6195,7 +6437,7 @@ async function runCheck(args) {
6195
6437
  process.exitCode = 1;
6196
6438
  return;
6197
6439
  }
6198
- const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
6440
+ const rawText = existsSync12(args.statePath) ? readFileSync16(args.statePath, "utf8") : "";
6199
6441
  const root = dirname5(resolve11(args.statePath));
6200
6442
  const lint = await runLint(state, {});
6201
6443
  const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
@@ -6356,6 +6598,22 @@ function runSplit(args) {
6356
6598
  `Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
6357
6599
  );
6358
6600
  }
6601
+ var SKILL_SRC = join11(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
6602
+ function runSkill(args) {
6603
+ if (args.print) {
6604
+ console.log(readFileSync16(join11(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
6605
+ return;
6606
+ }
6607
+ const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
6608
+ if (existsSync12(dest) && !args.importForce) {
6609
+ console.error(`${dest} already exists; pass --force to overwrite`);
6610
+ process.exitCode = 1;
6611
+ return;
6612
+ }
6613
+ mkdirSync4(dirname5(dest), { recursive: true });
6614
+ cpSync(SKILL_SRC, dest, { recursive: true });
6615
+ console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
6616
+ }
6359
6617
  var GLOBAL_OPTS = [
6360
6618
  ["-f, --file <path>", "State file to use (default: ./glotfile.json)"],
6361
6619
  ["-h, --help", "Show this help"]
@@ -6441,6 +6699,14 @@ var COMMAND_HELP = {
6441
6699
  summary: "Convert glotfile.json into a glotfile/ directory of per-locale files (faster, reviewable git diffs).",
6442
6700
  usage: "glotfile split",
6443
6701
  options: []
6702
+ },
6703
+ skill: {
6704
+ summary: "Install the Claude Code skill for managing glotfile into ./.claude/skills/glotfile/.",
6705
+ usage: "glotfile skill [--print] [--force]",
6706
+ options: [
6707
+ ["--print", "Write SKILL.md to stdout instead of installing"],
6708
+ ["--force", "Overwrite an existing installed skill"]
6709
+ ]
6444
6710
  }
6445
6711
  };
6446
6712
  function formatOpts(opts) {
@@ -6491,6 +6757,7 @@ async function main(argv) {
6491
6757
  if (args.command === "scan") return runScanCmd(args);
6492
6758
  if (args.command === "prune") return runPrune(args);
6493
6759
  if (args.command === "split") return runSplit(args);
6760
+ if (args.command === "skill") return runSkill(args);
6494
6761
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
6495
6762
  const { url } = await startServer2({ statePath: args.statePath, dev: args.dev });
6496
6763
  if (args.dev) console.log(`Glotfile dev API on ${url} \u2014 open the UI at the Vite "Local:" URL above`);
@@ -6506,5 +6773,6 @@ export {
6506
6773
  main,
6507
6774
  parseArgs,
6508
6775
  runPrune,
6776
+ runSkill,
6509
6777
  watchTargetFor
6510
6778
  };