glotfile 0.7.5 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4788,6 +4788,23 @@ function parseAttrs(s) {
4788
4788
  for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
4789
4789
  return out;
4790
4790
  }
4791
+ function decodeLocations(body) {
4792
+ const out = [];
4793
+ const seen = /* @__PURE__ */ new Set();
4794
+ for (const g of body.matchAll(/<context-group\b[^>]*\bpurpose="location"[^>]*>([\s\S]*?)<\/context-group>/g)) {
4795
+ const inner = g[1];
4796
+ const file = inner.match(/<context\b[^>]*context-type="sourcefile"[^>]*>([\s\S]*?)<\/context>/)?.[1];
4797
+ if (file === void 0) continue;
4798
+ const lineRaw = inner.match(/<context\b[^>]*context-type="linenumber"[^>]*>([\s\S]*?)<\/context>/)?.[1];
4799
+ const decodedFile = decodeEntities(file.trim());
4800
+ const line = lineRaw ? parseInt(lineRaw.trim(), 10) || 1 : 1;
4801
+ const dedup = `${decodedFile}:${line}`;
4802
+ if (seen.has(dedup)) continue;
4803
+ seen.add(dedup);
4804
+ out.push({ file: decodedFile, line });
4805
+ }
4806
+ return out;
4807
+ }
4791
4808
  function decodeInline(raw, addMeta) {
4792
4809
  let out = "";
4793
4810
  let last = 0;
@@ -4859,6 +4876,10 @@ var angularXliff2 = {
4859
4876
  entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
4860
4877
  seen(targetLocale);
4861
4878
  }
4879
+ if (entry.locations === void 0) {
4880
+ const locs = decodeLocations(body);
4881
+ if (locs.length) entry.locations = locs;
4882
+ }
4862
4883
  }
4863
4884
  }
4864
4885
  return { locales, keys, warnings };
@@ -5605,6 +5626,96 @@ function assemble2(parsed, opts) {
5605
5626
  };
5606
5627
  }
5607
5628
 
