glotfile 0.4.6 → 0.5.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.
@@ -176,9 +176,6 @@ function validate(raw) {
176
176
  if (lint.ignore !== void 0 && (!Array.isArray(lint.ignore) || !lint.ignore.every((g) => typeof g === "string"))) {
177
177
  fail("config.lint.ignore must be an array of strings");
178
178
  }
179
- if (lint.dictionary !== void 0 && (!Array.isArray(lint.dictionary) || !lint.dictionary.every((w) => typeof w === "string"))) {
180
- fail("config.lint.dictionary must be an array of strings");
181
- }
182
179
  if (lint.spelling !== void 0) {
183
180
  if (!isObject(lint.spelling)) fail("config.lint.spelling must be an object");
184
181
  if (lint.spelling.locales !== void 0 && (!isObject(lint.spelling.locales) || !Object.values(lint.spelling.locales).every((v) => typeof v === "string"))) {
@@ -243,6 +240,20 @@ function validate(raw) {
243
240
  lv.value = lv.value.trim();
244
241
  }
245
242
  }
243
+ if (entry.suppressions !== void 0) {
244
+ if (!Array.isArray(entry.suppressions)) fail(`key "${key}" suppressions must be an array`);
245
+ for (const s of entry.suppressions) {
246
+ if (!isObject(s) || typeof s.locale !== "string" || typeof s.source !== "string") {
247
+ fail(`key "${key}" has an invalid suppression (needs string rule, locale, source)`);
248
+ }
249
+ if (!RULE_IDS.includes(s.rule)) {
250
+ fail(`key "${key}" suppression has unknown rule id "${String(s.rule)}"`);
251
+ }
252
+ if (s.at !== void 0 && typeof s.at !== "string") {
253
+ fail(`key "${key}" suppression "at" must be a string`);
254
+ }
255
+ }
256
+ }
246
257
  if (entry.notes !== void 0) {
247
258
  if (!Array.isArray(entry.notes)) fail(`key "${key}" notes must be an array`);
248
259
  for (const n of entry.notes) {
@@ -520,6 +531,36 @@ var init_normalize = __esm({
520
531
  }
521
532
  });
522
533
 
534
+ // src/server/lint/suppress.ts
535
+ import { createHash } from "crypto";
536
+ function sourceSignature(entry, sourceLocale) {
537
+ const lv = entry.values[sourceLocale];
538
+ if (entry.plural) {
539
+ return Object.entries(lv?.forms ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([cat, body]) => `${cat}:${normalizeSource(body ?? "")}`).join("|");
540
+ }
541
+ return normalizeSource(lv?.value ?? "");
542
+ }
543
+ function sourceHash(entry, sourceLocale) {
544
+ return createHash("sha256").update(sourceSignature(entry, sourceLocale)).digest("hex").slice(0, 12);
545
+ }
546
+ function findSuppression(entry, sourceLocale, ruleId, locale) {
547
+ if (!entry.suppressions?.length) return void 0;
548
+ const current = sourceHash(entry, sourceLocale);
549
+ return entry.suppressions.find((s) => s.rule === ruleId && s.locale === locale && s.source === current);
550
+ }
551
+ function pruneStaleSuppressions(entry, sourceLocale) {
552
+ if (!entry.suppressions?.length) return;
553
+ const current = sourceHash(entry, sourceLocale);
554
+ entry.suppressions = entry.suppressions.filter((s) => s.source === current);
555
+ if (!entry.suppressions.length) delete entry.suppressions;
556
+ }
557
+ var init_suppress = __esm({
558
+ "src/server/lint/suppress.ts"() {
559
+ "use strict";
560
+ init_normalize();
561
+ }
562
+ });
563
+
523
564
  // src/server/state.ts
524
565
  import { readFileSync as readFileSync2, existsSync as existsSync2, rmSync as rmSync3 } from "fs";
525
566
  import { randomUUID } from "crypto";
@@ -541,6 +582,7 @@ function normalizeState(state) {
541
582
  const remapped = {};
542
583
  for (const [loc, lv] of Object.entries(entry.values)) remapped[canonLocale(loc)] = lv;
543
584
  entry.values = remapped;
585
+ for (const s of entry.suppressions ?? []) s.locale = canonLocale(s.locale);
544
586
  }
545
587
  for (const output of state.config.outputs) {
546
588
  if (!output.localeMap) continue;
@@ -641,6 +683,7 @@ function setSourceValue(state, key, value) {
641
683
  lv.state = "needs-review";
642
684
  }
643
685
  }
686
+ pruneStaleSuppressions(entry, state.config.sourceLocale);
644
687
  }
645
688
  }
646
689
  function setTargetValue(state, key, locale, value, clock = systemClock) {
@@ -664,6 +707,7 @@ function setSourcePluralForms(state, key, forms) {
664
707
  lv.state = "needs-review";
665
708
  }
666
709
  }
710
+ pruneStaleSuppressions(entry, state.config.sourceLocale);
667
711
  }
