glotfile 0.4.6 → 0.5.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.
- package/dist/server/cli.js +206 -24
- package/dist/server/server.js +154 -14
- 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/server.js
CHANGED
|
@@ -202,9 +202,6 @@ function validate(raw) {
|
|
|
202
202
|
if (lint.ignore !== void 0 && (!Array.isArray(lint.ignore) || !lint.ignore.every((g) => typeof g === "string"))) {
|
|
203
203
|
fail("config.lint.ignore must be an array of strings");
|
|
204
204
|
}
|
|
205
|
-
if (lint.dictionary !== void 0 && (!Array.isArray(lint.dictionary) || !lint.dictionary.every((w) => typeof w === "string"))) {
|
|
206
|
-
fail("config.lint.dictionary must be an array of strings");
|
|
207
|
-
}
|
|
208
205
|
if (lint.spelling !== void 0) {
|
|
209
206
|
if (!isObject(lint.spelling)) fail("config.lint.spelling must be an object");
|
|
210
207
|
if (lint.spelling.locales !== void 0 && (!isObject(lint.spelling.locales) || !Object.values(lint.spelling.locales).every((v) => typeof v === "string"))) {
|
|
@@ -269,6 +266,20 @@ function validate(raw) {
|
|
|
269
266
|
lv.value = lv.value.trim();
|
|
270
267
|
}
|
|
271
268
|
}
|
|
269
|
+
if (entry.suppressions !== void 0) {
|
|
270
|
+
if (!Array.isArray(entry.suppressions)) fail(`key "${key}" suppressions must be an array`);
|
|
271
|
+
for (const s of entry.suppressions) {
|
|
272
|
+
if (!isObject(s) || typeof s.locale !== "string" || typeof s.source !== "string") {
|
|
273
|
+
fail(`key "${key}" has an invalid suppression (needs string rule, locale, source)`);
|
|
274
|
+
}
|
|
275
|
+
if (!RULE_IDS.includes(s.rule)) {
|
|
276
|
+
fail(`key "${key}" suppression has unknown rule id "${String(s.rule)}"`);
|
|
277
|
+
}
|
|
278
|
+
if (s.at !== void 0 && typeof s.at !== "string") {
|
|
279
|
+
fail(`key "${key}" suppression "at" must be a string`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
272
283
|
if (entry.notes !== void 0) {
|
|
273
284
|
if (!Array.isArray(entry.notes)) fail(`key "${key}" notes must be an array`);
|
|
274
285
|
for (const n of entry.notes) {
|
|
@@ -512,6 +523,30 @@ function normalizeSource(value) {
|
|
|
512
523
|
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
513
524
|
}
|
|
514
525
|
|
|
526
|
+
// src/server/lint/suppress.ts
|
|
527
|
+
import { createHash } from "crypto";
|
|
528
|
+
function sourceSignature(entry, sourceLocale) {
|
|
529
|
+
const lv = entry.values[sourceLocale];
|
|
530
|
+
if (entry.plural) {
|
|
531
|
+
return Object.entries(lv?.forms ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([cat, body]) => `${cat}:${normalizeSource(body ?? "")}`).join("|");
|
|
532
|
+
}
|
|
533
|
+
return normalizeSource(lv?.value ?? "");
|
|
534
|
+
}
|
|
535
|
+
function sourceHash(entry, sourceLocale) {
|
|
536
|
+
return createHash("sha256").update(sourceSignature(entry, sourceLocale)).digest("hex").slice(0, 12);
|
|
537
|
+
}
|
|
538
|
+
function findSuppression(entry, sourceLocale, ruleId, locale) {
|
|
539
|
+
if (!entry.suppressions?.length) return void 0;
|
|
540
|
+
const current = sourceHash(entry, sourceLocale);
|
|
541
|
+
return entry.suppressions.find((s) => s.rule === ruleId && s.locale === locale && s.source === current);
|
|
542
|
+
}
|
|
543
|
+
function pruneStaleSuppressions(entry, sourceLocale) {
|
|
544
|
+
if (!entry.suppressions?.length) return;
|
|
545
|
+
const current = sourceHash(entry, sourceLocale);
|
|
546
|
+
entry.suppressions = entry.suppressions.filter((s) => s.source === current);
|
|
547
|
+
if (!entry.suppressions.length) delete entry.suppressions;
|
|
548
|
+
}
|
|
549
|
+
|
|
515
550
|
// src/server/state.ts
|
|
516
551
|
var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
517
552
|
function canonLocale(locale) {
|
|
@@ -532,6 +567,7 @@ function normalizeState(state) {
|
|
|
532
567
|
const remapped = {};
|
|
533
568
|
for (const [loc, lv] of Object.entries(entry.values)) remapped[canonLocale(loc)] = lv;
|
|
534
569
|
entry.values = remapped;
|
|
570
|
+
for (const s of entry.suppressions ?? []) s.locale = canonLocale(s.locale);
|
|
535
571
|
}
|
|
536
572
|
for (const output of state.config.outputs) {
|
|
537
573
|
if (!output.localeMap) continue;
|
|
@@ -623,6 +659,7 @@ function setSourceValue(state, key, value) {
|
|
|
623
659
|
lv.state = "needs-review";
|
|
624
660
|
}
|
|
625
661
|
}
|
|
662
|
+
pruneStaleSuppressions(entry, state.config.sourceLocale);
|
|
626
663
|
}
|
|
627
664
|
}
|
|
628
665
|
function setTargetValue(state, key, locale, value, clock = systemClock) {
|
|
@@ -646,6 +683,7 @@ function setSourcePluralForms(state, key, forms) {
|
|
|
646
683
|
lv.state = "needs-review";
|
|
647
684
|
}
|
|
648
685
|
}
|
|
686
|
+
pruneStaleSuppressions(entry, state.config.sourceLocale);
|
|
649
687
|
}
|
|
650
688
|
}
|
|
651
689
|
function setPluralForms(state, key, locale, forms, clock = systemClock) {
|
|
@@ -719,6 +757,19 @@ function deleteNote(state, key, id) {
|
|
|
719
757
|
if (!entry.notes) return;
|
|
720
758
|
entry.notes = entry.notes.filter((n) => n.id !== id);
|
|
721
759
|
}
|
|
760
|
+
function addSuppression(state, key, rule, locale, clock = systemClock) {
|
|
761
|
+
const entry = requireKey(state, key);
|
|
762
|
+
if (!RULE_IDS.includes(rule)) throw new GlotfileError(`Unknown lint rule: ${rule}`);
|
|
763
|
+
const list = (entry.suppressions ?? []).filter((s) => !(s.rule === rule && s.locale === locale));
|
|
764
|
+
list.push({ rule, locale, source: sourceHash(entry, state.config.sourceLocale), at: clock() });
|
|
765
|
+
entry.suppressions = list;
|
|
766
|
+
}
|
|
767
|
+
function removeSuppression(state, key, rule, locale) {
|
|
768
|
+
const entry = requireKey(state, key);
|
|
769
|
+
if (!entry.suppressions) return;
|
|
770
|
+
entry.suppressions = entry.suppressions.filter((s) => !(s.rule === rule && s.locale === locale));
|
|
771
|
+
if (!entry.suppressions.length) delete entry.suppressions;
|
|
772
|
+
}
|
|
722
773
|
function upsertGlossaryEntry(state, entry) {
|
|
723
774
|
const i = state.glossary.findIndex((e) => e.term === entry.term);
|
|
724
775
|
if (i === -1) state.glossary.push(entry);
|
|
@@ -755,6 +806,23 @@ function applyMachineTranslationForms(state, key, locale, forms, clock = systemC
|
|
|
755
806
|
return true;
|
|
756
807
|
}
|
|
757
808
|
|
|
809
|
+
// src/server/lint/accept.ts
|
|
810
|
+
function acceptFindings(state, findings, opts = {}, clock = systemClock) {
|
|
811
|
+
const byRule = {};
|
|
812
|
+
let accepted = 0;
|
|
813
|
+
for (const f of findings) {
|
|
814
|
+
if (f.locale === "" || f.suppressed) continue;
|
|
815
|
+
if (f.severity === "error" && !opts.includeErrors) continue;
|
|
816
|
+
if (opts.rules && !opts.rules.includes(f.ruleId)) continue;
|
|
817
|
+
if (opts.locales && !opts.locales.includes(f.locale)) continue;
|
|
818
|
+
if (!state.keys[f.key]) continue;
|
|
819
|
+
addSuppression(state, f.key, f.ruleId, f.locale, clock);
|
|
820
|
+
byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1;
|
|
821
|
+
accepted++;
|
|
822
|
+
}
|
|
823
|
+
return { accepted, byRule };
|
|
824
|
+
}
|
|
825
|
+
|
|
758
826
|
// src/server/scan.ts
|
|
759
827
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
760
828
|
import { resolve as resolve2 } from "path";
|
|
@@ -1653,11 +1721,19 @@ function spellValue(locale, value, ignore) {
|
|
|
1653
1721
|
|
|
1654
1722
|
// src/server/checks.ts
|
|
1655
1723
|
var CHECK_IDS = ["untranslated", "placeholder", "spelling", "length", "glossary"];
|
|
1724
|
+
var CHECK_RULE = {
|
|
1725
|
+
untranslated: "empty-translation",
|
|
1726
|
+
placeholder: "placeholder-mismatch",
|
|
1727
|
+
spelling: "spelling",
|
|
1728
|
+
length: "max-length",
|
|
1729
|
+
glossary: "glossary-violation"
|
|
1730
|
+
};
|
|
1656
1731
|
function contains(haystack, needle, caseSensitive) {
|
|
1657
1732
|
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1658
1733
|
}
|
|
1659
1734
|
function runChecks(state, opts = {}) {
|
|
1660
|
-
const
|
|
1735
|
+
const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
|
|
1736
|
+
const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
|
|
1661
1737
|
const issues = [];
|
|
1662
1738
|
let spellPending = false;
|
|
1663
1739
|
const { sourceLocale } = state.config;
|
|
@@ -1785,7 +1861,11 @@ function runChecks(state, opts = {}) {
|
|
|
1785
1861
|
}
|
|
1786
1862
|
}
|
|
1787
1863
|
}
|
|
1788
|
-
|
|
1864
|
+
const visible = issues.filter((i) => {
|
|
1865
|
+
const entry = state.keys[i.key];
|
|
1866
|
+
return !entry || !findSuppression(entry, sourceLocale, CHECK_RULE[i.check], i.locale);
|
|
1867
|
+
});
|
|
1868
|
+
return { issues: visible, spellPending };
|
|
1789
1869
|
}
|
|
1790
1870
|
|
|
1791
1871
|
// src/server/lint/spelling.ts
|
|
@@ -2027,7 +2107,10 @@ function sortFindings(findings) {
|
|
|
2027
2107
|
}
|
|
2028
2108
|
function countSeverities(findings) {
|
|
2029
2109
|
let error = 0, warn = 0;
|
|
2030
|
-
for (const f of findings)
|
|
2110
|
+
for (const f of findings) {
|
|
2111
|
+
if (f.suppressed) continue;
|
|
2112
|
+
f.severity === "error" ? error++ : warn++;
|
|
2113
|
+
}
|
|
2031
2114
|
return { error, warn };
|
|
2032
2115
|
}
|
|
2033
2116
|
async function loadSpellers(locales, config, load, warn) {
|
|
@@ -2053,7 +2136,7 @@ async function runLint(state, options = {}) {
|
|
|
2053
2136
|
const active = rules.filter(isActive);
|
|
2054
2137
|
const spellingOn = active.some((r) => r.id === "spelling");
|
|
2055
2138
|
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
2056
|
-
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.
|
|
2139
|
+
const allowWords = spellingOn ? buildAllowWords(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
|
|
2057
2140
|
const ctx = {
|
|
2058
2141
|
config,
|
|
2059
2142
|
sourceLocale: state.config.sourceLocale,
|
|
@@ -2065,16 +2148,23 @@ async function runLint(state, options = {}) {
|
|
|
2065
2148
|
const ignoreRes = (config.ignore ?? []).map(globToRegExp2);
|
|
2066
2149
|
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
2067
2150
|
const findings = [];
|
|
2151
|
+
let suppressed = 0;
|
|
2068
2152
|
for (const rule of active) {
|
|
2069
2153
|
const severity = resolveSeverity(rule.id, config);
|
|
2070
2154
|
for (const raw of rule.run(state, ctx)) {
|
|
2071
2155
|
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
2072
2156
|
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
2157
|
+
const entry = state.keys[raw.key];
|
|
2158
|
+
if (raw.locale !== "" && entry && findSuppression(entry, state.config.sourceLocale, rule.id, raw.locale)) {
|
|
2159
|
+
suppressed++;
|
|
2160
|
+
if (options.includeSuppressed) findings.push({ ...raw, severity, suppressed: true });
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2073
2163
|
findings.push({ ...raw, severity });
|
|
2074
2164
|
}
|
|
2075
2165
|
}
|
|
2076
2166
|
const sorted = sortFindings(findings);
|
|
2077
|
-
const counts = countSeverities(sorted);
|
|
2167
|
+
const counts = { ...countSeverities(sorted), suppressed };
|
|
2078
2168
|
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
2079
2169
|
}
|
|
2080
2170
|
|
|
@@ -4108,6 +4198,7 @@ import { homedir } from "os";
|
|
|
4108
4198
|
import { join as join8 } from "path";
|
|
4109
4199
|
var THEMES = ["system", "light", "dark"];
|
|
4110
4200
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
4201
|
+
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
4111
4202
|
var defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
|
|
4112
4203
|
var DEFAULTS = { theme: "system" };
|
|
4113
4204
|
function readJson(path) {
|
|
@@ -4120,7 +4211,10 @@ function readJson(path) {
|
|
|
4120
4211
|
}
|
|
4121
4212
|
function loadUiPrefs(path) {
|
|
4122
4213
|
const raw = readJson(path);
|
|
4123
|
-
|
|
4214
|
+
const prefs = { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
|
|
4215
|
+
if (isPanelWidth(raw.keyColumnWidth)) prefs.keyColumnWidth = Math.round(raw.keyColumnWidth);
|
|
4216
|
+
if (isPanelWidth(raw.detailPanelWidth)) prefs.detailPanelWidth = Math.round(raw.detailPanelWidth);
|
|
4217
|
+
return prefs;
|
|
4124
4218
|
}
|
|
4125
4219
|
function saveUiPrefs(path, prefs) {
|
|
4126
4220
|
const merged = { ...readJson(path), ...prefs };
|
|
@@ -4256,9 +4350,20 @@ function createApi(deps) {
|
|
|
4256
4350
|
app.get("/state", (c) => c.json(load()));
|
|
4257
4351
|
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
4258
4352
|
app.put("/ui-prefs", async (c) => {
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4261
|
-
|
|
4353
|
+
const body = await c.req.json();
|
|
4354
|
+
const patch = {};
|
|
4355
|
+
if ("theme" in body) {
|
|
4356
|
+
if (!isThemeMode(body.theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
|
|
4357
|
+
patch.theme = body.theme;
|
|
4358
|
+
}
|
|
4359
|
+
for (const field of ["keyColumnWidth", "detailPanelWidth"]) {
|
|
4360
|
+
if (field in body) {
|
|
4361
|
+
if (!isPanelWidth(body[field])) return c.json({ error: `${field} must be a number between 120 and 1200` }, 400);
|
|
4362
|
+
patch[field] = Math.round(body[field]);
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
if (Object.keys(patch).length === 0) return c.json({ error: "no recognized preferences in body" }, 400);
|
|
4366
|
+
saveUiPrefs(uiPrefsPath, patch);
|
|
4262
4367
|
return c.json({ ok: true });
|
|
4263
4368
|
});
|
|
4264
4369
|
app.get("/local-settings", (c) => c.json(loadLocalSettings(projectRoot)));
|
|
@@ -4732,12 +4837,47 @@ function createApi(deps) {
|
|
|
4732
4837
|
};
|
|
4733
4838
|
app.get("/lint", async (c) => {
|
|
4734
4839
|
const state = load();
|
|
4840
|
+
const includeSuppressed = c.req.query("includeSuppressed") === "1";
|
|
4735
4841
|
const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
|
|
4736
|
-
} });
|
|
4842
|
+
}, includeSuppressed });
|
|
4737
4843
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
|
|
4738
|
-
const counts = countSeverities(findings);
|
|
4844
|
+
const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
|
|
4739
4845
|
return c.json({ findings, counts, ok: counts.error === 0 });
|
|
4740
4846
|
});
|
|
4847
|
+
app.post("/keys/:key/suppressions", async (c) => {
|
|
4848
|
+
const key = c.req.param("key");
|
|
4849
|
+
const { rule, locale } = await c.req.json();
|
|
4850
|
+
if (typeof rule !== "string" || !rule) return c.json({ error: "rule is required" }, 400);
|
|
4851
|
+
if (typeof locale !== "string" || !locale) return c.json({ error: "locale is required" }, 400);
|
|
4852
|
+
const s = load();
|
|
4853
|
+
addSuppression(s, key, rule, locale);
|
|
4854
|
+
persist(s);
|
|
4855
|
+
logChange({ kind: "suppression", summary: `Suppressed ${rule} for ${key} [${locale}]`, key, locale, after: rule });
|
|
4856
|
+
return c.json({ ok: true });
|
|
4857
|
+
});
|
|
4858
|
+
app.delete("/keys/:key/suppressions", (c) => {
|
|
4859
|
+
const key = c.req.param("key");
|
|
4860
|
+
const rule = c.req.query("rule") ?? "";
|
|
4861
|
+
const locale = c.req.query("locale") ?? "";
|
|
4862
|
+
if (!rule || !locale) return c.json({ error: "rule and locale are required" }, 400);
|
|
4863
|
+
const s = load();
|
|
4864
|
+
removeSuppression(s, key, rule, locale);
|
|
4865
|
+
persist(s);
|
|
4866
|
+
logChange({ kind: "suppression", summary: `Unsuppressed ${rule} for ${key} [${locale}]`, key, locale, before: rule });
|
|
4867
|
+
return c.json({ ok: true });
|
|
4868
|
+
});
|
|
4869
|
+
app.post("/lint/accept", async (c) => {
|
|
4870
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4871
|
+
const s = load();
|
|
4872
|
+
const lint = await runLint(s, { loadSpeller: cachedLoader, warn: () => {
|
|
4873
|
+
} });
|
|
4874
|
+
const result = acceptFindings(s, lint.findings, { rules: body.rules, locales: body.locales });
|
|
4875
|
+
if (result.accepted > 0) {
|
|
4876
|
+
persist(s);
|
|
4877
|
+
logChange({ kind: "suppression", summary: `Suppressed ${result.accepted} finding(s)`, after: result.byRule });
|
|
4878
|
+
}
|
|
4879
|
+
return c.json({ ok: true, ...result });
|
|
4880
|
+
});
|
|
4741
4881
|
app.get("/checks", (c) => {
|
|
4742
4882
|
const param = c.req.query("checks");
|
|
4743
4883
|
const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
|