glotfile 0.8.8 → 1.0.0

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.
@@ -852,6 +852,153 @@ var init_state = __esm({
852
852
  }
853
853
  });
854
854
 
855
+ // src/server/stats.ts
856
+ function countWords(text) {
857
+ const t = text.trim();
858
+ return t === "" ? 0 : t.split(/\s+/).length;
859
+ }
860
+ function namespaceOf(key) {
861
+ const i = key.indexOf(".");
862
+ return i === -1 ? "(root)" : key.slice(0, i);
863
+ }
864
+ function pct(n, d) {
865
+ return d === 0 ? 0 : Math.round(n / d * 1e3) / 10;
866
+ }
867
+ function sourceText(entry, sourceLocale) {
868
+ const lv = entry.values[sourceLocale];
869
+ if (!lv) return "";
870
+ return entry.plural ? lv.forms?.other ?? "" : lv.value ?? "";
871
+ }
872
+ function isPresent(entry, locale) {
873
+ const lv = entry.values[locale];
874
+ if (!lv) return false;
875
+ return entry.plural ? (lv.forms?.other ?? "").trim() !== "" : (lv.value ?? "").trim() !== "";
876
+ }
877
+ function classify(entry, locale) {
878
+ if (!isPresent(entry, locale)) return "missing";
879
+ const st = entry.values[locale].state;
880
+ if (st === "reviewed") return "reviewed";
881
+ if (st === "needs-review") return "needsReview";
882
+ return "machine";
883
+ }
884
+ function groupCompletion(state, keys, targets, name) {
885
+ let translated = 0;
886
+ let reviewed = 0;
887
+ for (const k of keys) {
888
+ const entry = state.keys[k];
889
+ for (const locale of targets) {
890
+ const bucket = classify(entry, locale);
891
+ if (bucket !== "missing") translated++;
892
+ if (bucket === "reviewed") reviewed++;
893
+ }
894
+ }
895
+ const cells = keys.length * targets.length;
896
+ return { name, total: keys.length, translatedPct: pct(translated, cells), reviewedPct: pct(reviewed, cells) };
897
+ }
898
+ function worstFirst(a, b) {
899
+ return a.translatedPct - b.translatedPct || a.name.localeCompare(b.name);
900
+ }
901
+ function computeStats(state) {
902
+ const { sourceLocale, locales } = state.config;
903
+ const targets = locales.filter((l) => l !== sourceLocale);
904
+ const allKeys = Object.keys(state.keys);
905
+ const expected = allKeys.filter((k) => !state.keys[k].skipTranslate);
906
+ const locales_ = targets.map((locale) => {
907
+ const counts = { reviewed: 0, needsReview: 0, machine: 0, missing: 0 };
908
+ let sourceWords = 0;
909
+ let missingWords = 0;
910
+ for (const k of expected) {
911
+ const entry = state.keys[k];
912
+ const w = countWords(sourceText(entry, sourceLocale));
913
+ sourceWords += w;
914
+ const bucket = classify(entry, locale);
915
+ counts[bucket]++;
916
+ if (bucket === "missing") missingWords += w;
917
+ }
918
+ const total = expected.length;
919
+ const translated = counts.reviewed + counts.needsReview + counts.machine;
920
+ return {
921
+ locale,
922
+ total,
923
+ counts,
924
+ translated,
925
+ reviewed: counts.reviewed,
926
+ translatedPct: pct(translated, total),
927
+ reviewedPct: pct(counts.reviewed, total),
928
+ words: { source: sourceWords, missing: missingWords }
929
+ };
930
+ });
931
+ const cells = expected.length * targets.length;
932
+ let translatedCells = 0;
933
+ let reviewedCells = 0;
934
+ for (const ls of locales_) {
935
+ translatedCells += ls.translated;
936
+ reviewedCells += ls.reviewed;
937
+ }
938
+ const nsMap = /* @__PURE__ */ new Map();
939
+ for (const k of expected) {
940
+ const ns = namespaceOf(k);
941
+ (nsMap.get(ns) ?? nsMap.set(ns, []).get(ns)).push(k);
942
+ }
943
+ const byNamespace = [...nsMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
944
+ const tagMap = /* @__PURE__ */ new Map();
945
+ for (const k of expected) {
946
+ for (const tag of state.keys[k].tags ?? []) {
947
+ (tagMap.get(tag) ?? tagMap.set(tag, []).get(tag)).push(k);
948
+ }
949
+ }
950
+ const byTag = [...tagMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
951
+ return {
952
+ totals: {
953
+ keys: allKeys.length,
954
+ locales: targets.length,
955
+ translatedPct: pct(translatedCells, cells),
956
+ reviewedPct: pct(reviewedCells, cells),
957
+ sourceWords: expected.reduce((sum, k) => sum + countWords(sourceText(state.keys[k], sourceLocale)), 0)
958
+ },
959
+ locales: locales_,
960
+ byNamespace,
961
+ byTag
962
+ };
963
+ }
964
+ var init_stats = __esm({
965
+ "src/server/stats.ts"() {
966
+ "use strict";
967
+ }
968
+ });
969
+
970
+ // src/server/cell-state.ts
971
+ function cellState(entry, locale, sourceLocale) {
972
+ const lv = entry.values[locale];
973
+ if (locale === sourceLocale) {
974
+ const has = entry.plural ? !!lv?.forms?.other?.trim() : !!lv?.value?.trim();
975
+ return has ? "source" : "missing";
976
+ }
977
+ const present = entry.plural ? categoriesFor(locale).every((c) => (lv?.forms?.[c] ?? "") !== "") : !!lv?.value;
978
+ if (!present) return "missing";
979
+ const st = lv.state;
980
+ return st === "reviewed" ? "reviewed" : st === "needs-review" ? "needs-review" : "machine";
981
+ }
982
+ var EFFECTIVE_STATES;
983
+ var init_cell_state = __esm({
984
+ "src/server/cell-state.ts"() {
985
+ "use strict";
986
+ init_plurals();
987
+ EFFECTIVE_STATES = ["source", "missing", "machine", "needs-review", "reviewed"];
988
+ }
989
+ });
990
+
991
+ // src/server/glob.ts
992
+ function globToRegExp(glob) {
993
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
994
+ return new RegExp(`^${escaped}$`);
995
+ }
996
+ var init_glob = __esm({
997
+ "src/server/glob.ts"() {
998
+ "use strict";
999
+ }
1000
+ });
1001
+
855
1002
  // src/server/adapters/options.ts
856
1003
  function applyCase(canonical, style) {
857
1004
  const sep4 = style === "lower-underscore" || style === "bcp47-underscore" ? "_" : "-";
@@ -3132,17 +3279,6 @@ var init_glossary = __esm({
3132
3279
  }
3133
3280
  });
3134
3281
 
3135
- // src/server/glob.ts
3136
- function globToRegExp(glob) {
3137
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3138
- return new RegExp(`^${escaped}$`);
3139
- }
3140
- var init_glob = __esm({
3141
- "src/server/glob.ts"() {
3142
- "use strict";
3143
- }
3144
- });
3145
-
3146
3282
  // src/server/ai/run.ts
3147
3283
  import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
3148
3284
  import { resolve as resolve4, extname } from "path";
@@ -3150,6 +3286,8 @@ function selectRequests(state, opts) {
3150
3286
  const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
3151
3287
  const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
3152
3288
  const keySet = opts.keys ? new Set(opts.keys) : null;
3289
+ const stateSet = opts.states ? new Set(opts.states) : null;
3290
+ const skip = (st) => stateSet ? !stateSet.has(st) : !!opts.onlyMissing && st !== "missing";
3153
3291
  const reqs = [];
3154
3292
  let id = 0;
3155
3293
  for (const key of Object.keys(state.keys).sort()) {
@@ -3163,9 +3301,7 @@ function selectRequests(state, opts) {
3163
3301
  const other = sourceForms?.other;
3164
3302
  if (!sourceForms || !other) continue;
3165
3303
  for (const locale of targets) {
3166
- const have = entry.values[locale]?.forms ?? {};
3167
- const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
3168
- if (opts.onlyMissing && complete) continue;
3304
+ if (skip(cellState(entry, locale, state.config.sourceLocale))) continue;
3169
3305
  const glossary = relevantGlossary(other, locale, state.glossary);
3170
3306
  const literals = quotedLiterals(other);
3171
3307
  reqs.push({
@@ -3187,8 +3323,7 @@ function selectRequests(state, opts) {
3187
3323
  const source = sourceLv?.value;
3188
3324
  if (!source) continue;
3189
3325
  for (const locale of targets) {
3190
- const existing = entry.values[locale]?.value;
3191
- if (opts.onlyMissing && existing) continue;
3326
+ if (skip(cellState(entry, locale, state.config.sourceLocale))) continue;
3192
3327
  const glossary = relevantGlossary(source, locale, state.glossary);
3193
3328
  const literals = quotedLiterals(source);
3194
3329
  reqs.push({
@@ -3349,6 +3484,7 @@ var init_run = __esm({
3349
3484
  init_placeholders();
3350
3485
  init_plurals();
3351
3486
  init_state();
3487
+ init_cell_state();
3352
3488
  init_glob();
3353
3489
  init_batch();
3354
3490
  MEDIA_TYPES = {
@@ -6601,121 +6737,6 @@ var init_run3 = __esm({
6601
6737
  }
6602
6738
  });
6603
6739
 
6604
- // src/server/stats.ts
6605
- function countWords(text) {
6606
- const t = text.trim();
6607
- return t === "" ? 0 : t.split(/\s+/).length;
6608
- }
6609
- function namespaceOf(key) {
6610
- const i = key.indexOf(".");
6611
- return i === -1 ? "(root)" : key.slice(0, i);
6612
- }
6613
- function pct(n, d) {
6614
- return d === 0 ? 0 : Math.round(n / d * 1e3) / 10;
6615
- }
6616
- function sourceText(entry, sourceLocale) {
6617
- const lv = entry.values[sourceLocale];
6618
- if (!lv) return "";
6619
- return entry.plural ? lv.forms?.other ?? "" : lv.value ?? "";
6620
- }
6621
- function isPresent(entry, locale) {
6622
- const lv = entry.values[locale];
6623
- if (!lv) return false;
6624
- return entry.plural ? (lv.forms?.other ?? "").trim() !== "" : (lv.value ?? "").trim() !== "";
6625
- }
6626
- function classify(entry, locale) {
6627
- if (!isPresent(entry, locale)) return "missing";
6628
- const st = entry.values[locale].state;
6629
- if (st === "reviewed") return "reviewed";
6630
- if (st === "needs-review") return "needsReview";
6631
- return "machine";
6632
- }
6633
- function groupCompletion(state, keys, targets, name) {
6634
- let translated = 0;
6635
- let reviewed = 0;
6636
- for (const k of keys) {
6637
- const entry = state.keys[k];
6638
- for (const locale of targets) {
6639
- const bucket = classify(entry, locale);
6640
- if (bucket !== "missing") translated++;
6641
- if (bucket === "reviewed") reviewed++;
6642
- }
6643
- }
6644
- const cells = keys.length * targets.length;
6645
- return { name, total: keys.length, translatedPct: pct(translated, cells), reviewedPct: pct(reviewed, cells) };
6646
- }
6647
- function worstFirst(a, b) {
6648
- return a.translatedPct - b.translatedPct || a.name.localeCompare(b.name);
6649
- }
6650
- function computeStats(state) {
6651
- const { sourceLocale, locales } = state.config;
6652
- const targets = locales.filter((l) => l !== sourceLocale);
6653
- const allKeys = Object.keys(state.keys);
6654
- const expected = allKeys.filter((k) => !state.keys[k].skipTranslate);
6655
- const locales_ = targets.map((locale) => {
6656
- const counts = { reviewed: 0, needsReview: 0, machine: 0, missing: 0 };
6657
- let sourceWords = 0;
6658
- let missingWords = 0;
6659
- for (const k of expected) {
6660
- const entry = state.keys[k];
6661
- const w = countWords(sourceText(entry, sourceLocale));
6662
- sourceWords += w;
6663
- const bucket = classify(entry, locale);
6664
- counts[bucket]++;
6665
- if (bucket === "missing") missingWords += w;
6666
- }
6667
- const total = expected.length;
6668
- const translated = counts.reviewed + counts.needsReview + counts.machine;
6669
- return {
6670
- locale,
6671
- total,
6672
- counts,
6673
- translated,
6674
- reviewed: counts.reviewed,
6675
- translatedPct: pct(translated, total),
6676
- reviewedPct: pct(counts.reviewed, total),
6677
- words: { source: sourceWords, missing: missingWords }
6678
- };
6679
- });
6680
- const cells = expected.length * targets.length;
6681
- let translatedCells = 0;
6682
- let reviewedCells = 0;
6683
- for (const ls of locales_) {
6684
- translatedCells += ls.translated;
6685
- reviewedCells += ls.reviewed;
6686
- }
6687
- const nsMap = /* @__PURE__ */ new Map();
6688
- for (const k of expected) {
6689
- const ns = namespaceOf(k);
6690
- (nsMap.get(ns) ?? nsMap.set(ns, []).get(ns)).push(k);
6691
- }
6692
- const byNamespace = [...nsMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
6693
- const tagMap = /* @__PURE__ */ new Map();
6694
- for (const k of expected) {
6695
- for (const tag of state.keys[k].tags ?? []) {
6696
- (tagMap.get(tag) ?? tagMap.set(tag, []).get(tag)).push(k);
6697
- }
6698
- }
6699
- const byTag = [...tagMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
6700
- return {
6701
- totals: {
6702
- keys: allKeys.length,
6703
- locales: targets.length,
6704
- translatedPct: pct(translatedCells, cells),
6705
- reviewedPct: pct(reviewedCells, cells),
6706
- sourceWords: expected.reduce((sum, k) => sum + countWords(sourceText(state.keys[k], sourceLocale)), 0)
6707
- },
6708
- locales: locales_,
6709
- byNamespace,
6710
- byTag
6711
- };
6712
- }
6713
- var init_stats = __esm({
6714
- "src/server/stats.ts"() {
6715
- "use strict";
6716
- }
6717
- });
6718
-
6719
6740
  // src/server/checks.ts
6720
6741
  function runChecks(state, opts = {}) {
6721
6742
  const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
@@ -8406,6 +8427,129 @@ var init_server = __esm({
8406
8427
 
8407
8428
  // src/server/cli.ts
8408
8429
  init_state();
8430
+ init_stats();
8431
+ import { resolve as resolve11, dirname as dirname5, join as join19, basename as basename2 } from "path";
8432
+ import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
8433
+ import { fileURLToPath as fileURLToPath2 } from "url";
8434
+
8435
+ // src/server/agent-cli.ts
8436
+ init_state();
8437
+ init_cell_state();
8438
+ init_glob();
8439
+ function projectCell(cell, fields) {
8440
+ const out = {};
8441
+ for (const f of fields) {
8442
+ if (f === "value" && cell.value !== void 0) out.value = cell.value;
8443
+ else if (f === "value" && cell.forms !== void 0) out.forms = cell.forms;
8444
+ else if (f === "state") out.state = cell.state;
8445
+ else if (f === "updatedAt" && cell.updatedAt !== void 0) out.updatedAt = cell.updatedAt;
8446
+ }
8447
+ return out;
8448
+ }
8449
+ function runGet(state, opts) {
8450
+ const { sourceLocale } = state.config;
8451
+ const shown = (opts.locales?.length ? opts.locales : state.config.locales).map(canonLocale);
8452
+ const targetsShown = shown.filter((l) => l !== sourceLocale);
8453
+ const res = opts.keyGlobs?.length ? opts.keyGlobs.map(globToRegExp) : null;
8454
+ const stateSet = opts.states?.length ? new Set(opts.states) : null;
8455
+ const fields = opts.fields?.length ? opts.fields : ["value", "state"];
8456
+ const fullEntry = fields.includes("all");
8457
+ const keys = [];
8458
+ const json = {};
8459
+ const ndjson = [];
8460
+ for (const key of Object.keys(state.keys).sort()) {
8461
+ if (res && !res.some((re) => re.test(key))) continue;
8462
+ const entry = state.keys[key];
8463
+ if (stateSet && !targetsShown.some((l) => stateSet.has(cellState(entry, l, sourceLocale)))) continue;
8464
+ keys.push(key);
8465
+ if (fullEntry) {
8466
+ const values = {};
8467
+ for (const l of shown) if (entry.values[l]) values[l] = entry.values[l];
8468
+ json[key] = { ...entry, values };
8469
+ ndjson.push({ key, ...entry, values });
8470
+ continue;
8471
+ }
8472
+ const cells = {};
8473
+ for (const locale of shown) {
8474
+ const st = cellState(entry, locale, sourceLocale);
8475
+ if (stateSet && locale !== sourceLocale && !stateSet.has(st)) continue;
8476
+ const lv = entry.values[locale];
8477
+ const cell = entry.plural ? { forms: lv?.forms ?? {}, state: st, updatedAt: lv?.updatedAt } : { value: lv?.value ?? "", state: st, updatedAt: lv?.updatedAt };
8478
+ const projected = projectCell(cell, fields);
8479
+ cells[locale] = projected;
8480
+ ndjson.push({ key, locale, ...projected });
8481
+ }
8482
+ json[key] = cells;
8483
+ }
8484
+ return { keys, json, ndjson };
8485
+ }
8486
+ function applyOne(state, op, clock) {
8487
+ switch (op.op) {
8488
+ case "create":
8489
+ createKey(state, op.key, op.value, clock);
8490
+ return;
8491
+ case "set-source":
8492
+ setSourceValue(state, op.key, op.value);
8493
+ return;
8494
+ case "set-target":
8495
+ setTargetValue(state, op.key, op.locale, op.value, clock);
8496
+ if (op.state && op.state !== "reviewed") setKeyState(state, op.key, op.locale, op.state);
8497
+ return;
8498
+ case "set-source-forms":
8499
+ setSourcePluralForms(state, op.key, op.forms);
8500
+ return;
8501
+ case "set-forms":
8502
+ setPluralForms(state, op.key, op.locale, op.forms, clock);
8503
+ if (op.state && op.state !== "reviewed") setKeyState(state, op.key, op.locale, op.state);
8504
+ return;
8505
+ case "set-state":
8506
+ setKeyState(state, op.key, op.locale, op.state);
8507
+ return;
8508
+ case "clear":
8509
+ clearValue(state, op.key, op.locale);
8510
+ return;
8511
+ default:
8512
+ throw new Error(`Unknown op: ${op.op ?? "(missing)"}`);
8513
+ }
8514
+ }
8515
+ function parseOps(raw) {
8516
+ let data;
8517
+ try {
8518
+ data = JSON.parse(raw);
8519
+ } catch (e) {
8520
+ throw new Error(`apply expects a JSON array of operations on stdin: ${e.message}`);
8521
+ }
8522
+ if (!Array.isArray(data)) throw new Error("apply expects a JSON array of operations on stdin");
8523
+ return data.map((o, i) => {
8524
+ if (!o || typeof o !== "object" || typeof o.op !== "string") {
8525
+ throw new Error(`operation ${i} is not an { "op": ... } object`);
8526
+ }
8527
+ return o;
8528
+ });
8529
+ }
8530
+ function applyOps(state, ops, opts = {}) {
8531
+ const clock = opts.clock ?? systemClock;
8532
+ const touched = /* @__PURE__ */ new Set();
8533
+ const errors = [];
8534
+ let applied = 0;
8535
+ for (let i = 0; i < ops.length; i++) {
8536
+ const op = ops[i];
8537
+ try {
8538
+ applyOne(state, op, clock);
8539
+ applied++;
8540
+ if (op.key) touched.add(op.key);
8541
+ } catch (e) {
8542
+ errors.push({ index: i, op: op.op, key: op.key, error: e instanceof Error ? e.message : String(e) });
8543
+ if (!opts.continueOnError) break;
8544
+ }
8545
+ }
8546
+ return { applied, keysTouched: [...touched].sort(), errors };
8547
+ }
8548
+
8549
+ // src/server/cli.ts
8550
+ init_cell_state();
8551
+ init_glob();
8552
+ init_schema();
8409
8553
  init_export_run();
8410
8554
  init_storage();
8411
8555
  init_ai();
@@ -8425,9 +8569,6 @@ init_usage();
8425
8569
  init_context();
8426
8570
  init_run2();
8427
8571
  init_outputs();
8428
- import { resolve as resolve11, dirname as dirname5, join as join19, basename as basename2 } from "path";
8429
- import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
8430
- import { fileURLToPath as fileURLToPath2 } from "url";
8431
8572
 
8432
8573
  // src/server/lint/locate.ts
8433
8574
  function locate(rawText, key) {
@@ -8503,7 +8644,7 @@ function formatSarif(report, ctx) {
8503
8644
  }
8504
8645
 
8505
8646
  // src/server/cli.ts
8506
- var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "scan", "prune", "split", "skill", "batch"];
8647
+ var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "scan", "prune", "split", "skill", "batch", "get", "stats", "set", "set-state", "clear", "apply"];
8507
8648
  var isCommand = (s) => s != null && COMMANDS.includes(s);
8508
8649
  function parseArgs(argv) {
8509
8650
  const statePath = resolve11(process.cwd(), "glotfile.json");
@@ -8579,9 +8720,21 @@ function parseArgs(argv) {
8579
8720
  else if (flag === "--batch") args.batch = true;
8580
8721
  else if (flag === "--wait") args.wait = true;
8581
8722
  else if (flag === "--print") args.print = true;
8723
+ else if (flag === "--state" && next) {
8724
+ args.states = next.split(",");
8725
+ i++;
8726
+ } else if (flag === "--fields" && next) {
8727
+ args.fields = next.split(",");
8728
+ i++;
8729
+ } else if (flag === "--keys-only") args.keysOnly = true;
8730
+ else if (flag === "--value" && next) {
8731
+ args.value = next;
8732
+ i++;
8733
+ } else if (flag === "--create") args.create = true;
8734
+ else if (flag === "--continue-on-error") args.continueOnError = true;
8582
8735
  else if (args.command === "batch" && (flag === "status" || flag === "apply" || flag === "cancel")) {
8583
8736
  args.batchAction = flag;
8584
- }
8737
+ } else if (!flag.startsWith("-")) (args.positionals ??= []).push(flag);
8585
8738
  }
8586
8739
  return args;
8587
8740
  }
@@ -8641,16 +8794,42 @@ function makeProviderOrExit(ai) {
8641
8794
  return null;
8642
8795
  }
8643
8796
  }
8797
+ function parseStates(args, allowSource) {
8798
+ if (!args.states?.length) return void 0;
8799
+ const allowed = allowSource ? EFFECTIVE_STATES : EFFECTIVE_STATES.filter((s) => s !== "source");
8800
+ for (const s of args.states) {
8801
+ if (!allowed.includes(s)) {
8802
+ console.error(`Unknown --state '${s}'. Expected one of: ${allowed.join(", ")}.`);
8803
+ process.exit(1);
8804
+ }
8805
+ }
8806
+ return args.states;
8807
+ }
8808
+ function translateSelection(args) {
8809
+ const states = parseStates(args, false);
8810
+ return {
8811
+ locales: args.locales,
8812
+ keyGlob: args.keyGlob,
8813
+ ...states ? { states } : { onlyMissing: args.all ? false : args.onlyMissing ?? true }
8814
+ };
8815
+ }
8816
+ function readStdin() {
8817
+ try {
8818
+ return readFileSync24(0, "utf8");
8819
+ } catch {
8820
+ return "";
8821
+ }
8822
+ }
8823
+ function matchKeys(state, glob) {
8824
+ const re = globToRegExp(glob);
8825
+ return Object.keys(state.keys).filter((k) => re.test(k)).sort();
8826
+ }
8644
8827
  async function runTranslate(args) {
8645
8828
  const state = loadState(args.statePath);
8646
8829
  const projectRoot = dirname5(resolve11(args.statePath));
8647
8830
  if (args.estimate) {
8648
8831
  const ai = loadLocalSettings(projectRoot).ai;
8649
- const est = estimateTranslation(state, ai, {
8650
- onlyMissing: args.all ? false : args.onlyMissing ?? true,
8651
- locales: args.locales,
8652
- keyGlob: args.keyGlob
8653
- });
8832
+ const est = estimateTranslation(state, ai, translateSelection(args));
8654
8833
  if (!est.requests) {
8655
8834
  console.log("Nothing to translate.");
8656
8835
  return;
@@ -8669,13 +8848,7 @@ async function runTranslate(args) {
8669
8848
  }
8670
8849
  return;
8671
8850
  }
8672
- const reqs = selectRequests(state, {
8673
- // Default to translating only empty values; --all forces a full re-translate
8674
- // (overwriting existing translations). --only missing stays as a no-op alias.
8675
- onlyMissing: args.all ? false : args.onlyMissing ?? true,
8676
- locales: args.locales,
8677
- keyGlob: args.keyGlob
8678
- });
8851
+ const reqs = selectRequests(state, translateSelection(args));
8679
8852
  const toTranslate = [...reqs];
8680
8853
  if (args.batch) {
8681
8854
  if (!toTranslate.length) {
@@ -9235,6 +9408,172 @@ function runSkill(args) {
9235
9408
  cpSync(SKILL_SRC, dest, { recursive: true });
9236
9409
  console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
9237
9410
  }
9411
+ function runGetCmd(args) {
9412
+ const state = loadState(args.statePath);
9413
+ const keyGlobs = [...args.positionals ?? [], ...args.keyGlob ? [args.keyGlob] : []];
9414
+ const out = runGet(state, {
9415
+ keyGlobs: keyGlobs.length ? keyGlobs : void 0,
9416
+ locales: args.locales,
9417
+ states: parseStates(args, true),
9418
+ fields: args.fields
9419
+ });
9420
+ if (args.keysOnly) {
9421
+ for (const k of out.keys) console.log(k);
9422
+ return;
9423
+ }
9424
+ if (args.format === "ndjson") {
9425
+ for (const row of out.ndjson) console.log(JSON.stringify(row));
9426
+ return;
9427
+ }
9428
+ console.log(JSON.stringify(out.json, null, 2));
9429
+ }
9430
+ function runStatsCmd(args) {
9431
+ const state = loadState(args.statePath);
9432
+ const stats = computeStats(state);
9433
+ let locales = stats.locales;
9434
+ if (args.locales?.length) {
9435
+ const want = new Set(args.locales.map(canonLocale));
9436
+ locales = locales.filter((l) => want.has(l.locale));
9437
+ }
9438
+ if (args.format === "text") {
9439
+ console.log(`${stats.totals.keys} key(s) \xB7 ${stats.totals.locales} target locale(s) \xB7 ${stats.totals.translatedPct}% translated, ${stats.totals.reviewedPct}% reviewed`);
9440
+ for (const l of locales) {
9441
+ const c = l.counts;
9442
+ console.log(` ${l.locale.padEnd(8)} ${String(l.translatedPct).padStart(5)}% translated (reviewed ${c.reviewed}, machine ${c.machine}, needs-review ${c.needsReview}, missing ${c.missing})`);
9443
+ }
9444
+ return;
9445
+ }
9446
+ console.log(JSON.stringify({ totals: stats.totals, locales }, null, 2));
9447
+ }
9448
+ function countNeedsReview(state, key) {
9449
+ const entry = state.keys[key];
9450
+ if (!entry) return 0;
9451
+ let n = 0;
9452
+ for (const [loc, lv] of Object.entries(entry.values)) {
9453
+ if (loc !== state.config.sourceLocale && lv.state === "needs-review") n++;
9454
+ }
9455
+ return n;
9456
+ }
9457
+ function runSet(args) {
9458
+ const pos = args.positionals ?? [];
9459
+ const key = pos[0];
9460
+ if (!key) {
9461
+ console.error("Usage: glotfile set <key> [value] [--locale <code>] [--state <state>] [--create]");
9462
+ process.exitCode = 1;
9463
+ return;
9464
+ }
9465
+ let value = args.value ?? pos[1];
9466
+ if (value === void 0) {
9467
+ const piped = readStdin();
9468
+ value = piped.length ? piped.replace(/\r?\n$/, "") : void 0;
9469
+ }
9470
+ if (value === void 0) {
9471
+ console.error('set requires a value (positional, --value, or piped on stdin). Use --value "" to set an empty value.');
9472
+ process.exitCode = 1;
9473
+ return;
9474
+ }
9475
+ const state = loadState(args.statePath);
9476
+ const sl = state.config.sourceLocale;
9477
+ const locale = args.locales?.[0] ? canonLocale(args.locales[0]) : sl;
9478
+ try {
9479
+ if (locale === sl) {
9480
+ if (args.create && !state.keys[key]) createKey(state, key, value);
9481
+ const before = countNeedsReview(state, key);
9482
+ setSourceValue(state, key, value);
9483
+ saveState(args.statePath, state);
9484
+ const flipped = countNeedsReview(state, key) - before;
9485
+ console.log(`set ${key} (${sl})${flipped > 0 ? ` \u2014 ${flipped} translation(s) now need re-translation (run \`glotfile translate --state needs-review\`)` : ""}`);
9486
+ } else {
9487
+ setTargetValue(state, key, locale, value);
9488
+ const override = args.states?.[0];
9489
+ if (override && override !== "reviewed") setKeyState(state, key, locale, override);
9490
+ saveState(args.statePath, state);
9491
+ console.log(`set ${key} (${locale})`);
9492
+ }
9493
+ } catch (e) {
9494
+ console.error(e.message);
9495
+ process.exitCode = 1;
9496
+ }
9497
+ }
9498
+ function runSetStateCmd(args) {
9499
+ const pos = args.positionals ?? [];
9500
+ const sel = args.keyGlob ?? pos[0];
9501
+ const stateName = args.keyGlob ? pos[0] : pos[1];
9502
+ if (!sel || !stateName) {
9503
+ console.error("Usage: glotfile set-state <key|glob> <state> [--locale <list>] (state: machine | needs-review | reviewed)");
9504
+ process.exitCode = 1;
9505
+ return;
9506
+ }
9507
+ if (!STATES.includes(stateName)) {
9508
+ console.error(`Unknown state '${stateName}'. Expected one of: ${STATES.join(", ")}.`);
9509
+ process.exitCode = 1;
9510
+ return;
9511
+ }
9512
+ const state = loadState(args.statePath);
9513
+ const sl = state.config.sourceLocale;
9514
+ const locales = (args.locales?.length ? args.locales : state.config.locales.filter((l) => l !== sl)).map(canonLocale);
9515
+ const keys = matchKeys(state, sel);
9516
+ let n = 0;
9517
+ for (const key of keys) {
9518
+ for (const loc of locales) {
9519
+ if (state.keys[key].values[loc]) {
9520
+ setKeyState(state, key, loc, stateName);
9521
+ n++;
9522
+ }
9523
+ }
9524
+ }
9525
+ saveState(args.statePath, state);
9526
+ console.log(`Set ${n} cell(s) to ${stateName} across ${keys.length} key(s).`);
9527
+ }
9528
+ function runClearCmd(args) {
9529
+ const sel = args.keyGlob ?? args.positionals?.[0];
9530
+ if (!sel) {
9531
+ console.error("Usage: glotfile clear <key|glob> --locale <list>");
9532
+ process.exitCode = 1;
9533
+ return;
9534
+ }
9535
+ if (!args.locales?.length) {
9536
+ console.error("clear requires --locale <list> (the locale(s) to empty).");
9537
+ process.exitCode = 1;
9538
+ return;
9539
+ }
9540
+ const state = loadState(args.statePath);
9541
+ const sl = state.config.sourceLocale;
9542
+ const locales = args.locales.map(canonLocale);
9543
+ if (locales.includes(sl)) {
9544
+ console.error(`Cannot clear the source locale (${sl}); edit it with \`glotfile set\` instead.`);
9545
+ process.exitCode = 1;
9546
+ return;
9547
+ }
9548
+ const keys = matchKeys(state, sel);
9549
+ let n = 0;
9550
+ for (const key of keys) {
9551
+ for (const loc of locales) {
9552
+ if (state.keys[key].values[loc]) {
9553
+ clearValue(state, key, loc);
9554
+ n++;
9555
+ }
9556
+ }
9557
+ }
9558
+ saveState(args.statePath, state);
9559
+ console.log(`Cleared ${n} value(s) \u2192 untranslated.`);
9560
+ }
9561
+ function runApply(args) {
9562
+ let ops;
9563
+ try {
9564
+ ops = parseOps(readStdin());
9565
+ } catch (e) {
9566
+ console.error(e.message);
9567
+ process.exitCode = 1;
9568
+ return;
9569
+ }
9570
+ const state = loadState(args.statePath);
9571
+ const r = applyOps(state, ops, { continueOnError: args.continueOnError });
9572
+ const saved = (args.continueOnError || r.errors.length === 0) && !args.dryRun;
9573
+ if (saved) saveState(args.statePath, state);
9574
+ console.log(JSON.stringify({ applied: r.applied, keysTouched: r.keysTouched, saved, dryRun: !!args.dryRun, errors: r.errors }, null, 2));
9575
+ if (r.errors.length) process.exitCode = 1;
9576
+ }
9238
9577
  var GLOBAL_OPTS = [
9239
9578
  ["-f, --file <path>", "State file to use (default: ./glotfile.json)"],
9240
9579
  ["-h, --help", "Show this help"]
@@ -9258,9 +9597,10 @@ var COMMAND_HELP = {
9258
9597
  },
9259
9598
  translate: {
9260
9599
  summary: "AI-translate missing strings into your target locales (writes back to the state file).",
9261
- usage: "glotfile translate [--all] [--estimate] [--locale <list>] [--key <glob>]",
9600
+ usage: "glotfile translate [--all] [--state <list>] [--estimate] [--locale <list>] [--key <glob>]",
9262
9601
  options: [
9263
9602
  ["--all", "Re-translate every string, not just empty values"],
9603
+ ["--state <list>", "Re-translate only targets in these states: missing|machine|needs-review|reviewed (e.g. needs-review = strings a source edit invalidated)"],
9264
9604
  ["--estimate", "Print batches, tokens and estimated cost without translating"],
9265
9605
  ["--locale <list>", "Comma-separated target locales (alias: --locales)"],
9266
9606
  ["--key <glob>", "Only keys matching this glob"],
@@ -9355,6 +9695,64 @@ var COMMAND_HELP = {
9355
9695
  ["apply", "Fetch results and write translations (auto-runs when finished)"],
9356
9696
  ["cancel", "Cancel the pending batch and discard the handle"]
9357
9697
  ]
9698
+ },
9699
+ get: {
9700
+ summary: "Extract values from the catalog (filtered) without loading the whole file. Prints JSON.",
9701
+ usage: "glotfile get [<key-glob>\u2026] [--key <glob>] [--locale <list>] [--state <list>] [--fields <list>] [--keys-only] [--format json|ndjson]",
9702
+ options: [
9703
+ ["<key-glob>\u2026", "Key globs to include (e.g. auth.*); positional, repeatable. Default: all keys"],
9704
+ ["--key <glob>", "Additional key glob (merged with positionals)"],
9705
+ ["--locale <list>", "Locales to show (default: all configured locales, source included)"],
9706
+ ["--state <list>", "Only keys whose shown target locales are in these states: source|missing|machine|needs-review|reviewed"],
9707
+ ["--fields <list>", "Cell fields to project: value,state,updatedAt (default value,state); 'all' = the full key entry"],
9708
+ ["--keys-only", "Print just the matched key names, one per line"],
9709
+ ["--format <fmt>", "json (default, nested) or ndjson (one row per cell)"]
9710
+ ]
9711
+ },
9712
+ stats: {
9713
+ summary: "Per-locale progress counts (translated / reviewed / machine / needs-review / missing).",
9714
+ usage: "glotfile stats [--locale <list>] [--format json|text]",
9715
+ options: [
9716
+ ["--locale <list>", "Restrict to these comma-separated locales"],
9717
+ ["--format <fmt>", "json (default) or text"]
9718
+ ]
9719
+ },
9720
+ set: {
9721
+ summary: "Set one value: the source string (default \u2014 flips downstream translations to needs-review) or a target (--locale).",
9722
+ usage: "glotfile set <key> [value] [--locale <code>] [--state <state>] [--create]",
9723
+ options: [
9724
+ ["<key> [value]", "Key, then the value (or pass --value, or pipe it on stdin)"],
9725
+ ["--locale <code>", "Set this target locale instead of the source"],
9726
+ ["--value <v>", "The value (alternative to the positional / stdin)"],
9727
+ ["--state <state>", "Resulting state for a target write (default reviewed): machine|needs-review|reviewed"],
9728
+ ["--create", "Create the key (scalar) if it does not exist yet"]
9729
+ ]
9730
+ },
9731
+ "set-state": {
9732
+ summary: "Flip the review state of one key \u2014 or many via a glob \u2014 across locales.",
9733
+ usage: "glotfile set-state <key|glob> <state> [--locale <list>]",
9734
+ options: [
9735
+ ["<key|glob> <state>", "Key/glob, then machine | needs-review | reviewed"],
9736
+ ["--key <glob>", "Glob selecting keys (alternative to the positional key)"],
9737
+ ["--locale <list>", "Locales to affect (default: every target locale)"]
9738
+ ]
9739
+ },
9740
+ clear: {
9741
+ summary: "Empty target value(s) so they read as untranslated (and get refilled by a plain translate).",
9742
+ usage: "glotfile clear <key|glob> --locale <list>",
9743
+ options: [
9744
+ ["<key|glob>", "Key or glob to clear"],
9745
+ ["--key <glob>", "Glob selecting keys (alternative to the positional key)"],
9746
+ ["--locale <list>", "Required: the locale(s) to empty (cannot be the source)"]
9747
+ ]
9748
+ },
9749
+ apply: {
9750
+ summary: "Apply a JSON batch of write operations from stdin in one load \u2192 save (atomic by default).",
9751
+ usage: "glotfile apply [--dry-run] [--continue-on-error] < ops.json",
9752
+ options: [
9753
+ ["--dry-run", "Report what would change without writing"],
9754
+ ["--continue-on-error", "Apply the survivors past a failing op instead of stopping (and saving nothing)"]
9755
+ ]
9358
9756
  }
9359
9757
  };
9360
9758
  function formatOpts(opts) {
@@ -9413,6 +9811,12 @@ async function main(argv) {
9413
9811
  if (args.command === "split") return runSplit(args);
9414
9812
  if (args.command === "skill") return runSkill(args);
9415
9813
  if (args.command === "batch") return runBatch(args);
9814
+ if (args.command === "get") return runGetCmd(args);
9815
+ if (args.command === "stats") return runStatsCmd(args);
9816
+ if (args.command === "set") return runSet(args);
9817
+ if (args.command === "set-state") return runSetStateCmd(args);
9818
+ if (args.command === "clear") return runClearCmd(args);
9819
+ if (args.command === "apply") return runApply(args);
9416
9820
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
9417
9821
  const { url } = await startServer2({ statePath: args.statePath, dev: args.dev, open: !args.noOpen });
9418
9822
  if (args.dev) console.log(`Glotfile dev API on ${url} \u2014 open the UI at the Vite "Local:" URL above`);