668
712
  }
669
713
  function setPluralForms(state, key, locale, forms, clock = systemClock) {
@@ -737,6 +781,19 @@ function deleteNote(state, key, id) {
737
781
  if (!entry.notes) return;
738
782
  entry.notes = entry.notes.filter((n) => n.id !== id);
739
783
  }
784
+ function addSuppression(state, key, rule, locale, clock = systemClock) {
785
+ const entry = requireKey(state, key);
786
+ if (!RULE_IDS.includes(rule)) throw new GlotfileError(`Unknown lint rule: ${rule}`);
787
+ const list = (entry.suppressions ?? []).filter((s) => !(s.rule === rule && s.locale === locale));
788
+ list.push({ rule, locale, source: sourceHash(entry, state.config.sourceLocale), at: clock() });
789
+ entry.suppressions = list;
790
+ }
791
+ function removeSuppression(state, key, rule, locale) {
792
+ const entry = requireKey(state, key);
793
+ if (!entry.suppressions) return;
794
+ entry.suppressions = entry.suppressions.filter((s) => !(s.rule === rule && s.locale === locale));
795
+ if (!entry.suppressions.length) delete entry.suppressions;
796
+ }
740
797
  function upsertGlossaryEntry(state, entry) {
741
798
  const i = state.glossary.findIndex((e) => e.term === entry.term);
742
799
  if (i === -1) state.glossary.push(entry);
@@ -782,6 +839,8 @@ var init_state = __esm({
782
839
  init_plurals();
783
840
  init_storage();
784
841
  init_normalize();
842
+ init_suppress();
843
+ init_registry();
785
844
  systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
786
845
  }
787
846
  });
@@ -2747,7 +2806,7 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
2747
2806
  const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
2748
2807
  return { skipped: keys.size };
2749
2808
  }
2750
- async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal) {
2809
+ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity) {
2751
2810
  if (!reqs.length) return [];
2752
2811
  const byLocale = /* @__PURE__ */ new Map();
2753
2812
  for (const req of reqs) {
@@ -2758,26 +2817,42 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
2758
2817
  }
2759
2818
  group.push(req);
2760
2819
  }
2820
+ const localeBatches = [...byLocale.entries()].map(([locale, group]) => ({
2821
+ locale,
2822
+ batches: chunk(group, Math.max(1, batchSize))
2823
+ }));
2824
+ const jobs = [];
2825
+ const maxBatches = Math.max(...localeBatches.map((g) => g.batches.length));
2826
+ for (let i = 0; i < maxBatches; i++) {
2827
+ for (const g of localeBatches) {
2828
+ if (i < g.batches.length) jobs.push({ locale: g.locale, batch: g.batches[i] });
2829
+ }
2830
+ }
2831
+ const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
2832
+ const started = /* @__PURE__ */ new Set();
2761
2833
  const total = reqs.length;
2762
2834
  let done = 0;
2763
2835
  const allResults = [];
2764
- const groups = [...byLocale.values()];
2765
2836
  let next = 0;
