glotfile 0.5.1 → 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();
@@ -3309,7 +3361,11 @@ var init_scanner = __esm({
3309
3361
  apple: [
3310
3362
  /NSLocalizedString\s*\(\s*@?"([^"]+)"/g,
3311
3363
  /String\s*\(\s*localized:\s*"([^"]+)"/g,
3312
- /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
3313
3369
  ]
3314
3370
  };
3315
3371
  PREFIX_PATTERNS = {
@@ -3327,7 +3383,7 @@ var init_scanner = __esm({
3327
3383
  /(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
3328
3384
  ]
3329
3385
  };
3330
- CACHE_VERSION = 4;
3386
+ CACHE_VERSION = 5;
3331
3387
  EXT_SCANNER = {
3332
3388
  ".php": "laravel",
3333
3389
  ".vue": "js-i18n",
@@ -3967,6 +4023,32 @@ function detectArb(root) {
3967
4023
  }
3968
4024
  return null;
3969
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
+ }
3970
4052
  function detect(root, formatOverride) {
3971
4053
  if (!existsSync10(root)) return null;
3972
4054
  if (formatOverride) {
@@ -3986,11 +4068,12 @@ var init_detect = __esm({
3986
4068
  "use strict";
3987
4069
  LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3988
4070
  VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
3989
- DETECTORS = [detectLaravel, detectVue, detectArb];
4071
+ DETECTORS = [detectLaravel, detectVue, detectArb, detectApple];
3990
4072
  BY_FORMAT = {
3991
4073
  "laravel-php": detectLaravel,
3992
4074
  "vue-i18n-json": (root) => detectVue(root, true),
3993
- "flutter-arb": detectArb
4075
+ "flutter-arb": detectArb,
4076
+ "apple-strings": detectApple
3994
4077
  };
3995
4078
  }
3996
4079
  });
@@ -4216,6 +4299,139 @@ var init_flutter_arb2 = __esm({
4216
4299
  }
4217
4300
  });
4218
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
+
4219
4435
  // src/server/import/parsers/index.ts
4220
4436
  function getParser(name) {
4221
4437
  const p = REGISTRY[name];
@@ -4229,10 +4445,12 @@ var init_parsers = __esm({
4229
4445
  init_vue_i18n_json2();
4230
4446
  init_laravel_php2();
4231
4447
  init_flutter_arb2();
4448
+ init_apple_strings2();
4232
4449
  REGISTRY = {
4233
4450
  [vueI18nJson2.name]: vueI18nJson2,
4234
4451
  [laravelPhp2.name]: laravelPhp2,
4235
- [flutterArb2.name]: flutterArb2
4452
+ [flutterArb2.name]: flutterArb2,
4453
+ [appleStrings2.name]: appleStrings2
4236
4454
  };
4237
4455
  }
4238
4456
  });
@@ -4242,10 +4460,13 @@ function assemble2(parsed, opts) {
4242
4460
  const warnings = [...parsed.warnings];
4243
4461
  const base = OUTPUT_BY_FORMAT[opts.format];
4244
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;
4245
4465
  const rawLocales = [.../* @__PURE__ */ new Set([opts.sourceLocale, ...parsed.locales])];
4246
4466
  const pairs = rawLocales.map((obs) => [canonLocale(obs), obs]);
4247
4467
  const inferred = inferLocaleStyle(pairs, getAdapter(base.adapter).defaultLocaleCase);
4248
- const output = { ...base, ...inferred };
4468
+ const { rootRelative: _rootRelative, ...baseOutput } = base;
4469
+ const output = { ...baseOutput, path, ...inferred };
4249
4470
  const sourceLocale = canonLocale(opts.sourceLocale);
4250
4471
  const locales = [...new Set(rawLocales.map(canonLocale))].sort();
4251
4472
  const keys = {};
@@ -4314,7 +4535,8 @@ var init_assemble = __esm({
4314
4535
  OUTPUT_BY_FORMAT = {
4315
4536
  "laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
4316
4537
  "vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
4317
- "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 }
4318
4540
  };
4319
4541
  }
4320
4542
  });
@@ -4325,6 +4547,7 @@ __export(run_exports, {
4325
4547
  previewImport: () => previewImport,
4326
4548
  runImport: () => runImport
4327
4549
  });
4550
+ import { relative as relative3 } from "path";
4328
4551
  function previewImport(projectRoot, format) {
4329
4552
  const det = detect(projectRoot, format);
4330
4553
  if (!det) return null;
@@ -4358,7 +4581,8 @@ function runImport(opts) {
4358
4581
  const assembled = assemble2(parsed, {
4359
4582
  sourceLocale: opts.sourceLocale ?? det.sourceLocale,
4360
4583
  format: det.format,
4361
- cldr: opts.cldr
4584
+ cldr: opts.cldr,
4585
+ localeRootRel: relative3(opts.projectRoot, det.localeRoot)
4362
4586
  });
4363
4587
  const { warnings, ...rest } = assembled;
4364
4588
  const state = validate(rest);
@@ -4734,12 +4958,12 @@ var init_checks = __esm({
4734
4958
  });
4735
4959
 
4736
4960
  // src/server/ui-prefs.ts
4737
- import { readFileSync as readFileSync13 } from "fs";
4961
+ import { readFileSync as readFileSync14 } from "fs";
4738
4962
  import { homedir } from "os";
4739
- import { join as join8 } from "path";
4963
+ import { join as join9 } from "path";
4740
4964
  function readJson2(path) {
4741
4965
  try {
4742
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
4966
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
4743
4967
  return parsed && typeof parsed === "object" ? parsed : {};
4744
4968
  } catch {
4745
4969
  return {};
@@ -4764,7 +4988,7 @@ var init_ui_prefs = __esm({
4764
4988
  THEMES = ["system", "light", "dark"];
4765
4989
  isThemeMode = (v) => THEMES.includes(v);
4766
4990
  isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4767
- defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
4991
+ defaultUiPrefsPath = () => join9(homedir(), ".glotfile", "ui.json");
4768
4992
  DEFAULTS = { theme: "system" };
4769
4993
  }
4770
4994
  });
@@ -4772,13 +4996,13 @@ var init_ui_prefs = __esm({
4772
4996
  // src/server/api.ts
4773
4997
  import { Hono } from "hono";
4774
4998
  import { streamSSE } from "hono/streaming";
4775
- import { readFileSync as readFileSync14, existsSync as existsSync11, readdirSync as readdirSync8, statSync as statSync5, rmSync as rmSync4 } from "fs";
4776
- 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";
4777
5001
  function projectName(root) {
4778
5002
  const nameFile = resolve9(root, ".idea", ".name");
4779
5003
  if (existsSync11(nameFile)) {
4780
5004
  try {
4781
- const name = readFileSync14(nameFile, "utf8").trim();
5005
+ const name = readFileSync15(nameFile, "utf8").trim();
4782
5006
  if (name) return name;
4783
5007
  } catch {
4784
5008
  }
@@ -4893,7 +5117,7 @@ function createApi(deps) {
4893
5117
  app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
4894
5118
  app.get("/files", (c) => {
4895
5119
  const found = /* @__PURE__ */ new Map();
4896
- const activeRel = relative3(projectRoot, deps.statePath);
5120
+ const activeRel = relative4(projectRoot, deps.statePath);
4897
5121
  found.set(deps.statePath, {
4898
5122
  name: basename(deps.statePath),
4899
5123
  path: deps.statePath,
@@ -4903,7 +5127,7 @@ function createApi(deps) {
4903
5127
  if (depth > 4) return;
4904
5128
  let entries = [];
4905
5129
  try {
4906
- entries = readdirSync8(dir);
5130
+ entries = readdirSync9(dir);
4907
5131
  } catch {
4908
5132
  return;
4909
5133
  }
@@ -4917,7 +5141,7 @@ function createApi(deps) {
4917
5141
  filePath = abs;
4918
5142
  } else {
4919
5143
  try {
4920
- if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
5144
+ if (statSync6(abs).isDirectory()) walk(abs, depth + 1);
4921
5145
  } catch {
4922
5146
  }
4923
5147
  continue;
@@ -4925,7 +5149,7 @@ function createApi(deps) {
4925
5149
  if (found.has(filePath)) continue;
4926
5150
  try {
4927
5151
  loadState(filePath);
4928
- const rel = relative3(projectRoot, filePath);
5152
+ const rel = relative4(projectRoot, filePath);
4929
5153
  found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
4930
5154
  } catch {
4931
5155
  }
@@ -5004,7 +5228,7 @@ function createApi(deps) {
5004
5228
  for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
5005
5229
  const root = dirname3(resolve9(deps.statePath));
5006
5230
  const abs = resolve9(root, screenshot);
5007
- const rel = relative3(root, abs);
5231
+ const rel = relative4(root, abs);
5008
5232
  const seg0 = rel.split(sep2)[0] ?? "";
5009
5233
  if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
5010
5234
  try {
@@ -5735,7 +5959,7 @@ __export(server_exports, {
5735
5959
  import { Hono as Hono2 } from "hono";
5736
5960
  import { serve } from "@hono/node-server";
5737
5961
  import { fileURLToPath } from "url";
5738
- 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";
5739
5963
  import { readFile, stat } from "fs/promises";
5740
5964
  import { createServer } from "net";
5741
5965
  import open from "open";
@@ -5778,7 +6002,7 @@ function buildApp(opts) {
5778
6002
  const file = await readFileResponse(target);
5779
6003
  if (file) return file;
5780
6004
  }
5781
- const index = await readFileResponse(join9(root, "index.html"));
6005
+ const index = await readFileResponse(join10(root, "index.html"));
5782
6006
  if (index) return index;
5783
6007
  return c.notFound();
5784
6008
  });
@@ -5836,7 +6060,7 @@ var init_server = __esm({
5836
6060
  init_scan();
5837
6061
  init_scanner();
5838
6062
  here = dirname4(fileURLToPath(import.meta.url));
5839
- DEFAULT_UI_DIR = join9(here, "..", "ui");
6063
+ DEFAULT_UI_DIR = join10(here, "..", "ui");
5840
6064
  MIME = {
5841
6065
  ".html": "text/html; charset=utf-8",
5842
6066
  ".js": "text/javascript; charset=utf-8",
@@ -5880,8 +6104,9 @@ init_scanner();
5880
6104
  init_context();
5881
6105
  init_run2();
5882
6106
  init_outputs();
5883
- import { resolve as resolve11, dirname as dirname5 } from "path";
5884
- 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";
5885
6110
 
5886
6111
  // src/server/lint/locate.ts
5887
6112
  function locate(rawText, key) {
@@ -5947,8 +6172,7 @@ function formatSarif(report, rawText) {
5947
6172
  }
5948
6173
 
5949
6174
  // src/server/cli.ts
5950
- import { fileURLToPath as fileURLToPath2 } from "url";
5951
- 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"];
5952
6176
  var isCommand = (s) => s != null && COMMANDS.includes(s);
5953
6177
  function parseArgs(argv) {
5954
6178
  const statePath = resolve11(process.cwd(), "glotfile.json");
@@ -6015,6 +6239,7 @@ function parseArgs(argv) {
6015
6239
  else if (flag === "--unused") args.unused = true;
6016
6240
  else if (flag === "--write") args.write = true;
6017
6241
  else if (flag === "--estimate") args.estimate = true;
6242
+ else if (flag === "--print") args.print = true;
6018
6243
  }
6019
6244
  return args;
6020
6245
  }
@@ -6188,7 +6413,7 @@ async function runLintCmd(args) {
6188
6413
  }
6189
6414
  return;
6190
6415
  }
6191
- const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
6416
+ const rawText = existsSync12(args.statePath) ? readFileSync16(args.statePath, "utf8") : "";
6192
6417
  const report = await runLint(state, {
6193
6418
  locales: args.locales,
6194
6419
  ruleIds: args.ruleIds,
@@ -6212,7 +6437,7 @@ async function runCheck(args) {
6212
6437
  process.exitCode = 1;
6213
6438
  return;
6214
6439
  }
6215
- const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
6440
+ const rawText = existsSync12(args.statePath) ? readFileSync16(args.statePath, "utf8") : "";
6216
6441
  const root = dirname5(resolve11(args.statePath));
6217
6442
  const lint = await runLint(state, {});
6218
6443
  const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
@@ -6373,6 +6598,22 @@ function runSplit(args) {
6373
6598
  `Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
6374
6599
  );
6375
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
+ }
6376
6617
  var GLOBAL_OPTS = [
6377
6618
  ["-f, --file <path>", "State file to use (default: ./glotfile.json)"],
6378
6619
  ["-h, --help", "Show this help"]
@@ -6458,6 +6699,14 @@ var COMMAND_HELP = {
6458
6699
  summary: "Convert glotfile.json into a glotfile/ directory of per-locale files (faster, reviewable git diffs).",
6459
6700
  usage: "glotfile split",
6460
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
+ ]
6461
6710
  }
6462
6711
  };
6463
6712
  function formatOpts(opts) {
@@ -6508,6 +6757,7 @@ async function main(argv) {
6508
6757
  if (args.command === "scan") return runScanCmd(args);
6509
6758
  if (args.command === "prune") return runPrune(args);
6510
6759
  if (args.command === "split") return runSplit(args);
6760
+ if (args.command === "skill") return runSkill(args);
6511
6761
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
6512
6762
  const { url } = await startServer2({ statePath: args.statePath, dev: args.dev });
6513
6763
  if (args.dev) console.log(`Glotfile dev API on ${url} \u2014 open the UI at the Vite "Local:" URL above`);
@@ -6523,5 +6773,6 @@ export {
6523
6773
  main,
6524
6774
  parseArgs,
6525
6775
  runPrune,
6776
+ runSkill,
6526
6777
  watchTargetFor
6527
6778
  };