5629
+ // src/server/import/merge.ts
5630
+ function hasContent(lv) {
5631
+ if (!lv) return false;
5632
+ return !!(lv.forms ? lv.forms.other?.trim() : lv.value?.trim());
5633
+ }
5634
+ function formsEqual(a, b) {
5635
+ const ak = Object.keys(a ?? {}).sort();
5636
+ const bk = Object.keys(b ?? {}).sort();
5637
+ if (ak.length !== bk.length || ak.some((k, i) => k !== bk[i])) return false;
5638
+ return ak.every((k) => a[k] === b[k]);
5639
+ }
5640
+ function sameSource(cur, inc, src) {
5641
+ if (!!cur.plural !== !!inc.plural) return false;
5642
+ const c = cur.values[src];
5643
+ const i = inc.values[src];
5644
+ return cur.plural ? formsEqual(c?.forms, i?.forms) : (c?.value ?? "") === (i?.value ?? "");
5645
+ }
5646
+ function applyIncomingSource(cur, inc, src, shapeChanged) {
5647
+ const incSrc = inc.values[src];
5648
+ if (shapeChanged) {
5649
+ if (inc.plural) cur.plural = { arg: inc.plural.arg };
5650
+ else delete cur.plural;
5651
+ cur.values = { [src]: { ...incSrc, state: "source" } };
5652
+ return;
5653
+ }
5654
+ const existing = cur.values[src];
5655
+ if (cur.plural) cur.values[src] = { ...existing, forms: incSrc?.forms, state: "source" };
5656
+ else cur.values[src] = { ...existing, value: incSrc?.value, state: "source" };
5657
+ }
5658
+ function cloneForAdd(inc, allowed) {
5659
+ const entry = structuredClone(inc);
5660
+ for (const loc of Object.keys(entry.values)) {
5661
+ if (!allowed.has(loc)) delete entry.values[loc];
5662
+ }
5663
+ return entry;
5664
+ }
5665
+ function mergeStates(existing, incoming, opts = {}) {
5666
+ const state = structuredClone(existing);
5667
+ const src = state.config.sourceLocale;
5668
+ const targets = state.config.locales.filter((l) => l !== src);
5669
+ const allowed = new Set(state.config.locales);
5670
+ const live = opts.liveKeys ?? new Set(Object.keys(incoming.keys));
5671
+ const plan = { added: [], sourceChanged: [], adopted: [], removed: [], unchanged: 0 };
5672
+ for (const [key, inc] of Object.entries(incoming.keys)) {
5673
+ if (!live.has(key)) continue;
5674
+ const cur = state.keys[key];
5675
+ if (!cur) {
5676
+ const entry = cloneForAdd(inc, allowed);
5677
+ if (!entry.createdAt) entry.createdAt = (/* @__PURE__ */ new Date()).toISOString();
5678
+ state.keys[key] = entry;
5679
+ plan.added.push(key);
5680
+ continue;
5681
+ }
5682
+ const shapeChanged = !!cur.plural !== !!inc.plural;
5683
+ const srcChanged = !sameSource(cur, inc, src);
5684
+ if (srcChanged) {
5685
+ applyIncomingSource(cur, inc, src, shapeChanged);
5686
+ plan.sourceChanged.push(key);
5687
+ for (const loc of targets) {
5688
+ const lv = cur.values[loc];
5689
+ if (lv && hasContent(lv)) lv.state = "needs-review";
5690
+ }
5691
+ }
5692
+ if (inc.placeholders) cur.placeholders = inc.placeholders;
5693
+ else delete cur.placeholders;
5694
+ if (!cur.description && inc.description) cur.description = inc.description;
5695
+ let adoptedHere = false;
5696
+ for (const loc of targets) {
5697
+ const incLv = inc.values[loc];
5698
+ if (!hasContent(incLv)) continue;
5699
+ if (hasContent(cur.values[loc])) continue;
5700
+ cur.values[loc] = { ...structuredClone(incLv), state: "reviewed" };
5701
+ plan.adopted.push({ key, locale: loc });
5702
+ adoptedHere = true;
5703
+ }
5704
+ if (!srcChanged && !adoptedHere) plan.unchanged++;
5705
+ }
5706
+ for (const key of Object.keys(state.keys)) {
5707
+ if (!live.has(key)) {
5708
+ plan.removed.push(key);
5709
+ if (opts.prune) delete state.keys[key];
5710
+ }
5711
+ }
5712
+ plan.added.sort();
5713
+ plan.sourceChanged.sort();
5714
+ plan.removed.sort();
5715
+ plan.adopted.sort((a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale));
5716
+ return { state, plan };
5717
+ }
5718
+
5608
5719
  // src/server/import/run.ts
5609
5720
  function previewImport(projectRoot, format) {
5610
5721
  const det = detect(projectRoot, format);
@@ -5628,6 +5739,29 @@ function previewImport(projectRoot, format) {
5628
5739
  sampleKeys
5629
5740
  };
5630
5741
  }