2766
2837
  async function worker() {
2767
- while (next < groups.length) {
2838
+ while (next < jobs.length) {
2768
2839
  if (signal?.aborted) break;
2769
- const group = groups[next++];
2770
- const locale = group[0].targetLocale;
2771
- hooks.onLocaleStart?.(locale);
2772
- const localeResults = await provider.translate(group, (_localeDone, _localeTotal, batchResults) => {
2773
- done += batchResults.length;
2774
- hooks.onBatchComplete?.(done, total, batchResults, locale);
2775
- }, signal, (raw, batchSize) => hooks.onMalformedReply?.(raw, batchSize, locale));
2776
- allResults.push(...localeResults);
2777
- if (!signal?.aborted) hooks.onLocaleDone?.(locale);
2840
+ const { locale, batch } = jobs[next++];
2841
+ if (!started.has(locale)) {
2842
+ started.add(locale);
2843
+ hooks.onLocaleStart?.(locale);
2844
+ }
2845
+ const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
2846
+ done += results.length;
2847
+ hooks.onBatchComplete?.(done, total, results, locale);
2848
+ }, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
2849
+ allResults.push(...batchResults);
2850
+ const left = remaining.get(locale) - 1;
2851
+ remaining.set(locale, left);
2852
+ if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
2778
2853
  }
2779
2854
  }
2780
- const workers = Array.from({ length: Math.min(concurrency, groups.length) }, worker);
2855
+ const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
2781
2856
  await Promise.all(workers);
2782
2857
  return allResults;
2783
2858
  }
