glotfile 0.4.1 → 0.4.3
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/bin/glotfile.js +0 -0
- package/dist/server/cli.js +646 -422
- package/dist/server/server.js +590 -87
- package/dist/ui/assets/index-CqpESIEu.css +1 -0
- package/dist/ui/assets/{index-DfZmbiXq.js → index-VdTDY_C8.js} +49 -19
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-DVTJ7ZX_.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -788,14 +788,14 @@ var init_state = __esm({
|
|
|
788
788
|
|
|
789
789
|
// src/server/adapters/options.ts
|
|
790
790
|
function applyCase(canonical, style) {
|
|
791
|
-
const
|
|
791
|
+
const sep4 = style === "lower-underscore" || style === "bcp47-underscore" ? "_" : "-";
|
|
792
792
|
const lower = style === "lower-hyphen" || style === "lower-underscore";
|
|
793
793
|
return canonical.split(/[-_]/).map((p, i) => {
|
|
794
794
|
if (lower || i === 0) return p.toLowerCase();
|
|
795
795
|
if (/^[a-z]{4}$/i.test(p)) return p[0].toUpperCase() + p.slice(1).toLowerCase();
|
|
796
796
|
if (/^[a-z]{2}$/i.test(p)) return p.toUpperCase();
|
|
797
797
|
return p;
|
|
798
|
-
}).join(
|
|
798
|
+
}).join(sep4);
|
|
799
799
|
}
|
|
800
800
|
function resolveLocaleToken(output, canonical, adapterDefault) {
|
|
801
801
|
const mapped = output.localeMap?.[canonical];
|
|
@@ -1457,6 +1457,114 @@ var init_vue_i18n_json = __esm({
|
|
|
1457
1457
|
}
|
|
1458
1458
|
});
|
|
1459
1459
|
|
|
1460
|
+
// src/server/adapters/angular-xliff.ts
|
|
1461
|
+
function xmlEscape2(s) {
|
|
1462
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1463
|
+
}
|
|
1464
|
+
function renderInterpolations(text, ids) {
|
|
1465
|
+
let out = "";
|
|
1466
|
+
let last = 0;
|
|
1467
|
+
for (const m of text.matchAll(/\{(\w+)\}/g)) {
|
|
1468
|
+
const name = m[1];
|
|
1469
|
+
let id = ids.get(name);
|
|
1470
|
+
if (id === void 0) {
|
|
1471
|
+
id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
|
|
1472
|
+
ids.set(name, id);
|
|
1473
|
+
}
|
|
1474
|
+
out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
1475
|
+
last = m.index + m[0].length;
|
|
1476
|
+
}
|
|
1477
|
+
return out + xmlEscape2(text.slice(last));
|
|
1478
|
+
}
|
|
1479
|
+
function renderPluralIcu(forms, ids) {
|
|
1480
|
+
const cats = [
|
|
1481
|
+
...Object.keys(forms).filter((c) => c.startsWith("=")),
|
|
1482
|
+
...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
|
|
1483
|
+
];
|
|
1484
|
+
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
|
|
1485
|
+
return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
|
|
1486
|
+
}
|
|
1487
|
+
function renderEmbeddedIcu(value) {
|
|
1488
|
+
const renamed = value.replace(
|
|
1489
|
+
/\{\s*\w+\s*,\s*(plural|select|selectordinal)\s*,/g,
|
|
1490
|
+
(_, type) => `{${type === "plural" ? "VAR_PLURAL" : "VAR_SELECT"}, ${type},`
|
|
1491
|
+
);
|
|
1492
|
+
return xmlEscape2(renamed);
|
|
1493
|
+
}
|
|
1494
|
+
function renderScalar(value, ids) {
|
|
1495
|
+
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
|
|
1496
|
+
}
|
|
1497
|
+
var DEFAULT_LOCALE_CASE7, angularXliff;
|
|
1498
|
+
var init_angular_xliff = __esm({
|
|
1499
|
+
"src/server/adapters/angular-xliff.ts"() {
|
|
1500
|
+
"use strict";
|
|
1501
|
+
init_adapters();
|
|
1502
|
+
init_options();
|
|
1503
|
+
init_placeholders();
|
|
1504
|
+
init_schema();
|
|
1505
|
+
DEFAULT_LOCALE_CASE7 = "bcp47-hyphen";
|
|
1506
|
+
angularXliff = {
|
|
1507
|
+
name: "angular-xliff",
|
|
1508
|
+
capabilities: {
|
|
1509
|
+
plural: "native",
|
|
1510
|
+
select: "native",
|
|
1511
|
+
nesting: "flat",
|
|
1512
|
+
metadata: true,
|
|
1513
|
+
placeholderStyle: "icu",
|
|
1514
|
+
fileGrouping: "per-locale"
|
|
1515
|
+
},
|
|
1516
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE7,
|
|
1517
|
+
export(state, output) {
|
|
1518
|
+
const files = [];
|
|
1519
|
+
const warnings = [];
|
|
1520
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
|
|
1521
|
+
const sourceLocale = state.config.sourceLocale;
|
|
1522
|
+
const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE7);
|
|
1523
|
+
const emptyAs = resolveEmptyAs(output, "source");
|
|
1524
|
+
const keys = Object.keys(state.keys).sort();
|
|
1525
|
+
for (const locale of state.config.locales) {
|
|
1526
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7);
|
|
1527
|
+
const units = [];
|
|
1528
|
+
for (const key of keys) {
|
|
1529
|
+
const entry = state.keys[key];
|
|
1530
|
+
let source;
|
|
1531
|
+
let target;
|
|
1532
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1533
|
+
if (entry.plural) {
|
|
1534
|
+
const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
|
|
1535
|
+
if (targetForms === null) continue;
|
|
1536
|
+
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
|
|
1537
|
+
target = renderPluralIcu(targetForms, ids);
|
|
1538
|
+
} else {
|
|
1539
|
+
const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
|
|
1540
|
+
if (targetValue === null) continue;
|
|
1541
|
+
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
|
|
1542
|
+
target = renderScalar(targetValue, ids);
|
|
1543
|
+
}
|
|
1544
|
+
units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
|
|
1545
|
+
units.push(` <source>${source}</source>`);
|
|
1546
|
+
units.push(` <target>${target}</target>`);
|
|
1547
|
+
if (entry.description) {
|
|
1548
|
+
units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
|
|
1549
|
+
}
|
|
1550
|
+
units.push(` </trans-unit>`);
|
|
1551
|
+
}
|
|
1552
|
+
const contents = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
1553
|
+
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
|
1554
|
+
<file source-language="${xmlEscape2(sourceToken)}" target-language="${xmlEscape2(token)}" datatype="plaintext" original="ng2.template">
|
|
1555
|
+
<body>
|
|
1556
|
+
` + (units.length ? units.join("\n") + "\n" : "") + ` </body>
|
|
1557
|
+
</file>
|
|
1558
|
+
</xliff>
|
|
1559
|
+
`;
|
|
1560
|
+
files.push({ path: resolvePath(output.path, token), contents });
|
|
1561
|
+
}
|
|
1562
|
+
return { files, warnings };
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1460
1568
|
// src/server/adapters/index.ts
|
|
1461
1569
|
function resolvePath(template, locale, namespace = "") {
|
|
1462
1570
|
return template.replaceAll("{locale}", locale).replaceAll("{namespace}", namespace);
|
|
@@ -1488,7 +1596,8 @@ function getRegistry() {
|
|
|
1488
1596
|
[i18nextJson.name]: i18nextJson,
|
|
1489
1597
|
[gettextPo.name]: gettextPo,
|
|
1490
1598
|
[appleStringsdict.name]: appleStringsdict,
|
|
1491
|
-
[vueI18nJson.name]: vueI18nJson
|
|
1599
|
+
[vueI18nJson.name]: vueI18nJson,
|
|
1600
|
+
[angularXliff.name]: angularXliff
|
|
1492
1601
|
};
|
|
1493
1602
|
}
|
|
1494
1603
|
function getAdapter(name) {
|
|
@@ -1507,12 +1616,13 @@ var init_adapters = __esm({
|
|
|
1507
1616
|
init_gettext_po();
|
|
1508
1617
|
init_apple_stringsdict();
|
|
1509
1618
|
init_vue_i18n_json();
|
|
1619
|
+
init_angular_xliff();
|
|
1510
1620
|
}
|
|
1511
1621
|
});
|
|
1512
1622
|
|
|
1513
1623
|
// src/server/export-run.ts
|
|
1514
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
1515
|
-
import { resolve } from "path";
|
|
1624
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, rmdirSync, statSync, unlinkSync } from "fs";
|
|
1625
|
+
import { dirname as dirname2, resolve, sep } from "path";
|
|
1516
1626
|
function effectiveLocales(config) {
|
|
1517
1627
|
const limit = config.exportLocales;
|
|
1518
1628
|
if (!limit || limit.length === 0) return config.locales;
|
|
@@ -1523,14 +1633,81 @@ function narrowForExport(state) {
|
|
|
1523
1633
|
if (locales.length === state.config.locales.length) return state;
|
|
1524
1634
|
return { ...state, config: { ...state.config, locales } };
|
|
1525
1635
|
}
|
|
1636
|
+
function escapeRegExp(s) {
|
|
1637
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1638
|
+
}
|
|
1639
|
+
function segmentRegExp(segment) {
|
|
1640
|
+
const pattern = escapeRegExp(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
|
|
1641
|
+
return new RegExp(`^${pattern}$`);
|
|
1642
|
+
}
|
|
1643
|
+
function removeEmptyDirs(dir, stopAt) {
|
|
1644
|
+
let current = dir;
|
|
1645
|
+
while (current !== stopAt && current.startsWith(stopAt + sep)) {
|
|
1646
|
+
try {
|
|
1647
|
+
rmdirSync(current);
|
|
1648
|
+
} catch {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
current = dirname2(current);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
1655
|
+
const segments = output.path.split("/").filter(Boolean);
|
|
1656
|
+
if (!segments.some((s) => s.includes("{locale}"))) return 0;
|
|
1657
|
+
const root = resolve(projectRoot);
|
|
1658
|
+
let deleted = 0;
|
|
1659
|
+
const stale = (token) => token !== void 0 && !validTokens.has(token) && LOCALE_TOKEN.test(token);
|
|
1660
|
+
const visit = (dir, index, locale) => {
|
|
1661
|
+
const segment = segments[index];
|
|
1662
|
+
const isLast = index === segments.length - 1;
|
|
1663
|
+
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
1664
|
+
const next = resolve(dir, segment);
|
|
1665
|
+
if (isLast) {
|
|
1666
|
+
if (stale(locale) && existsSync3(next) && statSync(next).isFile()) {
|
|
1667
|
+
unlinkSync(next);
|
|
1668
|
+
deleted++;
|
|
1669
|
+
removeEmptyDirs(dir, root);
|
|
1670
|
+
}
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
visit(next, index + 1, locale);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const re = segmentRegExp(segment);
|
|
1677
|
+
let entries;
|
|
1678
|
+
try {
|
|
1679
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1680
|
+
} catch {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
for (const entry of entries) {
|
|
1684
|
+
const m = entry.name.match(re);
|
|
1685
|
+
if (!m) continue;
|
|
1686
|
+
const token = m.groups?.locale ?? locale;
|
|
1687
|
+
if (isLast) {
|
|
1688
|
+
if (!entry.isFile() || !stale(token)) continue;
|
|
1689
|
+
unlinkSync(resolve(dir, entry.name));
|
|
1690
|
+
deleted++;
|
|
1691
|
+
removeEmptyDirs(dir, root);
|
|
1692
|
+
} else if (entry.isDirectory()) {
|
|
1693
|
+
visit(resolve(dir, entry.name), index + 1, token);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
visit(root, 0, void 0);
|
|
1698
|
+
return deleted;
|
|
1699
|
+
}
|
|
1526
1700
|
function exportToDisk(state, projectRoot, opts) {
|
|
1701
|
+
const allLocales = state.config.locales;
|
|
1527
1702
|
state = narrowForExport(state);
|
|
1528
1703
|
const outputs = opts?.adapter ? state.config.outputs.filter((o) => o.adapter === opts.adapter) : state.config.outputs;
|
|
1529
1704
|
const warnings = [];
|
|
1530
1705
|
let written = 0;
|
|
1531
1706
|
let skipped = 0;
|
|
1707
|
+
let deleted = 0;
|
|
1532
1708
|
for (const output of outputs) {
|
|
1533
|
-
const
|
|
1709
|
+
const adapter = getAdapter(output.adapter);
|
|
1710
|
+
const result = adapter.export(state, output);
|
|
1534
1711
|
warnings.push(...result.warnings);
|
|
1535
1712
|
const writtenPaths = /* @__PURE__ */ new Set();
|
|
1536
1713
|
for (const f of result.files) {
|
|
@@ -1552,14 +1729,19 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
1552
1729
|
writeFileAtomic(abs, f.contents);
|
|
1553
1730
|
written++;
|
|
1554
1731
|
}
|
|
1732
|
+
const validTokens = new Set(allLocales.map((l) => resolveLocaleToken(output, l, adapter.defaultLocaleCase)));
|
|
1733
|
+
deleted += pruneStaleLocaleFiles(output, validTokens, projectRoot);
|
|
1555
1734
|
}
|
|
1556
|
-
return { written, skipped, warnings };
|
|
1735
|
+
return { written, skipped, deleted, warnings };
|
|
1557
1736
|
}
|
|
1737
|
+
var LOCALE_TOKEN;
|
|
1558
1738
|
var init_export_run = __esm({
|
|
1559
1739
|
"src/server/export-run.ts"() {
|
|
1560
1740
|
"use strict";
|
|
1561
1741
|
init_adapters();
|
|
1742
|
+
init_options();
|
|
1562
1743
|
init_atomic_write();
|
|
1744
|
+
LOCALE_TOKEN = /^[a-z]{2,3}([_-][a-z0-9]+)*$/i;
|
|
1563
1745
|
}
|
|
1564
1746
|
});
|
|
1565
1747
|
|
|
@@ -1590,6 +1772,7 @@ function buildSystemPrompt(hasPluralItems) {
|
|
|
1590
1772
|
function buildBatchPrompt(reqs) {
|
|
1591
1773
|
const targetLocale = reqs[0]?.targetLocale ?? "";
|
|
1592
1774
|
const hasPluralItems = reqs.some((r) => r.plural !== void 0);
|
|
1775
|
+
const hasGlossaryItems = reqs.some((r) => r.glossary !== void 0 && r.glossary.length > 0);
|
|
1593
1776
|
const items = reqs.map((r) => {
|
|
1594
1777
|
const base = {
|
|
1595
1778
|
id: r.id,
|
|
@@ -1599,7 +1782,7 @@ function buildBatchPrompt(reqs) {
|
|
|
1599
1782
|
// Wrap in braces so the model sees "{site}" not "site" — makes the visual
|
|
1600
1783
|
// connection to the source string obvious and reduces rename errors.
|
|
1601
1784
|
placeholders: r.placeholders.map((p) => `{${p}}`),
|
|
1602
|
-
glossary: r.glossary
|
|
1785
|
+
...r.glossary?.length ? { glossary: r.glossary } : {},
|
|
1603
1786
|
hasScreenshot: r.image !== void 0
|
|
1604
1787
|
};
|
|
1605
1788
|
if (r.plural) {
|
|
@@ -1609,7 +1792,7 @@ function buildBatchPrompt(reqs) {
|
|
|
1609
1792
|
});
|
|
1610
1793
|
const returnFormat = hasPluralItems ? 'For a scalar item (has `source`) return {"id","translation"}; for a plural item (has `plural`) return {"id","forms"} with one string per required category.' : 'Return {"id","translation"} for each item.';
|
|
1611
1794
|
return `Translate every item below into the target locale: ${targetLocale}. All items share this one target language.
|
|
1612
|
-
Glossary entries are constraints you MUST apply. Items with hasScreenshot:true have a screenshot supplied as a separate image block above; use it for context. ${returnFormat} Return JSON {"items":[\u2026]}.
|
|
1795
|
+
` + (hasGlossaryItems ? "Glossary entries are constraints you MUST apply. " : "") + `Items with hasScreenshot:true have a screenshot supplied as a separate image block above; use it for context. ${returnFormat} Return JSON {"items":[\u2026]}.
|
|
1613
1796
|
` + JSON.stringify(items, null, 2);
|
|
1614
1797
|
}
|
|
1615
1798
|
function buildTranslateGemmaSystemPrompt(sourceLocale, targetLocale) {
|
|
@@ -2231,13 +2414,13 @@ var init_ai = __esm({
|
|
|
2231
2414
|
});
|
|
2232
2415
|
|
|
2233
2416
|
// src/server/glotfile-dir.ts
|
|
2234
|
-
import { existsSync as
|
|
2417
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2235
2418
|
import { resolve as resolve2 } from "path";
|
|
2236
2419
|
function ensureGlotfileDir(projectRoot) {
|
|
2237
2420
|
const dir = resolve2(projectRoot, ".glotfile");
|
|
2238
2421
|
mkdirSync3(dir, { recursive: true });
|
|
2239
2422
|
const ignore = resolve2(dir, ".gitignore");
|
|
2240
|
-
if (!
|
|
2423
|
+
if (!existsSync4(ignore)) {
|
|
2241
2424
|
try {
|
|
2242
2425
|
writeFileSync2(ignore, "*\n");
|
|
2243
2426
|
} catch {
|
|
@@ -2348,7 +2531,7 @@ var init_glob = __esm({
|
|
|
2348
2531
|
});
|
|
2349
2532
|
|
|
2350
2533
|
// src/server/ai/run.ts
|
|
2351
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
2534
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
2352
2535
|
import { resolve as resolve4, extname } from "path";
|
|
2353
2536
|
function selectRequests(state, opts) {
|
|
2354
2537
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
@@ -2431,7 +2614,7 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
2431
2614
|
if (!mediaType) continue;
|
|
2432
2615
|
if (!cache2.has(screenshot)) {
|
|
2433
2616
|
const abs = resolve4(projectRoot, screenshot);
|
|
2434
|
-
if (!
|
|
2617
|
+
if (!existsSync5(abs)) {
|
|
2435
2618
|
cache2.set(screenshot, null);
|
|
2436
2619
|
} else {
|
|
2437
2620
|
const buf = readFileSync5(abs);
|
|
@@ -2529,7 +2712,7 @@ var init_run = __esm({
|
|
|
2529
2712
|
});
|
|
2530
2713
|
|
|
2531
2714
|
// src/server/log.ts
|
|
2532
|
-
import { appendFileSync, readFileSync as readFileSync6, existsSync as
|
|
2715
|
+
import { appendFileSync, readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
2533
2716
|
import { resolve as resolve5 } from "path";
|
|
2534
2717
|
function logPath(projectRoot) {
|
|
2535
2718
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -2540,7 +2723,7 @@ function appendLog(projectRoot, entry) {
|
|
|
2540
2723
|
}
|
|
2541
2724
|
function readLog(projectRoot, limit = 100) {
|
|
2542
2725
|
const path = logPath(projectRoot);
|
|
2543
|
-
if (!
|
|
2726
|
+
if (!existsSync6(path)) return [];
|
|
2544
2727
|
const lines = readFileSync6(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
2545
2728
|
const entries = lines.map((l) => JSON.parse(l));
|
|
2546
2729
|
return entries.reverse().slice(0, limit);
|
|
@@ -2553,11 +2736,11 @@ var init_log = __esm({
|
|
|
2553
2736
|
});
|
|
2554
2737
|
|
|
2555
2738
|
// src/server/scan.ts
|
|
2556
|
-
import { existsSync as
|
|
2739
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
2557
2740
|
import { resolve as resolve6 } from "path";
|
|
2558
2741
|
function loadUsageCache(projectRoot) {
|
|
2559
2742
|
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
2560
|
-
if (!
|
|
2743
|
+
if (!existsSync7(path)) return null;
|
|
2561
2744
|
try {
|
|
2562
2745
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2563
2746
|
} catch {
|
|
@@ -2573,8 +2756,10 @@ function findMissing(state) {
|
|
|
2573
2756
|
const targets = state.config.locales.filter((l) => l !== state.config.sourceLocale).sort();
|
|
2574
2757
|
const out = [];
|
|
2575
2758
|
for (const key of Object.keys(state.keys).sort()) {
|
|
2759
|
+
const entry = state.keys[key];
|
|
2760
|
+
if (entry.skipTranslate) continue;
|
|
2576
2761
|
for (const locale of targets) {
|
|
2577
|
-
const v =
|
|
2762
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
|
|
2578
2763
|
if (!v) out.push({ key, locale });
|
|
2579
2764
|
}
|
|
2580
2765
|
}
|
|
@@ -2600,7 +2785,7 @@ var init_scan = __esm({
|
|
|
2600
2785
|
});
|
|
2601
2786
|
|
|
2602
2787
|
// src/server/scanner.ts
|
|
2603
|
-
import { readdirSync as
|
|
2788
|
+
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync8 } from "fs";
|
|
2604
2789
|
import { join as join3, extname as extname2, relative } from "path";
|
|
2605
2790
|
function scannerForExt(ext) {
|
|
2606
2791
|
return EXT_SCANNER[ext] ?? null;
|
|
@@ -2721,7 +2906,7 @@ function isIncluded(relPath, includePatterns) {
|
|
|
2721
2906
|
function* walkFiles(dir, root, exclude) {
|
|
2722
2907
|
let entries;
|
|
2723
2908
|
try {
|
|
2724
|
-
entries =
|
|
2909
|
+
entries = readdirSync3(dir);
|
|
2725
2910
|
} catch {
|
|
2726
2911
|
return;
|
|
2727
2912
|
}
|
|
@@ -2731,7 +2916,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
2731
2916
|
const rel = relative(root, abs);
|
|
2732
2917
|
let st;
|
|
2733
2918
|
try {
|
|
2734
|
-
st =
|
|
2919
|
+
st = statSync2(abs);
|
|
2735
2920
|
} catch {
|
|
2736
2921
|
continue;
|
|
2737
2922
|
}
|
|
@@ -2760,7 +2945,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
2760
2945
|
const abs = join3(projectRoot, relPath);
|
|
2761
2946
|
let st;
|
|
2762
2947
|
try {
|
|
2763
|
-
st =
|
|
2948
|
+
st = statSync2(abs);
|
|
2764
2949
|
} catch {
|
|
2765
2950
|
continue;
|
|
2766
2951
|
}
|
|
@@ -2878,7 +3063,7 @@ var init_scanner = __esm({
|
|
|
2878
3063
|
});
|
|
2879
3064
|
|
|
2880
3065
|
// src/server/ai/context.ts
|
|
2881
|
-
import { existsSync as
|
|
3066
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
2882
3067
|
import { resolve as resolve7 } from "path";
|
|
2883
3068
|
function globToRegExp2(glob) {
|
|
2884
3069
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -2893,7 +3078,7 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
2893
3078
|
for (const ref of selected) {
|
|
2894
3079
|
const absPath = resolve7(projectRoot, ref.file);
|
|
2895
3080
|
if (!fileCache.has(ref.file)) {
|
|
2896
|
-
if (!
|
|
3081
|
+
if (!existsSync8(absPath)) continue;
|
|
2897
3082
|
const content = readFileSync9(absPath, "utf8");
|
|
2898
3083
|
fileCache.set(ref.file, content.split("\n"));
|
|
2899
3084
|
}
|
|
@@ -3038,22 +3223,364 @@ var init_context = __esm({
|
|
|
3038
3223
|
}
|
|
3039
3224
|
});
|
|
3040
3225
|
|
|
3226
|
+
// src/server/lint/spelling.ts
|
|
3227
|
+
function tokenize(text) {
|
|
3228
|
+
return text.match(new RegExp("\\p{L}[\\p{L}'']*", "gu")) ?? [];
|
|
3229
|
+
}
|
|
3230
|
+
function buildAllowWords(glossary, dictionary2 = []) {
|
|
3231
|
+
const set = /* @__PURE__ */ new Set();
|
|
3232
|
+
const add = (s) => {
|
|
3233
|
+
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
3234
|
+
};
|
|
3235
|
+
for (const g of glossary) add(g.term);
|
|
3236
|
+
for (const w of dictionary2) add(w);
|
|
3237
|
+
return set;
|
|
3238
|
+
}
|
|
3239
|
+
var spellingRule, defaultLoader;
|
|
3240
|
+
var init_spelling = __esm({
|
|
3241
|
+
"src/server/lint/spelling.ts"() {
|
|
3242
|
+
"use strict";
|
|
3243
|
+
init_placeholders();
|
|
3244
|
+
spellingRule = {
|
|
3245
|
+
id: "spelling",
|
|
3246
|
+
run(state, ctx) {
|
|
3247
|
+
const out = [];
|
|
3248
|
+
for (const key of Object.keys(state.keys)) {
|
|
3249
|
+
const entry = state.keys[key];
|
|
3250
|
+
for (const locale of ctx.targetLocales) {
|
|
3251
|
+
const speller = ctx.spellers.get(locale);
|
|
3252
|
+
if (!speller) continue;
|
|
3253
|
+
const value = entry.values[locale]?.value;
|
|
3254
|
+
if (!value) continue;
|
|
3255
|
+
const placeholders = new Set(extractPlaceholders(value).map((p) => p.toLowerCase()));
|
|
3256
|
+
for (const word of tokenize(value)) {
|
|
3257
|
+
const lower = word.toLowerCase();
|
|
3258
|
+
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
3259
|
+
if (!speller.correct(word)) {
|
|
3260
|
+
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
return out;
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
defaultLoader = async (dictId) => {
|
|
3269
|
+
try {
|
|
3270
|
+
const nspellMod = await import("nspell");
|
|
3271
|
+
const nspell2 = nspellMod.default ?? nspellMod;
|
|
3272
|
+
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
3273
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
3274
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
3275
|
+
return nspell2(dict);
|
|
3276
|
+
} catch {
|
|
3277
|
+
return null;
|
|
3278
|
+
}
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
3282
|
+
|
|
3283
|
+
// src/server/lint/rules.ts
|
|
3284
|
+
var emptySourceRule, emptyTranslationRule, identicalToSourceRule, whitespaceRule, placeholderMismatchRule, icuMismatchRule, maxLengthRule, glossaryViolationRule, ALL_RULES;
|
|
3285
|
+
var init_rules = __esm({
|
|
3286
|
+
"src/server/lint/rules.ts"() {
|
|
3287
|
+
"use strict";
|
|
3288
|
+
init_scan();
|
|
3289
|
+
init_placeholders();
|
|
3290
|
+
init_run();
|
|
3291
|
+
init_spelling();
|
|
3292
|
+
emptySourceRule = {
|
|
3293
|
+
id: "empty-source",
|
|
3294
|
+
run(state, ctx) {
|
|
3295
|
+
const out = [];
|
|
3296
|
+
for (const key of Object.keys(state.keys)) {
|
|
3297
|
+
const entry = state.keys[key];
|
|
3298
|
+
const v = entry.plural ? entry.values[ctx.sourceLocale]?.forms?.other : entry.values[ctx.sourceLocale]?.value;
|
|
3299
|
+
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
3300
|
+
}
|
|
3301
|
+
return out;
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
emptyTranslationRule = {
|
|
3305
|
+
id: "empty-translation",
|
|
3306
|
+
run(state, ctx) {
|
|
3307
|
+
const out = [];
|
|
3308
|
+
for (const m of findMissing(state)) {
|
|
3309
|
+
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
3310
|
+
}
|
|
3311
|
+
for (const key of Object.keys(state.keys)) {
|
|
3312
|
+
for (const locale of ctx.targetLocales) {
|
|
3313
|
+
const v = state.keys[key].values[locale]?.value;
|
|
3314
|
+
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
return out;
|
|
3318
|
+
}
|
|
3319
|
+
};
|
|
3320
|
+
identicalToSourceRule = {
|
|
3321
|
+
id: "identical-to-source",
|
|
3322
|
+
run(state, ctx) {
|
|
3323
|
+
const out = [];
|
|
3324
|
+
for (const key of Object.keys(state.keys)) {
|
|
3325
|
+
const entry = state.keys[key];
|
|
3326
|
+
if (entry.skipTranslate) continue;
|
|
3327
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3328
|
+
if (!src) continue;
|
|
3329
|
+
for (const locale of ctx.targetLocales) {
|
|
3330
|
+
const v = entry.values[locale]?.value;
|
|
3331
|
+
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
return out;
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
whitespaceRule = {
|
|
3338
|
+
id: "whitespace",
|
|
3339
|
+
run(state, ctx) {
|
|
3340
|
+
const out = [];
|
|
3341
|
+
for (const key of Object.keys(state.keys)) {
|
|
3342
|
+
const entry = state.keys[key];
|
|
3343
|
+
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
3344
|
+
const srcEdge = src !== src.trim();
|
|
3345
|
+
for (const locale of ctx.targetLocales) {
|
|
3346
|
+
const v = entry.values[locale]?.value;
|
|
3347
|
+
if (!v) continue;
|
|
3348
|
+
if (v !== v.trim() !== srcEdge) {
|
|
3349
|
+
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
return out;
|
|
3354
|
+
}
|
|
3355
|
+
};
|
|
3356
|
+
placeholderMismatchRule = {
|
|
3357
|
+
id: "placeholder-mismatch",
|
|
3358
|
+
run(state, ctx) {
|
|
3359
|
+
const out = [];
|
|
3360
|
+
for (const key of Object.keys(state.keys)) {
|
|
3361
|
+
const entry = state.keys[key];
|
|
3362
|
+
if (entry.plural) {
|
|
3363
|
+
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
3364
|
+
if (!srcForm) continue;
|
|
3365
|
+
for (const locale of ctx.targetLocales) {
|
|
3366
|
+
const forms = entry.values[locale]?.forms;
|
|
3367
|
+
if (!forms) continue;
|
|
3368
|
+
const bad = Object.entries(forms).some(
|
|
3369
|
+
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
3370
|
+
);
|
|
3371
|
+
if (bad) {
|
|
3372
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3378
|
+
if (!src) continue;
|
|
3379
|
+
for (const locale of ctx.targetLocales) {
|
|
3380
|
+
const v = entry.values[locale]?.value;
|
|
3381
|
+
if (!v) continue;
|
|
3382
|
+
if (!placeholdersMatch(src, v)) {
|
|
3383
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
return out;
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
icuMismatchRule = {
|
|
3391
|
+
id: "icu-mismatch",
|
|
3392
|
+
run(state, ctx) {
|
|
3393
|
+
const out = [];
|
|
3394
|
+
for (const key of Object.keys(state.keys)) {
|
|
3395
|
+
const entry = state.keys[key];
|
|
3396
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3397
|
+
if (!src) continue;
|
|
3398
|
+
const srcIcu = isIcuPluralOrSelect(src);
|
|
3399
|
+
for (const locale of ctx.targetLocales) {
|
|
3400
|
+
const v = entry.values[locale]?.value;
|
|
3401
|
+
if (!v) continue;
|
|
3402
|
+
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
3403
|
+
out.push({
|
|
3404
|
+
ruleId: "icu-mismatch",
|
|
3405
|
+
key,
|
|
3406
|
+
locale,
|
|
3407
|
+
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
return out;
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
maxLengthRule = {
|
|
3416
|
+
id: "max-length",
|
|
3417
|
+
run(state, ctx) {
|
|
3418
|
+
const out = [];
|
|
3419
|
+
for (const key of Object.keys(state.keys)) {
|
|
3420
|
+
const entry = state.keys[key];
|
|
3421
|
+
const max = entry.maxLength;
|
|
3422
|
+
if (max == null) continue;
|
|
3423
|
+
for (const locale of ctx.targetLocales) {
|
|
3424
|
+
const v = entry.values[locale]?.value;
|
|
3425
|
+
if (v && v.length > max) {
|
|
3426
|
+
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
return out;
|
|
3431
|
+
}
|
|
3432
|
+
};
|
|
3433
|
+
glossaryViolationRule = {
|
|
3434
|
+
id: "glossary-violation",
|
|
3435
|
+
run(state, ctx) {
|
|
3436
|
+
const out = [];
|
|
3437
|
+
for (const key of Object.keys(state.keys)) {
|
|
3438
|
+
const entry = state.keys[key];
|
|
3439
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3440
|
+
if (!src) continue;
|
|
3441
|
+
for (const locale of ctx.targetLocales) {
|
|
3442
|
+
const v = entry.values[locale]?.value;
|
|
3443
|
+
if (!v) continue;
|
|
3444
|
+
for (const hint of relevantGlossary(src, locale, ctx.glossary)) {
|
|
3445
|
+
if (hint.doNotTranslate && !v.includes(hint.term)) {
|
|
3446
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `do-not-translate term "${hint.term}" is missing or altered` });
|
|
3447
|
+
}
|
|
3448
|
+
if (hint.forced && !v.includes(hint.forced)) {
|
|
3449
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `expected glossary translation "${hint.forced}" for "${hint.term}"` });
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return out;
|
|
3455
|
+
}
|
|
3456
|
+
};
|
|
3457
|
+
ALL_RULES = [
|
|
3458
|
+
emptySourceRule,
|
|
3459
|
+
emptyTranslationRule,
|
|
3460
|
+
placeholderMismatchRule,
|
|
3461
|
+
icuMismatchRule,
|
|
3462
|
+
glossaryViolationRule,
|
|
3463
|
+
maxLengthRule,
|
|
3464
|
+
identicalToSourceRule,
|
|
3465
|
+
whitespaceRule,
|
|
3466
|
+
spellingRule
|
|
3467
|
+
];
|
|
3468
|
+
}
|
|
3469
|
+
});
|
|
3470
|
+
|
|
3471
|
+
// src/server/lint/run.ts
|
|
3472
|
+
function resolveSeverity(id, config) {
|
|
3473
|
+
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
3474
|
+
}
|
|
3475
|
+
function sortFindings(findings) {
|
|
3476
|
+
return [...findings].sort(
|
|
3477
|
+
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
3478
|
+
);
|
|
3479
|
+
}
|
|
3480
|
+
function countSeverities(findings) {
|
|
3481
|
+
let error = 0, warn = 0;
|
|
3482
|
+
for (const f of findings) f.severity === "error" ? error++ : warn++;
|
|
3483
|
+
return { error, warn };
|
|
3484
|
+
}
|
|
3485
|
+
async function loadSpellers(locales, config, load, warn) {
|
|
3486
|
+
const map = /* @__PURE__ */ new Map();
|
|
3487
|
+
for (const locale of locales) {
|
|
3488
|
+
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
3489
|
+
const speller = await load(dictId);
|
|
3490
|
+
if (speller) map.set(locale, speller);
|
|
3491
|
+
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
3492
|
+
}
|
|
3493
|
+
return map;
|
|
3494
|
+
}
|
|
3495
|
+
async function runLint(state, options = {}) {
|
|
3496
|
+
const config = state.config.lint ?? {};
|
|
3497
|
+
const rules = options.rules ?? ALL_RULES;
|
|
3498
|
+
const warn = options.warn ?? ((m) => console.warn(m));
|
|
3499
|
+
const load = options.loadSpeller ?? defaultLoader;
|
|
3500
|
+
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
3501
|
+
const isActive = (rule) => {
|
|
3502
|
+
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
3503
|
+
return resolveSeverity(rule.id, config) !== "off";
|
|
3504
|
+
};
|
|
3505
|
+
const active = rules.filter(isActive);
|
|
3506
|
+
const spellingOn = active.some((r) => r.id === "spelling");
|
|
3507
|
+
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
3508
|
+
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
|
|
3509
|
+
const ctx = {
|
|
3510
|
+
config,
|
|
3511
|
+
sourceLocale: state.config.sourceLocale,
|
|
3512
|
+
targetLocales,
|
|
3513
|
+
glossary: state.glossary,
|
|
3514
|
+
spellers,
|
|
3515
|
+
allowWords
|
|
3516
|
+
};
|
|
3517
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp);
|
|
3518
|
+
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
3519
|
+
const findings = [];
|
|
3520
|
+
for (const rule of active) {
|
|
3521
|
+
const severity = resolveSeverity(rule.id, config);
|
|
3522
|
+
for (const raw of rule.run(state, ctx)) {
|
|
3523
|
+
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
3524
|
+
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
3525
|
+
findings.push({ ...raw, severity });
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
const sorted = sortFindings(findings);
|
|
3529
|
+
const counts = countSeverities(sorted);
|
|
3530
|
+
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
3531
|
+
}
|
|
3532
|
+
var init_run2 = __esm({
|
|
3533
|
+
"src/server/lint/run.ts"() {
|
|
3534
|
+
"use strict";
|
|
3535
|
+
init_glob();
|
|
3536
|
+
init_registry();
|
|
3537
|
+
init_rules();
|
|
3538
|
+
init_spelling();
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3542
|
+
// src/server/lint/outputs.ts
|
|
3543
|
+
import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
|
|
3544
|
+
import { resolve as resolve8 } from "path";
|
|
3545
|
+
function checkOutputs(state, root) {
|
|
3546
|
+
const out = [];
|
|
3547
|
+
for (const output of state.config.outputs) {
|
|
3548
|
+
const result = getAdapter(output.adapter).export(state, output);
|
|
3549
|
+
for (const file of result.files) {
|
|
3550
|
+
const abs = resolve8(root, file.path);
|
|
3551
|
+
const current = existsSync9(abs) ? readFileSync10(abs, "utf8") : null;
|
|
3552
|
+
if (current === null) {
|
|
3553
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
3554
|
+
} else if (current !== file.contents) {
|
|
3555
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
return out;
|
|
3560
|
+
}
|
|
3561
|
+
var init_outputs = __esm({
|
|
3562
|
+
"src/server/lint/outputs.ts"() {
|
|
3563
|
+
"use strict";
|
|
3564
|
+
init_adapters();
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
|
|
3041
3568
|
// src/server/import/detect.ts
|
|
3042
|
-
import { existsSync as
|
|
3569
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
3043
3570
|
import { join as join4 } from "path";
|
|
3044
3571
|
function safeIsDir(p) {
|
|
3045
3572
|
try {
|
|
3046
|
-
return
|
|
3573
|
+
return statSync3(p).isDirectory();
|
|
3047
3574
|
} catch {
|
|
3048
3575
|
return false;
|
|
3049
3576
|
}
|
|
3050
3577
|
}
|
|
3051
3578
|
function listDirs(dir) {
|
|
3052
|
-
return
|
|
3579
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join4(dir, e)));
|
|
3053
3580
|
}
|
|
3054
3581
|
function fileCount(dir) {
|
|
3055
3582
|
try {
|
|
3056
|
-
return
|
|
3583
|
+
return readdirSync4(dir).length;
|
|
3057
3584
|
} catch {
|
|
3058
3585
|
return 0;
|
|
3059
3586
|
}
|
|
@@ -3070,15 +3597,16 @@ function detectLaravel(root) {
|
|
|
3070
3597
|
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
3071
3598
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
3072
3599
|
}
|
|
3073
|
-
function detectVue(root) {
|
|
3600
|
+
function detectVue(root, forced = false) {
|
|
3074
3601
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
3075
3602
|
const localeRoot = join4(root, rel);
|
|
3076
3603
|
if (!safeIsDir(localeRoot)) continue;
|
|
3077
|
-
const locales =
|
|
3078
|
-
|
|
3604
|
+
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
3605
|
+
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
3606
|
+
if (enough) {
|
|
3079
3607
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
3080
3608
|
try {
|
|
3081
|
-
return
|
|
3609
|
+
return statSync3(join4(localeRoot, `${loc}.json`)).size;
|
|
3082
3610
|
} catch {
|
|
3083
3611
|
return 0;
|
|
3084
3612
|
}
|
|
@@ -3092,7 +3620,7 @@ function detectArb(root) {
|
|
|
3092
3620
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
3093
3621
|
const localeRoot = join4(root, rel);
|
|
3094
3622
|
if (!safeIsDir(localeRoot)) continue;
|
|
3095
|
-
const locales =
|
|
3623
|
+
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
3096
3624
|
if (locales.length >= 1) {
|
|
3097
3625
|
return { format: "flutter-arb", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
3098
3626
|
}
|
|
@@ -3100,7 +3628,7 @@ function detectArb(root) {
|
|
|
3100
3628
|
return null;
|
|
3101
3629
|
}
|
|
3102
3630
|
function detect(root, formatOverride) {
|
|
3103
|
-
if (!
|
|
3631
|
+
if (!existsSync10(root)) return null;
|
|
3104
3632
|
if (formatOverride) {
|
|
3105
3633
|
const fn = BY_FORMAT[formatOverride];
|
|
3106
3634
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -3121,7 +3649,7 @@ var init_detect = __esm({
|
|
|
3121
3649
|
DETECTORS = [detectLaravel, detectVue, detectArb];
|
|
3122
3650
|
BY_FORMAT = {
|
|
3123
3651
|
"laravel-php": detectLaravel,
|
|
3124
|
-
"vue-i18n-json": detectVue,
|
|
3652
|
+
"vue-i18n-json": (root) => detectVue(root, true),
|
|
3125
3653
|
"flutter-arb": detectArb
|
|
3126
3654
|
};
|
|
3127
3655
|
}
|
|
@@ -3155,7 +3683,7 @@ var init_flatten = __esm({
|
|
|
3155
3683
|
});
|
|
3156
3684
|
|
|
3157
3685
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
3158
|
-
import { readdirSync as
|
|
3686
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
|
|
3159
3687
|
import { join as join5 } from "path";
|
|
3160
3688
|
var LOCALE_RE2, vueI18nJson2;
|
|
3161
3689
|
var init_vue_i18n_json2 = __esm({
|
|
@@ -3169,7 +3697,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
3169
3697
|
const warnings = [];
|
|
3170
3698
|
const keys = {};
|
|
3171
3699
|
const locales = [];
|
|
3172
|
-
for (const file of
|
|
3700
|
+
for (const file of readdirSync5(localeRoot).sort()) {
|
|
3173
3701
|
if (!file.endsWith(".json")) continue;
|
|
3174
3702
|
const locale = file.slice(0, -".json".length);
|
|
3175
3703
|
if (!LOCALE_RE2.test(locale)) continue;
|
|
@@ -3203,18 +3731,18 @@ var init_placeholders2 = __esm({
|
|
|
3203
3731
|
});
|
|
3204
3732
|
|
|
3205
3733
|
// src/server/import/parsers/laravel-php.ts
|
|
3206
|
-
import { readdirSync as
|
|
3734
|
+
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
3207
3735
|
import { join as join6, relative as relative2 } from "path";
|
|
3208
3736
|
import { execFileSync } from "child_process";
|
|
3209
3737
|
function listDirs2(dir) {
|
|
3210
|
-
return
|
|
3738
|
+
return readdirSync6(dir).filter((e) => statSync4(join6(dir, e)).isDirectory());
|
|
3211
3739
|
}
|
|
3212
3740
|
function listPhpFiles(dir) {
|
|
3213
3741
|
const out = [];
|
|
3214
3742
|
const walk = (d) => {
|
|
3215
|
-
for (const e of
|
|
3743
|
+
for (const e of readdirSync6(d)) {
|
|
3216
3744
|
const full = join6(d, e);
|
|
3217
|
-
if (
|
|
3745
|
+
if (statSync4(full).isDirectory()) walk(full);
|
|
3218
3746
|
else if (e.endsWith(".php")) out.push(full);
|
|
3219
3747
|
}
|
|
3220
3748
|
};
|
|
@@ -3281,7 +3809,7 @@ var init_laravel_php2 = __esm({
|
|
|
3281
3809
|
});
|
|
3282
3810
|
|
|
3283
3811
|
// src/server/import/parsers/flutter-arb.ts
|
|
3284
|
-
import { readdirSync as
|
|
3812
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync12 } from "fs";
|
|
3285
3813
|
import { join as join7 } from "path";
|
|
3286
3814
|
function localeFromArbName(file) {
|
|
3287
3815
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -3315,7 +3843,7 @@ var init_flutter_arb2 = __esm({
|
|
|
3315
3843
|
const warnings = [];
|
|
3316
3844
|
const keys = {};
|
|
3317
3845
|
const locales = [];
|
|
3318
|
-
for (const file of
|
|
3846
|
+
for (const file of readdirSync7(localeRoot).sort()) {
|
|
3319
3847
|
if (!file.endsWith(".arb")) continue;
|
|
3320
3848
|
const locale = localeFromArbName(file);
|
|
3321
3849
|
if (!locale) continue;
|
|
@@ -3501,7 +4029,7 @@ function runImport(opts) {
|
|
|
3501
4029
|
localeCount: state.config.locales.length
|
|
3502
4030
|
};
|
|
3503
4031
|
}
|
|
3504
|
-
var
|
|
4032
|
+
var init_run3 = __esm({
|
|
3505
4033
|
"src/server/import/run.ts"() {
|
|
3506
4034
|
"use strict";
|
|
3507
4035
|
init_detect();
|
|
@@ -3887,11 +4415,11 @@ var init_ui_prefs = __esm({
|
|
|
3887
4415
|
// src/server/api.ts
|
|
3888
4416
|
import { Hono } from "hono";
|
|
3889
4417
|
import { streamSSE } from "hono/streaming";
|
|
3890
|
-
import { readFileSync as readFileSync14, existsSync as
|
|
3891
|
-
import { dirname as
|
|
4418
|
+
import { readFileSync as readFileSync14, existsSync as existsSync11, readdirSync as readdirSync8, statSync as statSync5, rmSync as rmSync4 } from "fs";
|
|
4419
|
+
import { dirname as dirname3, resolve as resolve9, basename, relative as relative3, sep as sep2 } from "path";
|
|
3892
4420
|
function projectName(root) {
|
|
3893
4421
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
3894
|
-
if (
|
|
4422
|
+
if (existsSync11(nameFile)) {
|
|
3895
4423
|
try {
|
|
3896
4424
|
const name = readFileSync14(nameFile, "utf8").trim();
|
|
3897
4425
|
if (name) return name;
|
|
@@ -3903,7 +4431,7 @@ function projectName(root) {
|
|
|
3903
4431
|
function createApi(deps) {
|
|
3904
4432
|
const app = new Hono();
|
|
3905
4433
|
const load = () => loadState(deps.statePath);
|
|
3906
|
-
const projectRoot =
|
|
4434
|
+
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
3907
4435
|
let translateQueue = Promise.resolve();
|
|
3908
4436
|
const withTranslateLock = (fn) => {
|
|
3909
4437
|
const next = translateQueue.then(fn, fn);
|
|
@@ -4001,13 +4529,13 @@ function createApi(deps) {
|
|
|
4001
4529
|
found.set(deps.statePath, {
|
|
4002
4530
|
name: basename(deps.statePath),
|
|
4003
4531
|
path: deps.statePath,
|
|
4004
|
-
relDir: activeRel !== basename(activeRel) ?
|
|
4532
|
+
relDir: activeRel !== basename(activeRel) ? dirname3(activeRel) : void 0
|
|
4005
4533
|
});
|
|
4006
4534
|
function walk(dir, depth) {
|
|
4007
4535
|
if (depth > 4) return;
|
|
4008
4536
|
let entries = [];
|
|
4009
4537
|
try {
|
|
4010
|
-
entries =
|
|
4538
|
+
entries = readdirSync8(dir);
|
|
4011
4539
|
} catch {
|
|
4012
4540
|
return;
|
|
4013
4541
|
}
|
|
@@ -4015,13 +4543,13 @@ function createApi(deps) {
|
|
|
4015
4543
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4016
4544
|
const abs = resolve9(dir, name);
|
|
4017
4545
|
let filePath = null;
|
|
4018
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
4546
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync11(resolve9(abs, "config.json"))) {
|
|
4019
4547
|
filePath = resolve9(dir, `${name}.json`);
|
|
4020
4548
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
4021
4549
|
filePath = abs;
|
|
4022
4550
|
} else {
|
|
4023
4551
|
try {
|
|
4024
|
-
if (
|
|
4552
|
+
if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
|
|
4025
4553
|
} catch {
|
|
4026
4554
|
}
|
|
4027
4555
|
continue;
|
|
@@ -4030,7 +4558,7 @@ function createApi(deps) {
|
|
|
4030
4558
|
try {
|
|
4031
4559
|
loadState(filePath);
|
|
4032
4560
|
const rel = relative3(projectRoot, filePath);
|
|
4033
|
-
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ?
|
|
4561
|
+
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
|
|
4034
4562
|
} catch {
|
|
4035
4563
|
}
|
|
4036
4564
|
}
|
|
@@ -4047,9 +4575,9 @@ function createApi(deps) {
|
|
|
4047
4575
|
const { path } = await c.req.json();
|
|
4048
4576
|
if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
|
|
4049
4577
|
const resolved = resolve9(projectRoot, path);
|
|
4050
|
-
const inside = resolved === projectRoot || resolved.startsWith(projectRoot +
|
|
4578
|
+
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
4051
4579
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
4052
|
-
if (!
|
|
4580
|
+
if (!existsSync11(resolved)) return c.json({ error: "file not found" }, 400);
|
|
4053
4581
|
loadState(resolved);
|
|
4054
4582
|
deps.statePath = resolved;
|
|
4055
4583
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -4106,11 +4634,11 @@ function createApi(deps) {
|
|
|
4106
4634
|
function removeOrphanScreenshot(s, screenshot) {
|
|
4107
4635
|
if (!screenshot) return;
|
|
4108
4636
|
for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
|
|
4109
|
-
const root =
|
|
4637
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4110
4638
|
const abs = resolve9(root, screenshot);
|
|
4111
4639
|
const rel = relative3(root, abs);
|
|
4112
|
-
const seg0 = rel.split(
|
|
4113
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
4640
|
+
const seg0 = rel.split(sep2)[0] ?? "";
|
|
4641
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
|
|
4114
4642
|
try {
|
|
4115
4643
|
rmSync4(abs);
|
|
4116
4644
|
} catch {
|
|
@@ -4362,7 +4890,7 @@ function createApi(deps) {
|
|
|
4362
4890
|
const body = await c.req.parseBody();
|
|
4363
4891
|
const file = body["file"];
|
|
4364
4892
|
if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
|
|
4365
|
-
const root =
|
|
4893
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4366
4894
|
const dirName = screenshotDirName(deps.statePath);
|
|
4367
4895
|
const dir = resolve9(root, dirName);
|
|
4368
4896
|
const filename = `${sanitize(key)}__${sanitize(file.name)}`;
|
|
@@ -4398,6 +4926,23 @@ function createApi(deps) {
|
|
|
4398
4926
|
return c.json({ files, warnings });
|
|
4399
4927
|
});
|
|
4400
4928
|
app.get("/scan/missing", (c) => c.json(findMissing(load())));
|
|
4929
|
+
const spellerCache = /* @__PURE__ */ new Map();
|
|
4930
|
+
const cachedLoader = (dictId) => {
|
|
4931
|
+
let p = spellerCache.get(dictId);
|
|
4932
|
+
if (!p) {
|
|
4933
|
+
p = defaultLoader(dictId);
|
|
4934
|
+
spellerCache.set(dictId, p);
|
|
4935
|
+
}
|
|
4936
|
+
return p;
|
|
4937
|
+
};
|
|
4938
|
+
app.get("/lint", async (c) => {
|
|
4939
|
+
const state = load();
|
|
4940
|
+
const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
|
|
4941
|
+
} });
|
|
4942
|
+
const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
|
|
4943
|
+
const counts = countSeverities(findings);
|
|
4944
|
+
return c.json({ findings, counts, ok: counts.error === 0 });
|
|
4945
|
+
});
|
|
4401
4946
|
app.get("/checks", (c) => {
|
|
4402
4947
|
const param = c.req.query("checks");
|
|
4403
4948
|
const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
|
|
@@ -4433,22 +4978,12 @@ function createApi(deps) {
|
|
|
4433
4978
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
4434
4979
|
});
|
|
4435
4980
|
app.post("/export", (c) => {
|
|
4436
|
-
const
|
|
4437
|
-
const
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
const result = adapter.export(s, output);
|
|
4443
|
-
warnings.push(...result.warnings);
|
|
4444
|
-
for (const f of result.files) {
|
|
4445
|
-
const abs = resolve9(root, f.path);
|
|
4446
|
-
writeFileAtomic(abs, f.contents);
|
|
4447
|
-
count++;
|
|
4448
|
-
}
|
|
4449
|
-
}
|
|
4450
|
-
console.log(`[export] ${count} file(s)${warnings.length ? `, ${warnings.length} warning(s)` : ""}`);
|
|
4451
|
-
return c.json({ files: count, warnings });
|
|
4981
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4982
|
+
const { written, skipped, deleted, warnings } = exportToDisk(load(), root);
|
|
4983
|
+
console.log(
|
|
4984
|
+
`[export] ${written + skipped} file(s)${deleted ? `, ${deleted} removed` : ""}${warnings.length ? `, ${warnings.length} warning(s)` : ""}`
|
|
4985
|
+
);
|
|
4986
|
+
return c.json({ files: written + skipped, warnings });
|
|
4452
4987
|
});
|
|
4453
4988
|
app.post("/translate/stream", async (c) => {
|
|
4454
4989
|
const signal = c.req.raw.signal;
|
|
@@ -4470,7 +5005,7 @@ function createApi(deps) {
|
|
|
4470
5005
|
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4471
5006
|
return;
|
|
4472
5007
|
}
|
|
4473
|
-
const { skipped } = attachScreenshotsForProvider(reqs, s,
|
|
5008
|
+
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4474
5009
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4475
5010
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4476
5011
|
let totalWritten = 0;
|
|
@@ -4548,7 +5083,7 @@ function createApi(deps) {
|
|
|
4548
5083
|
} catch (e) {
|
|
4549
5084
|
return c.json({ error: e.message }, 400);
|
|
4550
5085
|
}
|
|
4551
|
-
const { skipped } = attachScreenshotsForProvider(toTranslate, s,
|
|
5086
|
+
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4552
5087
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4553
5088
|
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4554
5089
|
const latest = load();
|
|
@@ -4735,13 +5270,16 @@ var init_api = __esm({
|
|
|
4735
5270
|
init_context();
|
|
4736
5271
|
init_stats();
|
|
4737
5272
|
init_checks();
|
|
5273
|
+
init_run2();
|
|
5274
|
+
init_outputs();
|
|
5275
|
+
init_spelling();
|
|
4738
5276
|
init_adapters();
|
|
4739
5277
|
init_ai();
|
|
4740
5278
|
init_run();
|
|
4741
5279
|
init_provider();
|
|
4742
5280
|
init_log();
|
|
4743
5281
|
init_schema();
|
|
4744
|
-
|
|
5282
|
+
init_run3();
|
|
4745
5283
|
init_export_run();
|
|
4746
5284
|
init_ui_prefs();
|
|
4747
5285
|
init_local_settings();
|
|
@@ -4760,7 +5298,7 @@ __export(server_exports, {
|
|
|
4760
5298
|
import { Hono as Hono2 } from "hono";
|
|
4761
5299
|
import { serve } from "@hono/node-server";
|
|
4762
5300
|
import { fileURLToPath } from "url";
|
|
4763
|
-
import { dirname as
|
|
5301
|
+
import { dirname as dirname4, join as join9, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
4764
5302
|
import { readFile, stat } from "fs/promises";
|
|
4765
5303
|
import { createServer } from "net";
|
|
4766
5304
|
import open from "open";
|
|
@@ -4777,16 +5315,16 @@ async function readFileResponse(absPath) {
|
|
|
4777
5315
|
}
|
|
4778
5316
|
function buildApp(opts) {
|
|
4779
5317
|
const app = new Hono2();
|
|
4780
|
-
|
|
4781
|
-
|
|
5318
|
+
const apiDeps = { statePath: opts.statePath, autoExport: true };
|
|
5319
|
+
app.route("/api", createApi(apiDeps));
|
|
4782
5320
|
app.get("/:dir/*", async (c, next) => {
|
|
4783
5321
|
const dirSeg = c.req.param("dir");
|
|
4784
5322
|
if (!dirSeg.endsWith("-screenshots")) return next();
|
|
4785
|
-
const shotsRoot = resolve10(
|
|
5323
|
+
const shotsRoot = resolve10(dirname4(resolve10(apiDeps.statePath)), dirSeg);
|
|
4786
5324
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4787
5325
|
const rest = pathname.slice(`/${dirSeg}`.length);
|
|
4788
5326
|
const target = resolve10(shotsRoot, "." + rest);
|
|
4789
|
-
const inside = target === shotsRoot || target.startsWith(shotsRoot +
|
|
5327
|
+
const inside = target === shotsRoot || target.startsWith(shotsRoot + sep3);
|
|
4790
5328
|
if (inside) {
|
|
4791
5329
|
const file = await readFileResponse(target);
|
|
4792
5330
|
if (file) return file;
|
|
@@ -4798,7 +5336,7 @@ function buildApp(opts) {
|
|
|
4798
5336
|
app.get("/*", async (c) => {
|
|
4799
5337
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4800
5338
|
const target = resolve10(root, "." + pathname);
|
|
4801
|
-
const inside = target === root || target.startsWith(root +
|
|
5339
|
+
const inside = target === root || target.startsWith(root + sep3);
|
|
4802
5340
|
if (inside && pathname !== "/") {
|
|
4803
5341
|
const file = await readFileResponse(target);
|
|
4804
5342
|
if (file) return file;
|
|
@@ -4840,7 +5378,7 @@ async function startServer(opts) {
|
|
|
4840
5378
|
});
|
|
4841
5379
|
}
|
|
4842
5380
|
function backgroundScan(statePath) {
|
|
4843
|
-
const projectRoot =
|
|
5381
|
+
const projectRoot = dirname4(resolve10(statePath));
|
|
4844
5382
|
Promise.resolve().then(() => {
|
|
4845
5383
|
const state = loadState(statePath);
|
|
4846
5384
|
const existing = loadUsageCache(projectRoot);
|
|
@@ -4860,7 +5398,7 @@ var init_server = __esm({
|
|
|
4860
5398
|
init_state();
|
|
4861
5399
|
init_scan();
|
|
4862
5400
|
init_scanner();
|
|
4863
|
-
here =
|
|
5401
|
+
here = dirname4(fileURLToPath(import.meta.url));
|
|
4864
5402
|
DEFAULT_UI_DIR = join9(here, "..", "ui");
|
|
4865
5403
|
MIME = {
|
|
4866
5404
|
".html": "text/html; charset=utf-8",
|
|
@@ -4902,327 +5440,10 @@ init_log();
|
|
|
4902
5440
|
init_scan();
|
|
4903
5441
|
init_scanner();
|
|
4904
5442
|
init_context();
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
init_glob();
|
|
4910
|
-
init_registry();
|
|
4911
|
-
|
|
4912
|
-
// src/server/lint/rules.ts
|
|
4913
|
-
init_scan();
|
|
4914
|
-
init_placeholders();
|
|
4915
|
-
init_run();
|
|
4916
|
-
|
|
4917
|
-
// src/server/lint/spelling.ts
|
|
4918
|
-
init_placeholders();
|
|
4919
|
-
function tokenize(text) {
|
|
4920
|
-
return text.match(new RegExp("\\p{L}[\\p{L}'']*", "gu")) ?? [];
|
|
4921
|
-
}
|
|
4922
|
-
function buildAllowWords(glossary, dictionary2 = []) {
|
|
4923
|
-
const set = /* @__PURE__ */ new Set();
|
|
4924
|
-
const add = (s) => {
|
|
4925
|
-
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
4926
|
-
};
|
|
4927
|
-
for (const g of glossary) add(g.term);
|
|
4928
|
-
for (const w of dictionary2) add(w);
|
|
4929
|
-
return set;
|
|
4930
|
-
}
|
|
4931
|
-
var spellingRule = {
|
|
4932
|
-
id: "spelling",
|
|
4933
|
-
run(state, ctx) {
|
|
4934
|
-
const out = [];
|
|
4935
|
-
for (const key of Object.keys(state.keys)) {
|
|
4936
|
-
const entry = state.keys[key];
|
|
4937
|
-
for (const locale of ctx.targetLocales) {
|
|
4938
|
-
const speller = ctx.spellers.get(locale);
|
|
4939
|
-
if (!speller) continue;
|
|
4940
|
-
const value = entry.values[locale]?.value;
|
|
4941
|
-
if (!value) continue;
|
|
4942
|
-
const placeholders = new Set(extractPlaceholders(value).map((p) => p.toLowerCase()));
|
|
4943
|
-
for (const word of tokenize(value)) {
|
|
4944
|
-
const lower = word.toLowerCase();
|
|
4945
|
-
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
4946
|
-
if (!speller.correct(word)) {
|
|
4947
|
-
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
4948
|
-
}
|
|
4949
|
-
}
|
|
4950
|
-
}
|
|
4951
|
-
}
|
|
4952
|
-
return out;
|
|
4953
|
-
}
|
|
4954
|
-
};
|
|
4955
|
-
var defaultLoader = async (dictId) => {
|
|
4956
|
-
try {
|
|
4957
|
-
const nspellMod = await import("nspell");
|
|
4958
|
-
const nspell2 = nspellMod.default ?? nspellMod;
|
|
4959
|
-
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
4960
|
-
const dictExport = dictMod.default ?? dictMod;
|
|
4961
|
-
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
4962
|
-
return nspell2(dict);
|
|
4963
|
-
} catch {
|
|
4964
|
-
return null;
|
|
4965
|
-
}
|
|
4966
|
-
};
|
|
4967
|
-
|
|
4968
|
-
// src/server/lint/rules.ts
|
|
4969
|
-
var emptySourceRule = {
|
|
4970
|
-
id: "empty-source",
|
|
4971
|
-
run(state, ctx) {
|
|
4972
|
-
const out = [];
|
|
4973
|
-
for (const key of Object.keys(state.keys)) {
|
|
4974
|
-
const v = state.keys[key].values[ctx.sourceLocale]?.value;
|
|
4975
|
-
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
4976
|
-
}
|
|
4977
|
-
return out;
|
|
4978
|
-
}
|
|
4979
|
-
};
|
|
4980
|
-
var emptyTranslationRule = {
|
|
4981
|
-
id: "empty-translation",
|
|
4982
|
-
run(state, ctx) {
|
|
4983
|
-
const out = [];
|
|
4984
|
-
for (const m of findMissing(state)) {
|
|
4985
|
-
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
4986
|
-
}
|
|
4987
|
-
for (const key of Object.keys(state.keys)) {
|
|
4988
|
-
for (const locale of ctx.targetLocales) {
|
|
4989
|
-
const v = state.keys[key].values[locale]?.value;
|
|
4990
|
-
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
4991
|
-
}
|
|
4992
|
-
}
|
|
4993
|
-
return out;
|
|
4994
|
-
}
|
|
4995
|
-
};
|
|
4996
|
-
var identicalToSourceRule = {
|
|
4997
|
-
id: "identical-to-source",
|
|
4998
|
-
run(state, ctx) {
|
|
4999
|
-
const out = [];
|
|
5000
|
-
for (const key of Object.keys(state.keys)) {
|
|
5001
|
-
const entry = state.keys[key];
|
|
5002
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5003
|
-
if (!src) continue;
|
|
5004
|
-
for (const locale of ctx.targetLocales) {
|
|
5005
|
-
const v = entry.values[locale]?.value;
|
|
5006
|
-
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
5007
|
-
}
|
|
5008
|
-
}
|
|
5009
|
-
return out;
|
|
5010
|
-
}
|
|
5011
|
-
};
|
|
5012
|
-
var whitespaceRule = {
|
|
5013
|
-
id: "whitespace",
|
|
5014
|
-
run(state, ctx) {
|
|
5015
|
-
const out = [];
|
|
5016
|
-
for (const key of Object.keys(state.keys)) {
|
|
5017
|
-
const entry = state.keys[key];
|
|
5018
|
-
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
5019
|
-
const srcEdge = src !== src.trim();
|
|
5020
|
-
for (const locale of ctx.targetLocales) {
|
|
5021
|
-
const v = entry.values[locale]?.value;
|
|
5022
|
-
if (!v) continue;
|
|
5023
|
-
if (v !== v.trim() !== srcEdge) {
|
|
5024
|
-
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
5025
|
-
}
|
|
5026
|
-
}
|
|
5027
|
-
}
|
|
5028
|
-
return out;
|
|
5029
|
-
}
|
|
5030
|
-
};
|
|
5031
|
-
var placeholderMismatchRule = {
|
|
5032
|
-
id: "placeholder-mismatch",
|
|
5033
|
-
run(state, ctx) {
|
|
5034
|
-
const out = [];
|
|
5035
|
-
for (const key of Object.keys(state.keys)) {
|
|
5036
|
-
const entry = state.keys[key];
|
|
5037
|
-
if (entry.plural) {
|
|
5038
|
-
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
5039
|
-
if (!srcForm) continue;
|
|
5040
|
-
for (const locale of ctx.targetLocales) {
|
|
5041
|
-
const forms = entry.values[locale]?.forms;
|
|
5042
|
-
if (!forms) continue;
|
|
5043
|
-
const bad = Object.entries(forms).some(
|
|
5044
|
-
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
5045
|
-
);
|
|
5046
|
-
if (bad) {
|
|
5047
|
-
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
5048
|
-
}
|
|
5049
|
-
}
|
|
5050
|
-
continue;
|
|
5051
|
-
}
|
|
5052
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5053
|
-
if (!src) continue;
|
|
5054
|
-
for (const locale of ctx.targetLocales) {
|
|
5055
|
-
const v = entry.values[locale]?.value;
|
|
5056
|
-
if (!v) continue;
|
|
5057
|
-
if (!placeholdersMatch(src, v)) {
|
|
5058
|
-
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
5059
|
-
}
|
|
5060
|
-
}
|
|
5061
|
-
}
|
|
5062
|
-
return out;
|
|
5063
|
-
}
|
|
5064
|
-
};
|
|
5065
|
-
var icuMismatchRule = {
|
|
5066
|
-
id: "icu-mismatch",
|
|
5067
|
-
run(state, ctx) {
|
|
5068
|
-
const out = [];
|
|
5069
|
-
for (const key of Object.keys(state.keys)) {
|
|
5070
|
-
const entry = state.keys[key];
|
|
5071
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5072
|
-
if (!src) continue;
|
|
5073
|
-
const srcIcu = isIcuPluralOrSelect(src);
|
|
5074
|
-
for (const locale of ctx.targetLocales) {
|
|
5075
|
-
const v = entry.values[locale]?.value;
|
|
5076
|
-
if (!v) continue;
|
|
5077
|
-
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
5078
|
-
out.push({
|
|
5079
|
-
ruleId: "icu-mismatch",
|
|
5080
|
-
key,
|
|
5081
|
-
locale,
|
|
5082
|
-
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
5083
|
-
});
|
|
5084
|
-
}
|
|
5085
|
-
}
|
|
5086
|
-
}
|
|
5087
|
-
return out;
|
|
5088
|
-
}
|
|
5089
|
-
};
|
|
5090
|
-
var maxLengthRule = {
|
|
5091
|
-
id: "max-length",
|
|
5092
|
-
run(state, ctx) {
|
|
5093
|
-
const out = [];
|
|
5094
|
-
for (const key of Object.keys(state.keys)) {
|
|
5095
|
-
const entry = state.keys[key];
|
|
5096
|
-
const max = entry.maxLength;
|
|
5097
|
-
if (max == null) continue;
|
|
5098
|
-
for (const locale of ctx.targetLocales) {
|
|
5099
|
-
const v = entry.values[locale]?.value;
|
|
5100
|
-
if (v && v.length > max) {
|
|
5101
|
-
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
5102
|
-
}
|
|
5103
|
-
}
|
|
5104
|
-
}
|
|
5105
|
-
return out;
|
|
5106
|
-
}
|
|
5107
|
-
};
|
|
5108
|
-
var glossaryViolationRule = {
|
|
5109
|
-
id: "glossary-violation",
|
|
5110
|
-
run(state, ctx) {
|
|
5111
|
-
const out = [];
|
|
5112
|
-
for (const key of Object.keys(state.keys)) {
|
|
5113
|
-
const entry = state.keys[key];
|
|
5114
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5115
|
-
if (!src) continue;
|
|
5116
|
-
for (const locale of ctx.targetLocales) {
|
|
5117
|
-
const v = entry.values[locale]?.value;
|
|
5118
|
-
if (!v) continue;
|
|
5119
|
-
for (const hint of relevantGlossary(src, locale, ctx.glossary)) {
|
|
5120
|
-
if (hint.doNotTranslate && !v.includes(hint.term)) {
|
|
5121
|
-
out.push({ ruleId: "glossary-violation", key, locale, message: `do-not-translate term "${hint.term}" is missing or altered` });
|
|
5122
|
-
}
|
|
5123
|
-
if (hint.forced && !v.includes(hint.forced)) {
|
|
5124
|
-
out.push({ ruleId: "glossary-violation", key, locale, message: `expected glossary translation "${hint.forced}" for "${hint.term}"` });
|
|
5125
|
-
}
|
|
5126
|
-
}
|
|
5127
|
-
}
|
|
5128
|
-
}
|
|
5129
|
-
return out;
|
|
5130
|
-
}
|
|
5131
|
-
};
|
|
5132
|
-
var ALL_RULES = [
|
|
5133
|
-
emptySourceRule,
|
|
5134
|
-
emptyTranslationRule,
|
|
5135
|
-
placeholderMismatchRule,
|
|
5136
|
-
icuMismatchRule,
|
|
5137
|
-
glossaryViolationRule,
|
|
5138
|
-
maxLengthRule,
|
|
5139
|
-
identicalToSourceRule,
|
|
5140
|
-
whitespaceRule,
|
|
5141
|
-
spellingRule
|
|
5142
|
-
];
|
|
5143
|
-
|
|
5144
|
-
// src/server/lint/run.ts
|
|
5145
|
-
function resolveSeverity(id, config) {
|
|
5146
|
-
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
5147
|
-
}
|
|
5148
|
-
function sortFindings(findings) {
|
|
5149
|
-
return [...findings].sort(
|
|
5150
|
-
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
5151
|
-
);
|
|
5152
|
-
}
|
|
5153
|
-
function countSeverities(findings) {
|
|
5154
|
-
let error = 0, warn = 0;
|
|
5155
|
-
for (const f of findings) f.severity === "error" ? error++ : warn++;
|
|
5156
|
-
return { error, warn };
|
|
5157
|
-
}
|
|
5158
|
-
async function loadSpellers(locales, config, load, warn) {
|
|
5159
|
-
const map = /* @__PURE__ */ new Map();
|
|
5160
|
-
for (const locale of locales) {
|
|
5161
|
-
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
5162
|
-
const speller = await load(dictId);
|
|
5163
|
-
if (speller) map.set(locale, speller);
|
|
5164
|
-
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
5165
|
-
}
|
|
5166
|
-
return map;
|
|
5167
|
-
}
|
|
5168
|
-
async function runLint(state, options = {}) {
|
|
5169
|
-
const config = state.config.lint ?? {};
|
|
5170
|
-
const rules = options.rules ?? ALL_RULES;
|
|
5171
|
-
const warn = options.warn ?? ((m) => console.warn(m));
|
|
5172
|
-
const load = options.loadSpeller ?? defaultLoader;
|
|
5173
|
-
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
5174
|
-
const isActive = (rule) => {
|
|
5175
|
-
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
5176
|
-
return resolveSeverity(rule.id, config) !== "off";
|
|
5177
|
-
};
|
|
5178
|
-
const active = rules.filter(isActive);
|
|
5179
|
-
const spellingOn = active.some((r) => r.id === "spelling");
|
|
5180
|
-
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
5181
|
-
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
|
|
5182
|
-
const ctx = {
|
|
5183
|
-
config,
|
|
5184
|
-
sourceLocale: state.config.sourceLocale,
|
|
5185
|
-
targetLocales,
|
|
5186
|
-
glossary: state.glossary,
|
|
5187
|
-
spellers,
|
|
5188
|
-
allowWords
|
|
5189
|
-
};
|
|
5190
|
-
const ignoreRes = (config.ignore ?? []).map(globToRegExp);
|
|
5191
|
-
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
5192
|
-
const findings = [];
|
|
5193
|
-
for (const rule of active) {
|
|
5194
|
-
const severity = resolveSeverity(rule.id, config);
|
|
5195
|
-
for (const raw of rule.run(state, ctx)) {
|
|
5196
|
-
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
5197
|
-
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
5198
|
-
findings.push({ ...raw, severity });
|
|
5199
|
-
}
|
|
5200
|
-
}
|
|
5201
|
-
const sorted = sortFindings(findings);
|
|
5202
|
-
const counts = countSeverities(sorted);
|
|
5203
|
-
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
5204
|
-
}
|
|
5205
|
-
|
|
5206
|
-
// src/server/lint/outputs.ts
|
|
5207
|
-
init_adapters();
|
|
5208
|
-
import { readFileSync as readFileSync10, existsSync as existsSync8 } from "fs";
|
|
5209
|
-
import { resolve as resolve8 } from "path";
|
|
5210
|
-
function checkOutputs(state, root) {
|
|
5211
|
-
const out = [];
|
|
5212
|
-
for (const output of state.config.outputs) {
|
|
5213
|
-
const result = getAdapter(output.adapter).export(state, output);
|
|
5214
|
-
for (const file of result.files) {
|
|
5215
|
-
const abs = resolve8(root, file.path);
|
|
5216
|
-
const current = existsSync8(abs) ? readFileSync10(abs, "utf8") : null;
|
|
5217
|
-
if (current === null) {
|
|
5218
|
-
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
5219
|
-
} else if (current !== file.contents) {
|
|
5220
|
-
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
5221
|
-
}
|
|
5222
|
-
}
|
|
5223
|
-
}
|
|
5224
|
-
return out;
|
|
5225
|
-
}
|
|
5443
|
+
init_run2();
|
|
5444
|
+
init_outputs();
|
|
5445
|
+
import { resolve as resolve11, dirname as dirname5 } from "path";
|
|
5446
|
+
import { readFileSync as readFileSync15, existsSync as existsSync12 } from "fs";
|
|
5226
5447
|
|
|
5227
5448
|
// src/server/lint/locate.ts
|
|
5228
5449
|
function locate(rawText, key) {
|
|
@@ -5365,7 +5586,7 @@ function watchTargetFor(statePath) {
|
|
|
5365
5586
|
return detectFormat(statePath) === "split" ? { path: splitDirFor(statePath), recursive: true } : { path: statePath, recursive: false };
|
|
5366
5587
|
}
|
|
5367
5588
|
async function runExport(args) {
|
|
5368
|
-
const root =
|
|
5589
|
+
const root = dirname5(resolve11(args.statePath));
|
|
5369
5590
|
const runOnce = () => {
|
|
5370
5591
|
const state = loadState(args.statePath);
|
|
5371
5592
|
const result = exportToDisk(state, root, args.adapter ? { adapter: args.adapter } : void 0);
|
|
@@ -5376,8 +5597,9 @@ async function runExport(args) {
|
|
|
5376
5597
|
return result;
|
|
5377
5598
|
};
|
|
5378
5599
|
if (!args.watch) {
|
|
5379
|
-
const { written, skipped } = runOnce();
|
|
5380
|
-
|
|
5600
|
+
const { written, skipped, deleted } = runOnce();
|
|
5601
|
+
const extras = [skipped ? `${skipped} unchanged` : "", deleted ? `${deleted} stale removed` : ""].filter(Boolean);
|
|
5602
|
+
console.log(`Exported ${written} file(s)${extras.length ? ` (${extras.join(", ")})` : ""}.`);
|
|
5381
5603
|
return;
|
|
5382
5604
|
}
|
|
5383
5605
|
const { watch } = await import("fs");
|
|
@@ -5389,8 +5611,10 @@ async function runExport(args) {
|
|
|
5389
5611
|
clearTimeout(timer);
|
|
5390
5612
|
timer = setTimeout(() => {
|
|
5391
5613
|
try {
|
|
5392
|
-
const { written } = runOnce();
|
|
5393
|
-
if (written)
|
|
5614
|
+
const { written, deleted } = runOnce();
|
|
5615
|
+
if (written || deleted) {
|
|
5616
|
+
console.log(`Re-exported ${written} file(s)${deleted ? ` (${deleted} stale removed)` : ""}.`);
|
|
5617
|
+
}
|
|
5394
5618
|
} catch (e) {
|
|
5395
5619
|
console.error(e.message);
|
|
5396
5620
|
}
|
|
@@ -5401,7 +5625,7 @@ async function runExport(args) {
|
|
|
5401
5625
|
}
|
|
5402
5626
|
async function runTranslate(args) {
|
|
5403
5627
|
const state = loadState(args.statePath);
|
|
5404
|
-
const projectRoot =
|
|
5628
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5405
5629
|
const reqs = selectRequests(state, {
|
|
5406
5630
|
// Default to translating only empty values; --all forces a full re-translate
|
|
5407
5631
|
// (overwriting existing translations). --only missing stays as a no-op alias.
|
|
@@ -5471,7 +5695,7 @@ function printReport(report, format, rawText) {
|
|
|
5471
5695
|
}
|
|
5472
5696
|
async function runLintCmd(args) {
|
|
5473
5697
|
const state = loadState(args.statePath);
|
|
5474
|
-
const rawText =
|
|
5698
|
+
const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5475
5699
|
const report = await runLint(state, { locales: args.locales, ruleIds: args.ruleIds });
|
|
5476
5700
|
printReport(report, args.format, rawText);
|
|
5477
5701
|
const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
|
|
@@ -5491,8 +5715,8 @@ async function runCheck(args) {
|
|
|
5491
5715
|
process.exitCode = 1;
|
|
5492
5716
|
return;
|
|
5493
5717
|
}
|
|
5494
|
-
const rawText =
|
|
5495
|
-
const root =
|
|
5718
|
+
const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5719
|
+
const root = dirname5(resolve11(args.statePath));
|
|
5496
5720
|
const lint = await runLint(state, {});
|
|
5497
5721
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
5498
5722
|
const counts = countSeverities(findings);
|
|
@@ -5501,10 +5725,10 @@ async function runCheck(args) {
|
|
|
5501
5725
|
if (!report.ok) process.exitCode = 1;
|
|
5502
5726
|
}
|
|
5503
5727
|
async function runImportCmd(args) {
|
|
5504
|
-
const { runImport: runImport2 } = await Promise.resolve().then(() => (
|
|
5505
|
-
const projectRoot = args.importSource ? resolve11(args.importSource) :
|
|
5728
|
+
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
5729
|
+
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
|
|
5506
5730
|
const out = resolve11(projectRoot, "glotfile.json");
|
|
5507
|
-
if (
|
|
5731
|
+
if (existsSync12(out) && !args.importForce) {
|
|
5508
5732
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
5509
5733
|
process.exitCode = 1;
|
|
5510
5734
|
return;
|
|
@@ -5529,7 +5753,7 @@ async function runImportCmd(args) {
|
|
|
5529
5753
|
}
|
|
5530
5754
|
async function runBuildContext(args) {
|
|
5531
5755
|
const state = loadState(args.statePath);
|
|
5532
|
-
const projectRoot =
|
|
5756
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5533
5757
|
const cache2 = loadUsageCache(projectRoot);
|
|
5534
5758
|
if (!cache2) {
|
|
5535
5759
|
console.error("No usage index found. Run 'glotfile scan' first.");
|
|
@@ -5600,7 +5824,7 @@ async function runBuildContext(args) {
|
|
|
5600
5824
|
}
|
|
5601
5825
|
async function runScanCmd(args) {
|
|
5602
5826
|
const state = loadState(args.statePath);
|
|
5603
|
-
const projectRoot =
|
|
5827
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5604
5828
|
const existing = loadUsageCache(projectRoot);
|
|
5605
5829
|
const result = runScan(projectRoot, state.config.scan ?? {}, existing);
|
|
5606
5830
|
const fileCount2 = Object.keys(result.files).length;
|
|
@@ -5619,7 +5843,7 @@ async function runPrune(args) {
|
|
|
5619
5843
|
for (const k of findEmptySourceKeys(state)) toRemove.add(k);
|
|
5620
5844
|
}
|
|
5621
5845
|
if (args.unused) {
|
|
5622
|
-
const projectRoot =
|
|
5846
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5623
5847
|
const cache2 = runScan(projectRoot, state.config.scan ?? {}, loadUsageCache(projectRoot));
|
|
5624
5848
|
const used = new Set(computeUsedKeys(state, cache2));
|
|
5625
5849
|
for (const k of Object.keys(state.keys)) {
|