5742
+ function runSync(opts) {
5743
+ const det = detect(opts.projectRoot, opts.format);
5744
+ if (!det) throw new Error(`No recognized locale files found in ${opts.projectRoot}`);
5745
+ const parser = getParser(det.format);
5746
+ const sourceLocale = opts.sourceLocale ?? det.sourceLocale;
5747
+ const parsed = parser.parse(
5748
+ det.localeRoot,
5749
+ opts.locales ? { locales: opts.locales } : void 0
5750
+ );
5751
+ const sourceParse = parser.parse(det.localeRoot, { locales: [sourceLocale] });
5752
+ const liveKeys = new Set(Object.keys(sourceParse.keys));
5753
+ const assembled = assemble2(parsed, {
5754
+ sourceLocale,
5755
+ format: det.format,
5756
+ cldr: opts.cldr,
5757
+ localeRootRel: relative3(opts.projectRoot, det.localeRoot)
5758
+ });
5759
+ const { warnings, ...rest } = assembled;
5760
+ const incoming = validate(rest);
5761
+ const existing = loadState(opts.statePath);
5762
+ const { state, plan } = mergeStates(existing, incoming, { prune: opts.prune, liveKeys });
5763
+ return { state, plan, warnings, keyCount: Object.keys(state.keys).length };
5764
+ }
5631
5765
  function runImport(opts) {
5632
5766
  const det = detect(opts.projectRoot, opts.format);
5633
5767
  if (!det) throw new Error(`No recognized locale files found in ${opts.projectRoot}`);
@@ -5652,6 +5786,36 @@ function runImport(opts) {
5652
5786
  };
5653
5787
  }
5654
5788
 
5789
+ // src/server/import/usage.ts
5790
+ var LOCATION_SCANNED_ADAPTERS = /* @__PURE__ */ new Set(["angular-xliff"]);
5791
+ function isLocationScannedState(state) {
5792
+ return state.config.outputs.some((o) => LOCATION_SCANNED_ADAPTERS.has(o.adapter));
5793
+ }
5794
+ function buildLocationUsageCache(parsed) {
5795
+ const files = {};
5796
+ for (const [key, pk] of Object.entries(parsed.keys)) {
5797
+ for (const loc of pk.locations ?? []) {
5798
+ const file = files[loc.file] ??= { mtime: 0, size: 0, refs: [], prefixes: [] };
5799
+ file.refs.push({ key, line: loc.line, col: 1, scanner: "angular-xliff" });
5800
+ }
5801
+ }
5802
+ return { version: CACHE_VERSION, scannedAt: (/* @__PURE__ */ new Date()).toISOString(), files };
5803
+ }
5804
+ function usageCounts(cache2) {
5805
+ return {
5806
+ files: Object.keys(cache2.files).length,
5807
+ refs: Object.values(cache2.files).reduce((n, f) => n + f.refs.length, 0)
5808
+ };
5809
+ }
5810
+ function refreshLocationUsage(projectRoot, format) {
5811
+ const det = detect(projectRoot, format);
5812
+ if (!det) return null;
5813
+ const parsed = getParser(det.format).parse(det.localeRoot, { locales: [det.sourceLocale] });
5814
+ const cache2 = buildLocationUsageCache(parsed);
5815
+ saveUsageCache(projectRoot, cache2);
5816
+ return cache2;
5817
+ }
5818
+
5655
5819
  // src/server/export-run.ts
5656
5820
  import { existsSync as existsSync12, readFileSync as readFileSync20, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
5657
5821
  import { dirname as dirname2, resolve as resolve7, sep } from "path";
@@ -6503,6 +6667,39 @@ function createApi(deps) {
6503
6667
  console.log(`[import] ${result.keyCount} key(s) across ${result.localeCount} locale(s)${result.warnings.length ? `, ${result.warnings.length} warning(s)` : ""}`);
6504
6668
  return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
6505
6669
  });