@@ -2813,6 +2888,7 @@ var init_run = __esm({
2813
2888
  init_plurals();
2814
2889
  init_state();
2815
2890
  init_glob();
2891
+ init_batch();
2816
2892
  MEDIA_TYPES = {
2817
2893
  ".png": "image/png",
2818
2894
  ".jpg": "image/jpeg",
@@ -3705,7 +3781,10 @@ function sortFindings(findings) {
3705
3781
  }
3706
3782
  function countSeverities(findings) {
3707
3783
  let error = 0, warn = 0;
3708
- for (const f of findings) f.severity === "error" ? error++ : warn++;
3784
+ for (const f of findings) {
3785
+ if (f.suppressed) continue;
3786
+ f.severity === "error" ? error++ : warn++;
3787
+ }
3709
3788
  return { error, warn };
3710
3789
  }
3711
3790
  async function loadSpellers(locales, config, load, warn) {
@@ -3731,7 +3810,7 @@ async function runLint(state, options = {}) {
3731
3810
  const active = rules.filter(isActive);
3732
3811
  const spellingOn = active.some((r) => r.id === "spelling");
3733
3812
  const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
3734
- const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
3813
+ const allowWords = spellingOn ? buildAllowWords(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
3735
3814
  const ctx = {
3736
3815
  config,
3737
3816
  sourceLocale: state.config.sourceLocale,
@@ -3743,16 +3822,23 @@ async function runLint(state, options = {}) {
3743
3822
  const ignoreRes = (config.ignore ?? []).map(globToRegExp);
3744
3823
  const localeFilter = options.locales ? new Set(options.locales) : null;
3745
3824
  const findings = [];
3825
+ let suppressed = 0;
3746
3826
  for (const rule of active) {
3747
3827
  const severity = resolveSeverity(rule.id, config);
3748
3828
  for (const raw of rule.run(state, ctx)) {
3749
3829
  if (ignoreRes.some((re) => re.test(raw.key))) continue;
3750
3830
  if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
3831
+ const entry = state.keys[raw.key];
3832
+ if (raw.locale !== "" && entry && findSuppression(entry, state.config.sourceLocale, rule.id, raw.locale)) {
3833
+ suppressed++;
3834
+ if (options.includeSuppressed) findings.push({ ...raw, severity, suppressed: true });
3835
+ continue;
3836
+ }
3751
3837
  findings.push({ ...raw, severity });
3752
3838
  }
3753
3839
  }
3754
3840
  const sorted = sortFindings(findings);
3755
- const counts = countSeverities(sorted);
3841
+ const counts = { ...countSeverities(sorted), suppressed };
3756
3842
  return { findings: sorted, counts, ok: counts.error === 0 };
3757
3843
  }
3758
3844
  var init_run2 = __esm({
@@ -3762,6 +3848,7 @@ var init_run2 = __esm({
3762
3848
  init_registry();
3763
3849
  init_rules();
3764
3850
  init_spelling();
3851
+ init_suppress();
3765
3852
  }
3766
3853
  });
3767
3854
 
@@ -3791,6 +3878,33 @@ var init_outputs = __esm({
3791
3878
  }
3792
3879
  });
3793
3880
 
3881
+ // src/server/lint/accept.ts
3882
+ var accept_exports = {};
3883
+ __export(accept_exports, {
3884
+ acceptFindings: () => acceptFindings
3885
+ });
3886
+ function acceptFindings(state, findings, opts = {}, clock = systemClock) {
3887
+ const byRule = {};
3888
+ let accepted = 0;
3889
+ for (const f of findings) {
3890
+ if (f.locale === "" || f.suppressed) continue;
3891
+ if (f.severity === "error" && !opts.includeErrors) continue;
3892
+ if (opts.rules && !opts.rules.includes(f.ruleId)) continue;
3893
+ if (opts.locales && !opts.locales.includes(f.locale)) continue;
3894
+ if (!state.keys[f.key]) continue;
3895
+ addSuppression(state, f.key, f.ruleId, f.locale, clock);
3896
+ byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1;
3897
+ accepted++;
3898
+ }
3899
+ return { accepted, byRule };
3900
+ }
3901
+ var init_accept = __esm({
3902
+ "src/server/lint/accept.ts"() {
3903
+ "use strict";
3904
+ init_state();
3905
+ }
3906
+ });
3907
+
3794
3908
  // src/server/import/detect.ts
3795
3909
  import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
3796
3910
  import { join as join4 } from "path";
@@ -4465,7 +4579,8 @@ function contains(haystack, needle, caseSensitive) {
4465
4579
  return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
4466
4580
  }
4467
4581
  function runChecks(state, opts = {}) {
4468
- const on = (id) => !opts.only || opts.only.includes(id);
4582
+ const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
4583
+ const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
4469
4584
  const issues = [];
4470
4585
  let spellPending = false;
4471
4586
  const { sourceLocale } = state.config;
@@ -4593,16 +4708,28 @@ function runChecks(state, opts = {}) {
4593
4708
  }
4594
4709
  }
4595
4710
  }
4596
- return { issues, spellPending };
4711
+ const visible = issues.filter((i) => {
4712
+ const entry = state.keys[i.key];
4713
+ return !entry || !findSuppression(entry, sourceLocale, CHECK_RULE[i.check], i.locale);
4714
+ });
4715
+ return { issues: visible, spellPending };
4597
4716
  }
4598
- var CHECK_IDS;
4717
+ var CHECK_IDS, CHECK_RULE;
4599
4718
  var init_checks = __esm({
4600
4719
  "src/server/checks.ts"() {
4601
4720
  "use strict";
4602
4721
  init_placeholders();
4603
4722
  init_run();
4604
4723
  init_spell();
4724
+ init_suppress();
4605
4725
  CHECK_IDS = ["untranslated", "placeholder", "spelling", "length", "glossary"];
4726
+ CHECK_RULE = {
4727
+ untranslated: "empty-translation",
4728
+ placeholder: "placeholder-mismatch",
4729
+ spelling: "spelling",
4730
+ length: "max-length",
4731
+ glossary: "glossary-violation"
4732
+ };
4606
4733
  }
4607
4734
  });
4608
4735
 
@@ -4620,19 +4747,23 @@ function readJson2(path) {
4620
4747
  }
4621
4748
  function loadUiPrefs(path) {
4622
4749
  const raw = readJson2(path);
4623
- return { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
4750
+ const prefs = { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
4751
+ if (isPanelWidth(raw.keyColumnWidth)) prefs.keyColumnWidth = Math.round(raw.keyColumnWidth);
4752
+ if (isPanelWidth(raw.detailPanelWidth)) prefs.detailPanelWidth = Math.round(raw.detailPanelWidth);
4753
+ return prefs;
4624
4754
  }
4625
4755
  function saveUiPrefs(path, prefs) {
4626
4756
  const merged = { ...readJson2(path), ...prefs };
4627
4757
  writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
4628
4758
  }
4629
- var THEMES, isThemeMode, defaultUiPrefsPath, DEFAULTS;
4759
+ var THEMES, isThemeMode, isPanelWidth, defaultUiPrefsPath, DEFAULTS;
4630
4760
  var init_ui_prefs = __esm({
4631
4761
  "src/server/ui-prefs.ts"() {
4632
4762
  "use strict";
4633
4763
  init_atomic_write();
4634
4764
  THEMES = ["system", "light", "dark"];
4635
4765
  isThemeMode = (v) => THEMES.includes(v);
4766
+ isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4636
4767
  defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
4637
4768
  DEFAULTS = { theme: "system" };
4638
4769
  }
@@ -4687,9 +4818,20 @@ function createApi(deps) {
4687
4818
  app.get("/state", (c) => c.json(load()));
4688
4819
  app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
4689
4820
  app.put("/ui-prefs", async (c) => {
4690
- const { theme } = await c.req.json();
4691
- if (!isThemeMode(theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
4692
- saveUiPrefs(uiPrefsPath, { theme });
4821
+ const body = await c.req.json();
4822
+ const patch = {};
4823
+ if ("theme" in body) {
4824
+ if (!isThemeMode(body.theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
4825
+ patch.theme = body.theme;
4826
+ }
4827
+ for (const field of ["keyColumnWidth", "detailPanelWidth"]) {
4828
+ if (field in body) {
4829
+ if (!isPanelWidth(body[field])) return c.json({ error: `${field} must be a number between 120 and 1200` }, 400);
4830
+ patch[field] = Math.round(body[field]);
4831
+ }
4832
+ }
4833
+ if (Object.keys(patch).length === 0) return c.json({ error: "no recognized preferences in body" }, 400);
4834
+ saveUiPrefs(uiPrefsPath, patch);
4693
4835
  return c.json({ ok: true });
4694
4836
  });
4695
4837
  app.get("/local-settings", (c) => c.json(loadLocalSettings(projectRoot)));
@@ -5163,12 +5305,47 @@ function createApi(deps) {
5163
5305
  };
5164
5306
  app.get("/lint", async (c) => {
5165
5307
  const state = load();
5308
+ const includeSuppressed = c.req.query("includeSuppressed") === "1";
5166
5309
  const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
5167
- } });
5310
+ }, includeSuppressed });
5168
5311
  const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
5169
- const counts = countSeverities(findings);
5312
+ const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
5170
5313
  return c.json({ findings, counts, ok: counts.error === 0 });
5171
5314
  });
5315
+ app.post("/keys/:key/suppressions", async (c) => {
5316
+ const key = c.req.param("key");
5317
+ const { rule, locale } = await c.req.json();
5318
+ if (typeof rule !== "string" || !rule) return c.json({ error: "rule is required" }, 400);
5319
+ if (typeof locale !== "string" || !locale) return c.json({ error: "locale is required" }, 400);
5320
+ const s = load();
5321
+ addSuppression(s, key, rule, locale);
5322
+ persist(s);
5323
+ logChange({ kind: "suppression", summary: `Suppressed ${rule} for ${key} [${locale}]`, key, locale, after: rule });
5324
+ return c.json({ ok: true });
5325
+ });
5326
+ app.delete("/keys/:key/suppressions", (c) => {
5327
+ const key = c.req.param("key");
5328
+ const rule = c.req.query("rule") ?? "";
5329
+ const locale = c.req.query("locale") ?? "";
5330
+ if (!rule || !locale) return c.json({ error: "rule and locale are required" }, 400);
5331
+ const s = load();
5332
+ removeSuppression(s, key, rule, locale);
5333
+ persist(s);
5334
+ logChange({ kind: "suppression", summary: `Unsuppressed ${rule} for ${key} [${locale}]`, key, locale, before: rule });
5335
+ return c.json({ ok: true });
5336
+ });
5337
+ app.post("/lint/accept", async (c) => {
5338
+ const body = await c.req.json().catch(() => ({}));
5339
+ const s = load();
5340
+ const lint = await runLint(s, { loadSpeller: cachedLoader, warn: () => {
5341
+ } });
5342
+ const result = acceptFindings(s, lint.findings, { rules: body.rules, locales: body.locales });
5343
+ if (result.accepted > 0) {
5344
+ persist(s);
5345
+ logChange({ kind: "suppression", summary: `Suppressed ${result.accepted} finding(s)`, after: result.byRule });
5346
+ }
5347
+ return c.json({ ok: true, ...result });
5348
+ });
5172
5349
  app.get("/checks", (c) => {
5173
5350
  const param = c.req.query("checks");
5174
5351
  const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
@@ -5293,7 +5470,7 @@ function createApi(deps) {
5293
5470
  raw
5294
5471
  });
5295
5472
  }
5296
- }, aiCfg.concurrency, signal);
5473
+ }, aiCfg.concurrency, signal, aiCfg.batchSize);
5297
5474
  if (!signal?.aborted) {
5298
5475
  console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
5299
5476
  await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
@@ -5336,7 +5513,7 @@ function createApi(deps) {
5336
5513
  raw
5337
5514
  });
5338
5515
  }
5339
- }, aiCfg.concurrency);
5516
+ }, aiCfg.concurrency, void 0, aiCfg.batchSize);
5340
5517
  const latest = load();
