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.
- package/dist/server/cli.js +239 -40
- package/dist/server/server.js +269 -113
- package/dist/ui/assets/index-DC89onXX.js +1904 -0
- package/dist/ui/assets/index-DPfAS4pJ.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-3IIAIpZW.css +0 -1
- package/dist/ui/assets/index-CrR0eUwT.js +0 -1891
package/dist/server/cli.js
CHANGED
|
@@ -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 <
|
|
2838
|
+
while (next < jobs.length) {
|
|
2768
2839
|
if (signal?.aborted) break;
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
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,
|
|
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)
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4691
|
-
|
|
4692
|
-
|
|
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
|
-
|
|
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 === "--
|
|
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, {
|
|
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: {
|