6670
+ app.post("/sync", async (c) => {
6671
+ if (Object.keys(load().keys).length === 0) {
6672
+ return c.json({ error: "nothing to sync into; import first" }, 400);
6673
+ }
6674
+ const body = await c.req.json().catch(() => ({}));
6675
+ let result;
6676
+ try {
6677
+ result = runSync({
6678
+ projectRoot,
6679
+ statePath: deps.statePath,
6680
+ format: body.format,
6681
+ sourceLocale: body.sourceLocale,
6682
+ locales: body.locales,
6683
+ cldr: body.cldr,
6684
+ prune: body.prune
6685
+ });
6686
+ } catch (e) {
6687
+ return c.json({ error: e.message }, 400);
6688
+ }
6689
+ if (body.apply !== true) {
6690
+ return c.json({ plan: result.plan, warnings: result.warnings });
6691
+ }
6692
+ persist(result.state);
6693
+ const usageCache = isLocationScannedState(result.state) ? refreshLocationUsage(projectRoot, body.format) : null;
6694
+ const usageRefs = usageCache ? usageCounts(usageCache).refs : void 0;
6695
+ const p = result.plan;
6696
+ logChange({
6697
+ kind: "import",
6698
+ summary: `Synced: +${p.added.length} added, ~${p.sourceChanged.length} changed, -${p.removed.length} removed${body.prune ? " (pruned)" : ""}`
6699
+ });
6700
+ console.log(`[sync] +${p.added.length} ~${p.sourceChanged.length} -${p.removed.length}${body.prune ? " pruned" : ""}`);
6701
+ return c.json({ applied: true, plan: result.plan, warnings: result.warnings, usageRefs });
6702
+ });
6506
6703
  app.post("/export", (c) => {
6507
6704
  const root = dirname3(resolve9(deps.statePath));
6508
6705
  const { written, skipped, deleted, warnings } = exportToDisk(load(), root);
@@ -6766,12 +6963,11 @@ function createApi(deps) {
6766
6963
  app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
6767
6964
  app.post("/scan", async (c) => {
6768
6965
  const s = load();
6769
- const existing = loadUsageCache(projectRoot);
6770
- const result = runScan(projectRoot, s.config.scan ?? {}, existing);
6771
- const fileCount2 = Object.keys(result.files).length;
6772
- const refCount = Object.values(result.files).reduce((n, f) => n + f.refs.length, 0);
6773
- console.log(`[scan] ${fileCount2} file(s), ${refCount} reference(s)`);
6774
- return c.json({ files: fileCount2, refs: refCount, scannedAt: result.scannedAt });
6966
+ const result = isLocationScannedState(s) ? refreshLocationUsage(projectRoot) : runScan(projectRoot, s.config.scan ?? {}, loadUsageCache(projectRoot));
6967
+ if (!result) return c.json({ files: 0, refs: 0, scannedAt: (/* @__PURE__ */ new Date()).toISOString() });
6968
+ const { files, refs } = usageCounts(result);
6969
+ console.log(`[scan] ${files} file(s), ${refs} reference(s)`);
6970
+ return c.json({ files, refs, scannedAt: result.scannedAt });
6775
6971
  });
6776
6972
  app.get("/scan", (c) => {
6777
6973
  const cache2 = loadUsageCache(projectRoot);
@@ -7118,11 +7314,16 @@ function backgroundScan(statePath) {
7118
7314
  const projectRoot = dirname4(resolve10(statePath));
7119
7315
  Promise.resolve().then(() => {
7120
7316
  const state = loadState(statePath);
7317
+ if (isLocationScannedState(state)) {
7318
+ const cache2 = refreshLocationUsage(projectRoot);
7319
+ const { files: files2, refs: refs2 } = cache2 ? usageCounts(cache2) : { files: 0, refs: 0 };
7320
+ console.log(`[scan] ${files2} file(s), ${refs2} reference(s) (from catalog locations)`);
7321
+ return;
7322
+ }
7121
7323
  const existing = loadUsageCache(projectRoot);
7122
7324
  const result = runScan(projectRoot, state.config.scan ?? {}, existing);
7123
- const fileCount2 = Object.keys(result.files).length;
7124
- const refCount = Object.values(result.files).reduce((n, f) => n + f.refs.length, 0);
7125
- console.log(`[scan] ${fileCount2} file(s), ${refCount} reference(s)`);
7325
+ const { files, refs } = usageCounts(result);
7326
+ console.log(`[scan] ${files} file(s), ${refs} reference(s)`);
7126
7327
  }).catch((err) => {
7127
7328
  console.warn("[scan] failed:", err instanceof Error ? err.message : String(err));
7128
7329
  });