5341
5518
  ({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
5342
5519
  const entry = {
@@ -5523,6 +5700,7 @@ var init_api = __esm({
5523
5700
  "src/server/api.ts"() {
5524
5701
  "use strict";
5525
5702
  init_state();
5703
+ init_accept();
5526
5704
  init_scan();
5527
5705
  init_scanner();
5528
5706
  init_context();
@@ -5732,10 +5910,11 @@ function formatText(report) {
5732
5910
  lastKey = f.key;
5733
5911
  }
5734
5912
  const loc = f.locale ? ` ${f.locale}` : "";
5735
- lines.push(` ${f.severity} ${f.ruleId}${loc} ${f.message}`);
5913
+ lines.push(` ${f.severity} ${f.ruleId}${loc} ${f.message}${f.suppressed ? " (suppressed)" : ""}`);
5736
5914
  }
5737
5915
  lines.push("");
5738
- lines.push(`\u2716 ${report.counts.error} error(s), ${report.counts.warn} warning(s)`);
5916
+ const suppressed = report.counts.suppressed ? `, ${report.counts.suppressed} suppressed` : "";
5917
+ lines.push(`\u2716 ${report.counts.error} error(s), ${report.counts.warn} warning(s)${suppressed}`);
5739
5918
  return lines.join("\n") + "\n";
5740
5919
  }
5741
5920
  function formatJson(report) {
@@ -5823,7 +6002,9 @@ function parseArgs(argv) {
5823
6002
  } else if (flag === "--max-warnings" && next) {
5824
6003
  args.maxWarnings = Number(next);
5825
6004
  i++;
5826
- } else if (flag === "--all") args.all = true;
6005
+ } else if (flag === "--include-suppressed") args.includeSuppressed = true;
6006
+ else if (flag === "--accept") args.accept = true;
6007
+ else if (flag === "--all") args.all = true;
5827
6008
  else if (flag === "--limit" && next) {
5828
6009
  args.limit = Number(next);
5829
6010
  i++;
@@ -5959,7 +6140,7 @@ async function runTranslate(args) {
5959
6140
  raw
5960
6141
  });
5961
6142
  }
5962
- });
6143
+ }, ai.concurrency, void 0, ai.batchSize);
5963
6144
  process.stdout.write("\n");
5964
6145
  if (!batchCallbackFired) {
5965
6146
  ({ written, errors } = applyResults(state, toTranslate, results));
@@ -5995,8 +6176,24 @@ function printReport(report, format, rawText) {
5995
6176
  }
5996
6177
  async function runLintCmd(args) {
5997
6178
  const state = loadState(args.statePath);
6179
+ if (args.accept) {
6180
+ const { acceptFindings: acceptFindings2 } = await Promise.resolve().then(() => (init_accept(), accept_exports));
6181
+ const report2 = await runLint(state, { locales: args.locales });
6182
+ const result = acceptFindings2(state, report2.findings, { rules: args.ruleIds, locales: args.locales });
6183
+ if (result.accepted > 0) saveState(args.statePath, state);
6184
+ console.log(`Suppressed ${result.accepted} warning(s).`);
6185
+ for (const [rule, n] of Object.entries(result.byRule)) console.log(` ${rule}: ${n}`);
6186
+ if (result.accepted > 0) {
6187
+ console.log("Each suppression expires automatically when its key's source text changes.");
6188
+ }
6189
+ return;
6190
+ }
5998
6191
  const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
5999
- const report = await runLint(state, { locales: args.locales, ruleIds: args.ruleIds });
6192
+ const report = await runLint(state, {
6193
+ locales: args.locales,
6194
+ ruleIds: args.ruleIds,
6195
+ includeSuppressed: args.includeSuppressed
6196
+ });
6000
6197
  printReport(report, args.format, rawText);
6001
6198
  const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
6002
6199
  if (!report.ok || tooManyWarnings) process.exitCode = 1;
@@ -6008,7 +6205,7 @@ async function runCheck(args) {
6008
6205
  } catch (e) {
6009
6206
  const report2 = {
6010
6207
  findings: [{ ruleId: "load-error", key: "", locale: "", severity: "error", message: e.message }],
6011
- counts: { error: 1, warn: 0 },
6208
+ counts: { error: 1, warn: 0, suppressed: 0 },
6012
6209
  ok: false
6013
6210
  };
6014
6211
  printReport(report2, args.format, "");
@@ -6019,7 +6216,7 @@ async function runCheck(args) {
6019
6216
  const root = dirname5(resolve11(args.statePath));
6020
6217
  const lint = await runLint(state, {});
6021
6218
  const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
6022
- const counts = countSeverities(findings);
6219
+ const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
6023
6220
  const report = { findings, counts, ok: counts.error === 0 };
6024
6221
  printReport(report, args.format, rawText);
6025
6222
  if (!report.ok) process.exitCode = 1;
@@ -6206,12 +6403,14 @@ var COMMAND_HELP = {
6206
6403
  },
6207
6404
  lint: {
6208
6405
  summary: "Check the catalog for problems (placeholders, length, glossary, \u2026).",
6209
- usage: "glotfile lint [--format <text|json|sarif>] [--locale <list>] [--rule <list>] [--max-warnings <n>]",
6406
+ usage: "glotfile lint [--format <text|json|sarif>] [--locale <list>] [--rule <list>] [--max-warnings <n>] [--include-suppressed] [--accept]",
6210
6407
  options: [
6211
6408
  ["--format <fmt>", "Output format: text (default), json, or sarif"],
6212
6409
  ["--locale <list>", "Restrict to these comma-separated locales"],
6213
6410
  ["--rule <list>", "Only run these comma-separated rule ids"],
6214
- ["--max-warnings <n>", "Exit non-zero if warnings exceed n"]
6411
+ ["--max-warnings <n>", "Exit non-zero if warnings exceed n"],
6412
+ ["--include-suppressed", "Also show findings hidden by suppressions"],
6413
+ ["--accept", "Suppress all current warnings (narrow with --rule/--locale); each expires when its source changes"]
6215
6414
  ]
6216
6415
  },
6217
6416
  check: {