glotfile 0.4.2 → 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/dist/server/cli.js +641 -418
- package/dist/server/server.js +585 -83
- package/dist/ui/assets/index-CqpESIEu.css +1 -0
- package/dist/ui/assets/{index-CJ_nmOjf.js → index-VdTDY_C8.js} +47 -17
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-De8z0F8Y.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
|
|
|
@@ -2232,13 +2414,13 @@ var init_ai = __esm({
|
|
|
2232
2414
|
});
|
|
2233
2415
|
|
|
2234
2416
|
// src/server/glotfile-dir.ts
|
|
2235
|
-
import { existsSync as
|
|
2417
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2236
2418
|
import { resolve as resolve2 } from "path";
|
|
2237
2419
|
function ensureGlotfileDir(projectRoot) {
|
|
2238
2420
|
const dir = resolve2(projectRoot, ".glotfile");
|
|
2239
2421
|
mkdirSync3(dir, { recursive: true });
|
|
2240
2422
|
const ignore = resolve2(dir, ".gitignore");
|
|
2241
|
-
if (!
|
|
2423
|
+
if (!existsSync4(ignore)) {
|
|
2242
2424
|
try {
|
|
2243
2425
|
writeFileSync2(ignore, "*\n");
|
|
2244
2426
|
} catch {
|
|
@@ -2349,7 +2531,7 @@ var init_glob = __esm({
|
|
|
2349
2531
|
});
|
|
2350
2532
|
|
|
2351
2533
|
// src/server/ai/run.ts
|
|
2352
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
2534
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
2353
2535
|
import { resolve as resolve4, extname } from "path";
|
|
2354
2536
|
function selectRequests(state, opts) {
|
|
2355
2537
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
@@ -2432,7 +2614,7 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
2432
2614
|
if (!mediaType) continue;
|
|
2433
2615
|
if (!cache2.has(screenshot)) {
|
|
2434
2616
|
const abs = resolve4(projectRoot, screenshot);
|
|
2435
|
-
if (!
|
|
2617
|
+
if (!existsSync5(abs)) {
|
|
2436
2618
|
cache2.set(screenshot, null);
|
|
2437
2619
|
} else {
|
|
2438
2620
|
const buf = readFileSync5(abs);
|
|
@@ -2530,7 +2712,7 @@ var init_run = __esm({
|
|
|
2530
2712
|
});
|
|
2531
2713
|
|
|
2532
2714
|
// src/server/log.ts
|
|
2533
|
-
import { appendFileSync, readFileSync as readFileSync6, existsSync as
|
|
2715
|
+
import { appendFileSync, readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
2534
2716
|
import { resolve as resolve5 } from "path";
|
|
2535
2717
|
function logPath(projectRoot) {
|
|
2536
2718
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -2541,7 +2723,7 @@ function appendLog(projectRoot, entry) {
|
|
|
2541
2723
|
}
|
|
2542
2724
|
function readLog(projectRoot, limit = 100) {
|
|
2543
2725
|
const path = logPath(projectRoot);
|
|
2544
|
-
if (!
|
|
2726
|
+
if (!existsSync6(path)) return [];
|
|
2545
2727
|
const lines = readFileSync6(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
2546
2728
|
const entries = lines.map((l) => JSON.parse(l));
|
|
2547
2729
|
return entries.reverse().slice(0, limit);
|
|
@@ -2554,11 +2736,11 @@ var init_log = __esm({
|
|
|
2554
2736
|
});
|
|
2555
2737
|
|
|
2556
2738
|
// src/server/scan.ts
|
|
2557
|
-
import { existsSync as
|
|
2739
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
2558
2740
|
import { resolve as resolve6 } from "path";
|
|
2559
2741
|
function loadUsageCache(projectRoot) {
|
|
2560
2742
|
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
2561
|
-
if (!
|
|
2743
|
+
if (!existsSync7(path)) return null;
|
|
2562
2744
|
try {
|
|
2563
2745
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2564
2746
|
} catch {
|
|
@@ -2574,8 +2756,10 @@ function findMissing(state) {
|
|
|
2574
2756
|
const targets = state.config.locales.filter((l) => l !== state.config.sourceLocale).sort();
|
|
2575
2757
|
const out = [];
|
|
2576
2758
|
for (const key of Object.keys(state.keys).sort()) {
|
|
2759
|
+
const entry = state.keys[key];
|
|
2760
|
+
if (entry.skipTranslate) continue;
|
|
2577
2761
|
for (const locale of targets) {
|
|
2578
|
-
const v =
|
|
2762
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
|
|
2579
2763
|
if (!v) out.push({ key, locale });
|
|
2580
2764
|
}
|
|
2581
2765
|
}
|
|
@@ -2601,7 +2785,7 @@ var init_scan = __esm({
|
|
|
2601
2785
|
});
|
|
2602
2786
|
|
|
2603
2787
|
// src/server/scanner.ts
|
|
2604
|
-
import { readdirSync as
|
|
2788
|
+
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync8 } from "fs";
|
|
2605
2789
|
import { join as join3, extname as extname2, relative } from "path";
|
|
2606
2790
|
function scannerForExt(ext) {
|
|
2607
2791
|
return EXT_SCANNER[ext] ?? null;
|
|
@@ -2722,7 +2906,7 @@ function isIncluded(relPath, includePatterns) {
|
|
|
2722
2906
|
function* walkFiles(dir, root, exclude) {
|
|
2723
2907
|
let entries;
|
|
2724
2908
|
try {
|
|
2725
|
-
entries =
|
|
2909
|
+
entries = readdirSync3(dir);
|
|
2726
2910
|
} catch {
|
|
2727
2911
|
return;
|
|
2728
2912
|
}
|
|
@@ -2732,7 +2916,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
2732
2916
|
const rel = relative(root, abs);
|
|
2733
2917
|
let st;
|
|
2734
2918
|
try {
|
|
2735
|
-
st =
|
|
2919
|
+
st = statSync2(abs);
|
|
2736
2920
|
} catch {
|
|
2737
2921
|
continue;
|
|
2738
2922
|
}
|
|
@@ -2761,7 +2945,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
2761
2945
|
const abs = join3(projectRoot, relPath);
|
|
2762
2946
|
let st;
|
|
2763
2947
|
try {
|
|
2764
|
-
st =
|
|
2948
|
+
st = statSync2(abs);
|
|
2765
2949
|
} catch {
|
|
2766
2950
|
continue;
|
|
2767
2951
|
}
|
|
@@ -2879,7 +3063,7 @@ var init_scanner = __esm({
|
|
|
2879
3063
|
});
|
|
2880
3064
|
|
|
2881
3065
|
// src/server/ai/context.ts
|
|
2882
|
-
import { existsSync as
|
|
3066
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
2883
3067
|
import { resolve as resolve7 } from "path";
|
|
2884
3068
|
function globToRegExp2(glob) {
|
|
2885
3069
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -2894,7 +3078,7 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
2894
3078
|
for (const ref of selected) {
|
|
2895
3079
|
const absPath = resolve7(projectRoot, ref.file);
|
|
2896
3080
|
if (!fileCache.has(ref.file)) {
|
|
2897
|
-
if (!
|
|
3081
|
+
if (!existsSync8(absPath)) continue;
|
|
2898
3082
|
const content = readFileSync9(absPath, "utf8");
|
|
2899
3083
|
fileCache.set(ref.file, content.split("\n"));
|
|
2900
3084
|
}
|
|
@@ -3039,22 +3223,364 @@ var init_context = __esm({
|
|
|
3039
3223
|
}
|
|
3040
3224
|
});
|
|
3041
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
|
+
|
|
3042
3568
|
// src/server/import/detect.ts
|
|
3043
|
-
import { existsSync as
|
|
3569
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
3044
3570
|
import { join as join4 } from "path";
|
|
3045
3571
|
function safeIsDir(p) {
|
|
3046
3572
|
try {
|
|
3047
|
-
return
|
|
3573
|
+
return statSync3(p).isDirectory();
|
|
3048
3574
|
} catch {
|
|
3049
3575
|
return false;
|
|
3050
3576
|
}
|
|
3051
3577
|
}
|
|
3052
3578
|
function listDirs(dir) {
|
|
3053
|
-
return
|
|
3579
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join4(dir, e)));
|
|
3054
3580
|
}
|
|
3055
3581
|
function fileCount(dir) {
|
|
3056
3582
|
try {
|
|
3057
|
-
return
|
|
3583
|
+
return readdirSync4(dir).length;
|
|
3058
3584
|
} catch {
|
|
3059
3585
|
return 0;
|
|
3060
3586
|
}
|
|
@@ -3071,15 +3597,16 @@ function detectLaravel(root) {
|
|
|
3071
3597
|
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
3072
3598
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
3073
3599
|
}
|
|
3074
|
-
function detectVue(root) {
|
|
3600
|
+
function detectVue(root, forced = false) {
|
|
3075
3601
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
3076
3602
|
const localeRoot = join4(root, rel);
|
|
3077
3603
|
if (!safeIsDir(localeRoot)) continue;
|
|
3078
|
-
const locales =
|
|
3079
|
-
|
|
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) {
|
|
3080
3607
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
3081
3608
|
try {
|
|
3082
|
-
return
|
|
3609
|
+
return statSync3(join4(localeRoot, `${loc}.json`)).size;
|
|
3083
3610
|
} catch {
|
|
3084
3611
|
return 0;
|
|
3085
3612
|
}
|
|
@@ -3093,7 +3620,7 @@ function detectArb(root) {
|
|
|
3093
3620
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
3094
3621
|
const localeRoot = join4(root, rel);
|
|
3095
3622
|
if (!safeIsDir(localeRoot)) continue;
|
|
3096
|
-
const locales =
|
|
3623
|
+
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
3097
3624
|
if (locales.length >= 1) {
|
|
3098
3625
|
return { format: "flutter-arb", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
3099
3626
|
}
|
|
@@ -3101,7 +3628,7 @@ function detectArb(root) {
|
|
|
3101
3628
|
return null;
|
|
3102
3629
|
}
|
|
3103
3630
|
function detect(root, formatOverride) {
|
|
3104
|
-
if (!
|
|
3631
|
+
if (!existsSync10(root)) return null;
|
|
3105
3632
|
if (formatOverride) {
|
|
3106
3633
|
const fn = BY_FORMAT[formatOverride];
|
|
3107
3634
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -3122,7 +3649,7 @@ var init_detect = __esm({
|
|
|
3122
3649
|
DETECTORS = [detectLaravel, detectVue, detectArb];
|
|
3123
3650
|
BY_FORMAT = {
|
|
3124
3651
|
"laravel-php": detectLaravel,
|
|
3125
|
-
"vue-i18n-json": detectVue,
|
|
3652
|
+
"vue-i18n-json": (root) => detectVue(root, true),
|
|
3126
3653
|
"flutter-arb": detectArb
|
|
3127
3654
|
};
|
|
3128
3655
|
}
|
|
@@ -3156,7 +3683,7 @@ var init_flatten = __esm({
|
|
|
3156
3683
|
});
|
|
3157
3684
|
|
|
3158
3685
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
3159
|
-
import { readdirSync as
|
|
3686
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
|
|
3160
3687
|
import { join as join5 } from "path";
|
|
3161
3688
|
var LOCALE_RE2, vueI18nJson2;
|
|
3162
3689
|
var init_vue_i18n_json2 = __esm({
|
|
@@ -3170,7 +3697,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
3170
3697
|
const warnings = [];
|
|
3171
3698
|
const keys = {};
|
|
3172
3699
|
const locales = [];
|
|
3173
|
-
for (const file of
|
|
3700
|
+
for (const file of readdirSync5(localeRoot).sort()) {
|
|
3174
3701
|
if (!file.endsWith(".json")) continue;
|
|
3175
3702
|
const locale = file.slice(0, -".json".length);
|
|
3176
3703
|
if (!LOCALE_RE2.test(locale)) continue;
|
|
@@ -3204,18 +3731,18 @@ var init_placeholders2 = __esm({
|
|
|
3204
3731
|
});
|
|
3205
3732
|
|
|
3206
3733
|
// src/server/import/parsers/laravel-php.ts
|
|
3207
|
-
import { readdirSync as
|
|
3734
|
+
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
3208
3735
|
import { join as join6, relative as relative2 } from "path";
|
|
3209
3736
|
import { execFileSync } from "child_process";
|
|
3210
3737
|
function listDirs2(dir) {
|
|
3211
|
-
return
|
|
3738
|
+
return readdirSync6(dir).filter((e) => statSync4(join6(dir, e)).isDirectory());
|
|
3212
3739
|
}
|
|
3213
3740
|
function listPhpFiles(dir) {
|
|
3214
3741
|
const out = [];
|
|
3215
3742
|
const walk = (d) => {
|
|
3216
|
-
for (const e of
|
|
3743
|
+
for (const e of readdirSync6(d)) {
|
|
3217
3744
|
const full = join6(d, e);
|
|
3218
|
-
if (
|
|
3745
|
+
if (statSync4(full).isDirectory()) walk(full);
|
|
3219
3746
|
else if (e.endsWith(".php")) out.push(full);
|
|
3220
3747
|
}
|
|
3221
3748
|
};
|
|
@@ -3282,7 +3809,7 @@ var init_laravel_php2 = __esm({
|
|
|
3282
3809
|
});
|
|
3283
3810
|
|
|
3284
3811
|
// src/server/import/parsers/flutter-arb.ts
|
|
3285
|
-
import { readdirSync as
|
|
3812
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync12 } from "fs";
|
|
3286
3813
|
import { join as join7 } from "path";
|
|
3287
3814
|
function localeFromArbName(file) {
|
|
3288
3815
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -3316,7 +3843,7 @@ var init_flutter_arb2 = __esm({
|
|
|
3316
3843
|
const warnings = [];
|
|
3317
3844
|
const keys = {};
|
|
3318
3845
|
const locales = [];
|
|
3319
|
-
for (const file of
|
|
3846
|
+
for (const file of readdirSync7(localeRoot).sort()) {
|
|
3320
3847
|
if (!file.endsWith(".arb")) continue;
|
|
3321
3848
|
const locale = localeFromArbName(file);
|
|
3322
3849
|
if (!locale) continue;
|
|
@@ -3502,7 +4029,7 @@ function runImport(opts) {
|
|
|
3502
4029
|
localeCount: state.config.locales.length
|
|
3503
4030
|
};
|
|
3504
4031
|
}
|
|
3505
|
-
var
|
|
4032
|
+
var init_run3 = __esm({
|
|
3506
4033
|
"src/server/import/run.ts"() {
|
|
3507
4034
|
"use strict";
|
|
3508
4035
|
init_detect();
|
|
@@ -3888,11 +4415,11 @@ var init_ui_prefs = __esm({
|
|
|
3888
4415
|
// src/server/api.ts
|
|
3889
4416
|
import { Hono } from "hono";
|
|
3890
4417
|
import { streamSSE } from "hono/streaming";
|
|
3891
|
-
import { readFileSync as readFileSync14, existsSync as
|
|
3892
|
-
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";
|
|
3893
4420
|
function projectName(root) {
|
|
3894
4421
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
3895
|
-
if (
|
|
4422
|
+
if (existsSync11(nameFile)) {
|
|
3896
4423
|
try {
|
|
3897
4424
|
const name = readFileSync14(nameFile, "utf8").trim();
|
|
3898
4425
|
if (name) return name;
|
|
@@ -3904,7 +4431,7 @@ function projectName(root) {
|
|
|
3904
4431
|
function createApi(deps) {
|
|
3905
4432
|
const app = new Hono();
|
|
3906
4433
|
const load = () => loadState(deps.statePath);
|
|
3907
|
-
const projectRoot =
|
|
4434
|
+
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
3908
4435
|
let translateQueue = Promise.resolve();
|
|
3909
4436
|
const withTranslateLock = (fn) => {
|
|
3910
4437
|
const next = translateQueue.then(fn, fn);
|
|
@@ -4002,13 +4529,13 @@ function createApi(deps) {
|
|
|
4002
4529
|
found.set(deps.statePath, {
|
|
4003
4530
|
name: basename(deps.statePath),
|
|
4004
4531
|
path: deps.statePath,
|
|
4005
|
-
relDir: activeRel !== basename(activeRel) ?
|
|
4532
|
+
relDir: activeRel !== basename(activeRel) ? dirname3(activeRel) : void 0
|
|
4006
4533
|
});
|
|
4007
4534
|
function walk(dir, depth) {
|
|
4008
4535
|
if (depth > 4) return;
|
|
4009
4536
|
let entries = [];
|
|
4010
4537
|
try {
|
|
4011
|
-
entries =
|
|
4538
|
+
entries = readdirSync8(dir);
|
|
4012
4539
|
} catch {
|
|
4013
4540
|
return;
|
|
4014
4541
|
}
|
|
@@ -4016,13 +4543,13 @@ function createApi(deps) {
|
|
|
4016
4543
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4017
4544
|
const abs = resolve9(dir, name);
|
|
4018
4545
|
let filePath = null;
|
|
4019
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
4546
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync11(resolve9(abs, "config.json"))) {
|
|
4020
4547
|
filePath = resolve9(dir, `${name}.json`);
|
|
4021
4548
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
4022
4549
|
filePath = abs;
|
|
4023
4550
|
} else {
|
|
4024
4551
|
try {
|
|
4025
|
-
if (
|
|
4552
|
+
if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
|
|
4026
4553
|
} catch {
|
|
4027
4554
|
}
|
|
4028
4555
|
continue;
|
|
@@ -4031,7 +4558,7 @@ function createApi(deps) {
|
|
|
4031
4558
|
try {
|
|
4032
4559
|
loadState(filePath);
|
|
4033
4560
|
const rel = relative3(projectRoot, filePath);
|
|
4034
|
-
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 });
|
|
4035
4562
|
} catch {
|
|
4036
4563
|
}
|
|
4037
4564
|
}
|
|
@@ -4048,9 +4575,9 @@ function createApi(deps) {
|
|
|
4048
4575
|
const { path } = await c.req.json();
|
|
4049
4576
|
if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
|
|
4050
4577
|
const resolved = resolve9(projectRoot, path);
|
|
4051
|
-
const inside = resolved === projectRoot || resolved.startsWith(projectRoot +
|
|
4578
|
+
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
4052
4579
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
4053
|
-
if (!
|
|
4580
|
+
if (!existsSync11(resolved)) return c.json({ error: "file not found" }, 400);
|
|
4054
4581
|
loadState(resolved);
|
|
4055
4582
|
deps.statePath = resolved;
|
|
4056
4583
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -4107,11 +4634,11 @@ function createApi(deps) {
|
|
|
4107
4634
|
function removeOrphanScreenshot(s, screenshot) {
|
|
4108
4635
|
if (!screenshot) return;
|
|
4109
4636
|
for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
|
|
4110
|
-
const root =
|
|
4637
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4111
4638
|
const abs = resolve9(root, screenshot);
|
|
4112
4639
|
const rel = relative3(root, abs);
|
|
4113
|
-
const seg0 = rel.split(
|
|
4114
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
4640
|
+
const seg0 = rel.split(sep2)[0] ?? "";
|
|
4641
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
|
|
4115
4642
|
try {
|
|
4116
4643
|
rmSync4(abs);
|
|
4117
4644
|
} catch {
|
|
@@ -4363,7 +4890,7 @@ function createApi(deps) {
|
|
|
4363
4890
|
const body = await c.req.parseBody();
|
|
4364
4891
|
const file = body["file"];
|
|
4365
4892
|
if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
|
|
4366
|
-
const root =
|
|
4893
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4367
4894
|
const dirName = screenshotDirName(deps.statePath);
|
|
4368
4895
|
const dir = resolve9(root, dirName);
|
|
4369
4896
|
const filename = `${sanitize(key)}__${sanitize(file.name)}`;
|
|
@@ -4399,6 +4926,23 @@ function createApi(deps) {
|
|
|
4399
4926
|
return c.json({ files, warnings });
|
|
4400
4927
|
});
|
|
4401
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
|
+
});
|
|
4402
4946
|
app.get("/checks", (c) => {
|
|
4403
4947
|
const param = c.req.query("checks");
|
|
4404
4948
|
const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
|
|
@@ -4434,22 +4978,12 @@ function createApi(deps) {
|
|
|
4434
4978
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
4435
4979
|
});
|
|
4436
4980
|
app.post("/export", (c) => {
|
|
4437
|
-
const
|
|
4438
|
-
const
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
const result = adapter.export(s, output);
|
|
4444
|
-
warnings.push(...result.warnings);
|
|
4445
|
-
for (const f of result.files) {
|
|
4446
|
-
const abs = resolve9(root, f.path);
|
|
4447
|
-
writeFileAtomic(abs, f.contents);
|
|
4448
|
-
count++;
|
|
4449
|
-
}
|
|
4450
|
-
}
|
|
4451
|
-
console.log(`[export] ${count} file(s)${warnings.length ? `, ${warnings.length} warning(s)` : ""}`);
|
|
4452
|
-
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 });
|
|
4453
4987
|
});
|
|
4454
4988
|
app.post("/translate/stream", async (c) => {
|
|
4455
4989
|
const signal = c.req.raw.signal;
|
|
@@ -4471,7 +5005,7 @@ function createApi(deps) {
|
|
|
4471
5005
|
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4472
5006
|
return;
|
|
4473
5007
|
}
|
|
4474
|
-
const { skipped } = attachScreenshotsForProvider(reqs, s,
|
|
5008
|
+
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4475
5009
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4476
5010
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4477
5011
|
let totalWritten = 0;
|
|
@@ -4549,7 +5083,7 @@ function createApi(deps) {
|
|
|
4549
5083
|
} catch (e) {
|
|
4550
5084
|
return c.json({ error: e.message }, 400);
|
|
4551
5085
|
}
|
|
4552
|
-
const { skipped } = attachScreenshotsForProvider(toTranslate, s,
|
|
5086
|
+
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4553
5087
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4554
5088
|
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4555
5089
|
const latest = load();
|
|
@@ -4736,13 +5270,16 @@ var init_api = __esm({
|
|
|
4736
5270
|
init_context();
|
|
4737
5271
|
init_stats();
|
|
4738
5272
|
init_checks();
|
|
5273
|
+
init_run2();
|
|
5274
|
+
init_outputs();
|
|
5275
|
+
init_spelling();
|
|
4739
5276
|
init_adapters();
|
|
4740
5277
|
init_ai();
|
|
4741
5278
|
init_run();
|
|
4742
5279
|
init_provider();
|
|
4743
5280
|
init_log();
|
|
4744
5281
|
init_schema();
|
|
4745
|
-
|
|
5282
|
+
init_run3();
|
|
4746
5283
|
init_export_run();
|
|
4747
5284
|
init_ui_prefs();
|
|
4748
5285
|
init_local_settings();
|
|
@@ -4761,7 +5298,7 @@ __export(server_exports, {
|
|
|
4761
5298
|
import { Hono as Hono2 } from "hono";
|
|
4762
5299
|
import { serve } from "@hono/node-server";
|
|
4763
5300
|
import { fileURLToPath } from "url";
|
|
4764
|
-
import { dirname as
|
|
5301
|
+
import { dirname as dirname4, join as join9, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
4765
5302
|
import { readFile, stat } from "fs/promises";
|
|
4766
5303
|
import { createServer } from "net";
|
|
4767
5304
|
import open from "open";
|
|
@@ -4783,11 +5320,11 @@ function buildApp(opts) {
|
|
|
4783
5320
|
app.get("/:dir/*", async (c, next) => {
|
|
4784
5321
|
const dirSeg = c.req.param("dir");
|
|
4785
5322
|
if (!dirSeg.endsWith("-screenshots")) return next();
|
|
4786
|
-
const shotsRoot = resolve10(
|
|
5323
|
+
const shotsRoot = resolve10(dirname4(resolve10(apiDeps.statePath)), dirSeg);
|
|
4787
5324
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4788
5325
|
const rest = pathname.slice(`/${dirSeg}`.length);
|
|
4789
5326
|
const target = resolve10(shotsRoot, "." + rest);
|
|
4790
|
-
const inside = target === shotsRoot || target.startsWith(shotsRoot +
|
|
5327
|
+
const inside = target === shotsRoot || target.startsWith(shotsRoot + sep3);
|
|
4791
5328
|
if (inside) {
|
|
4792
5329
|
const file = await readFileResponse(target);
|
|
4793
5330
|
if (file) return file;
|
|
@@ -4799,7 +5336,7 @@ function buildApp(opts) {
|
|
|
4799
5336
|
app.get("/*", async (c) => {
|
|
4800
5337
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4801
5338
|
const target = resolve10(root, "." + pathname);
|
|
4802
|
-
const inside = target === root || target.startsWith(root +
|
|
5339
|
+
const inside = target === root || target.startsWith(root + sep3);
|
|
4803
5340
|
if (inside && pathname !== "/") {
|
|
4804
5341
|
const file = await readFileResponse(target);
|
|
4805
5342
|
if (file) return file;
|
|
@@ -4841,7 +5378,7 @@ async function startServer(opts) {
|
|
|
4841
5378
|
});
|
|
4842
5379
|
}
|
|
4843
5380
|
function backgroundScan(statePath) {
|
|
4844
|
-
const projectRoot =
|
|
5381
|
+
const projectRoot = dirname4(resolve10(statePath));
|
|
4845
5382
|
Promise.resolve().then(() => {
|
|
4846
5383
|
const state = loadState(statePath);
|
|
4847
5384
|
const existing = loadUsageCache(projectRoot);
|
|
@@ -4861,7 +5398,7 @@ var init_server = __esm({
|
|
|
4861
5398
|
init_state();
|
|
4862
5399
|
init_scan();
|
|
4863
5400
|
init_scanner();
|
|
4864
|
-
here =
|
|
5401
|
+
here = dirname4(fileURLToPath(import.meta.url));
|
|
4865
5402
|
DEFAULT_UI_DIR = join9(here, "..", "ui");
|
|
4866
5403
|
MIME = {
|
|
4867
5404
|
".html": "text/html; charset=utf-8",
|
|
@@ -4903,327 +5440,10 @@ init_log();
|
|
|
4903
5440
|
init_scan();
|
|
4904
5441
|
init_scanner();
|
|
4905
5442
|
init_context();
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
init_glob();
|
|
4911
|
-
init_registry();
|
|
4912
|
-
|
|
4913
|
-
// src/server/lint/rules.ts
|
|
4914
|
-
init_scan();
|
|
4915
|
-
init_placeholders();
|
|
4916
|
-
init_run();
|
|
4917
|
-
|
|
4918
|
-
// src/server/lint/spelling.ts
|
|
4919
|
-
init_placeholders();
|
|
4920
|
-
function tokenize(text) {
|
|
4921
|
-
return text.match(new RegExp("\\p{L}[\\p{L}'']*", "gu")) ?? [];
|
|
4922
|
-
}
|
|
4923
|
-
function buildAllowWords(glossary, dictionary2 = []) {
|
|
4924
|
-
const set = /* @__PURE__ */ new Set();
|
|
4925
|
-
const add = (s) => {
|
|
4926
|
-
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
4927
|
-
};
|
|
4928
|
-
for (const g of glossary) add(g.term);
|
|
4929
|
-
for (const w of dictionary2) add(w);
|
|
4930
|
-
return set;
|
|
4931
|
-
}
|
|
4932
|
-
var spellingRule = {
|
|
4933
|
-
id: "spelling",
|
|
4934
|
-
run(state, ctx) {
|
|
4935
|
-
const out = [];
|
|
4936
|
-
for (const key of Object.keys(state.keys)) {
|
|
4937
|
-
const entry = state.keys[key];
|
|
4938
|
-
for (const locale of ctx.targetLocales) {
|
|
4939
|
-
const speller = ctx.spellers.get(locale);
|
|
4940
|
-
if (!speller) continue;
|
|
4941
|
-
const value = entry.values[locale]?.value;
|
|
4942
|
-
if (!value) continue;
|
|
4943
|
-
const placeholders = new Set(extractPlaceholders(value).map((p) => p.toLowerCase()));
|
|
4944
|
-
for (const word of tokenize(value)) {
|
|
4945
|
-
const lower = word.toLowerCase();
|
|
4946
|
-
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
4947
|
-
if (!speller.correct(word)) {
|
|
4948
|
-
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
4949
|
-
}
|
|
4950
|
-
}
|
|
4951
|
-
}
|
|
4952
|
-
}
|
|
4953
|
-
return out;
|
|
4954
|
-
}
|
|
4955
|
-
};
|
|
4956
|
-
var defaultLoader = async (dictId) => {
|
|
4957
|
-
try {
|
|
4958
|
-
const nspellMod = await import("nspell");
|
|
4959
|
-
const nspell2 = nspellMod.default ?? nspellMod;
|
|
4960
|
-
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
4961
|
-
const dictExport = dictMod.default ?? dictMod;
|
|
4962
|
-
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
4963
|
-
return nspell2(dict);
|
|
4964
|
-
} catch {
|
|
4965
|
-
return null;
|
|
4966
|
-
}
|
|
4967
|
-
};
|
|
4968
|
-
|
|
4969
|
-
// src/server/lint/rules.ts
|
|
4970
|
-
var emptySourceRule = {
|
|
4971
|
-
id: "empty-source",
|
|
4972
|
-
run(state, ctx) {
|
|
4973
|
-
const out = [];
|
|
4974
|
-
for (const key of Object.keys(state.keys)) {
|
|
4975
|
-
const v = state.keys[key].values[ctx.sourceLocale]?.value;
|
|
4976
|
-
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
4977
|
-
}
|
|
4978
|
-
return out;
|
|
4979
|
-
}
|
|
4980
|
-
};
|
|
4981
|
-
var emptyTranslationRule = {
|
|
4982
|
-
id: "empty-translation",
|
|
4983
|
-
run(state, ctx) {
|
|
4984
|
-
const out = [];
|
|
4985
|
-
for (const m of findMissing(state)) {
|
|
4986
|
-
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
4987
|
-
}
|
|
4988
|
-
for (const key of Object.keys(state.keys)) {
|
|
4989
|
-
for (const locale of ctx.targetLocales) {
|
|
4990
|
-
const v = state.keys[key].values[locale]?.value;
|
|
4991
|
-
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
4992
|
-
}
|
|
4993
|
-
}
|
|
4994
|
-
return out;
|
|
4995
|
-
}
|
|
4996
|
-
};
|
|
4997
|
-
var identicalToSourceRule = {
|
|
4998
|
-
id: "identical-to-source",
|
|
4999
|
-
run(state, ctx) {
|
|
5000
|
-
const out = [];
|
|
5001
|
-
for (const key of Object.keys(state.keys)) {
|
|
5002
|
-
const entry = state.keys[key];
|
|
5003
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5004
|
-
if (!src) continue;
|
|
5005
|
-
for (const locale of ctx.targetLocales) {
|
|
5006
|
-
const v = entry.values[locale]?.value;
|
|
5007
|
-
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
5008
|
-
}
|
|
5009
|
-
}
|
|
5010
|
-
return out;
|
|
5011
|
-
}
|
|
5012
|
-
};
|
|
5013
|
-
var whitespaceRule = {
|
|
5014
|
-
id: "whitespace",
|
|
5015
|
-
run(state, ctx) {
|
|
5016
|
-
const out = [];
|
|
5017
|
-
for (const key of Object.keys(state.keys)) {
|
|
5018
|
-
const entry = state.keys[key];
|
|
5019
|
-
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
5020
|
-
const srcEdge = src !== src.trim();
|
|
5021
|
-
for (const locale of ctx.targetLocales) {
|
|
5022
|
-
const v = entry.values[locale]?.value;
|
|
5023
|
-
if (!v) continue;
|
|
5024
|
-
if (v !== v.trim() !== srcEdge) {
|
|
5025
|
-
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
5026
|
-
}
|
|
5027
|
-
}
|
|
5028
|
-
}
|
|
5029
|
-
return out;
|
|
5030
|
-
}
|
|
5031
|
-
};
|
|
5032
|
-
var placeholderMismatchRule = {
|
|
5033
|
-
id: "placeholder-mismatch",
|
|
5034
|
-
run(state, ctx) {
|
|
5035
|
-
const out = [];
|
|
5036
|
-
for (const key of Object.keys(state.keys)) {
|
|
5037
|
-
const entry = state.keys[key];
|
|
5038
|
-
if (entry.plural) {
|
|
5039
|
-
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
5040
|
-
if (!srcForm) continue;
|
|
5041
|
-
for (const locale of ctx.targetLocales) {
|
|
5042
|
-
const forms = entry.values[locale]?.forms;
|
|
5043
|
-
if (!forms) continue;
|
|
5044
|
-
const bad = Object.entries(forms).some(
|
|
5045
|
-
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
5046
|
-
);
|
|
5047
|
-
if (bad) {
|
|
5048
|
-
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
5049
|
-
}
|
|
5050
|
-
}
|
|
5051
|
-
continue;
|
|
5052
|
-
}
|
|
5053
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5054
|
-
if (!src) continue;
|
|
5055
|
-
for (const locale of ctx.targetLocales) {
|
|
5056
|
-
const v = entry.values[locale]?.value;
|
|
5057
|
-
if (!v) continue;
|
|
5058
|
-
if (!placeholdersMatch(src, v)) {
|
|
5059
|
-
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
5060
|
-
}
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
return out;
|
|
5064
|
-
}
|
|
5065
|
-
};
|
|
5066
|
-
var icuMismatchRule = {
|
|
5067
|
-
id: "icu-mismatch",
|
|
5068
|
-
run(state, ctx) {
|
|
5069
|
-
const out = [];
|
|
5070
|
-
for (const key of Object.keys(state.keys)) {
|
|
5071
|
-
const entry = state.keys[key];
|
|
5072
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5073
|
-
if (!src) continue;
|
|
5074
|
-
const srcIcu = isIcuPluralOrSelect(src);
|
|
5075
|
-
for (const locale of ctx.targetLocales) {
|
|
5076
|
-
const v = entry.values[locale]?.value;
|
|
5077
|
-
if (!v) continue;
|
|
5078
|
-
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
5079
|
-
out.push({
|
|
5080
|
-
ruleId: "icu-mismatch",
|
|
5081
|
-
key,
|
|
5082
|
-
locale,
|
|
5083
|
-
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
5084
|
-
});
|
|
5085
|
-
}
|
|
5086
|
-
}
|
|
5087
|
-
}
|
|
5088
|
-
return out;
|
|
5089
|
-
}
|
|
5090
|
-
};
|
|
5091
|
-
var maxLengthRule = {
|
|
5092
|
-
id: "max-length",
|
|
5093
|
-
run(state, ctx) {
|
|
5094
|
-
const out = [];
|
|
5095
|
-
for (const key of Object.keys(state.keys)) {
|
|
5096
|
-
const entry = state.keys[key];
|
|
5097
|
-
const max = entry.maxLength;
|
|
5098
|
-
if (max == null) continue;
|
|
5099
|
-
for (const locale of ctx.targetLocales) {
|
|
5100
|
-
const v = entry.values[locale]?.value;
|
|
5101
|
-
if (v && v.length > max) {
|
|
5102
|
-
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
5103
|
-
}
|
|
5104
|
-
}
|
|
5105
|
-
}
|
|
5106
|
-
return out;
|
|
5107
|
-
}
|
|
5108
|
-
};
|
|
5109
|
-
var glossaryViolationRule = {
|
|
5110
|
-
id: "glossary-violation",
|
|
5111
|
-
run(state, ctx) {
|
|
5112
|
-
const out = [];
|
|
5113
|
-
for (const key of Object.keys(state.keys)) {
|
|
5114
|
-
const entry = state.keys[key];
|
|
5115
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5116
|
-
if (!src) continue;
|
|
5117
|
-
for (const locale of ctx.targetLocales) {
|
|
5118
|
-
const v = entry.values[locale]?.value;
|
|
5119
|
-
if (!v) continue;
|
|
5120
|
-
for (const hint of relevantGlossary(src, locale, ctx.glossary)) {
|
|
5121
|
-
if (hint.doNotTranslate && !v.includes(hint.term)) {
|
|
5122
|
-
out.push({ ruleId: "glossary-violation", key, locale, message: `do-not-translate term "${hint.term}" is missing or altered` });
|
|
5123
|
-
}
|
|
5124
|
-
if (hint.forced && !v.includes(hint.forced)) {
|
|
5125
|
-
out.push({ ruleId: "glossary-violation", key, locale, message: `expected glossary translation "${hint.forced}" for "${hint.term}"` });
|
|
5126
|
-
}
|
|
5127
|
-
}
|
|
5128
|
-
}
|
|
5129
|
-
}
|
|
5130
|
-
return out;
|
|
5131
|
-
}
|
|
5132
|
-
};
|
|
5133
|
-
var ALL_RULES = [
|
|
5134
|
-
emptySourceRule,
|
|
5135
|
-
emptyTranslationRule,
|
|
5136
|
-
placeholderMismatchRule,
|
|
5137
|
-
icuMismatchRule,
|
|
5138
|
-
glossaryViolationRule,
|
|
5139
|
-
maxLengthRule,
|
|
5140
|
-
identicalToSourceRule,
|
|
5141
|
-
whitespaceRule,
|
|
5142
|
-
spellingRule
|
|
5143
|
-
];
|
|
5144
|
-
|
|
5145
|
-
// src/server/lint/run.ts
|
|
5146
|
-
function resolveSeverity(id, config) {
|
|
5147
|
-
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
5148
|
-
}
|
|
5149
|
-
function sortFindings(findings) {
|
|
5150
|
-
return [...findings].sort(
|
|
5151
|
-
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
5152
|
-
);
|
|
5153
|
-
}
|
|
5154
|
-
function countSeverities(findings) {
|
|
5155
|
-
let error = 0, warn = 0;
|
|
5156
|
-
for (const f of findings) f.severity === "error" ? error++ : warn++;
|
|
5157
|
-
return { error, warn };
|
|
5158
|
-
}
|
|
5159
|
-
async function loadSpellers(locales, config, load, warn) {
|
|
5160
|
-
const map = /* @__PURE__ */ new Map();
|
|
5161
|
-
for (const locale of locales) {
|
|
5162
|
-
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
5163
|
-
const speller = await load(dictId);
|
|
5164
|
-
if (speller) map.set(locale, speller);
|
|
5165
|
-
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
5166
|
-
}
|
|
5167
|
-
return map;
|
|
5168
|
-
}
|
|
5169
|
-
async function runLint(state, options = {}) {
|
|
5170
|
-
const config = state.config.lint ?? {};
|
|
5171
|
-
const rules = options.rules ?? ALL_RULES;
|
|
5172
|
-
const warn = options.warn ?? ((m) => console.warn(m));
|
|
5173
|
-
const load = options.loadSpeller ?? defaultLoader;
|
|
5174
|
-
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
5175
|
-
const isActive = (rule) => {
|
|
5176
|
-
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
5177
|
-
return resolveSeverity(rule.id, config) !== "off";
|
|
5178
|
-
};
|
|
5179
|
-
const active = rules.filter(isActive);
|
|
5180
|
-
const spellingOn = active.some((r) => r.id === "spelling");
|
|
5181
|
-
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
5182
|
-
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
|
|
5183
|
-
const ctx = {
|
|
5184
|
-
config,
|
|
5185
|
-
sourceLocale: state.config.sourceLocale,
|
|
5186
|
-
targetLocales,
|
|
5187
|
-
glossary: state.glossary,
|
|
5188
|
-
spellers,
|
|
5189
|
-
allowWords
|
|
5190
|
-
};
|
|
5191
|
-
const ignoreRes = (config.ignore ?? []).map(globToRegExp);
|
|
5192
|
-
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
5193
|
-
const findings = [];
|
|
5194
|
-
for (const rule of active) {
|
|
5195
|
-
const severity = resolveSeverity(rule.id, config);
|
|
5196
|
-
for (const raw of rule.run(state, ctx)) {
|
|
5197
|
-
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
5198
|
-
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
5199
|
-
findings.push({ ...raw, severity });
|
|
5200
|
-
}
|
|
5201
|
-
}
|
|
5202
|
-
const sorted = sortFindings(findings);
|
|
5203
|
-
const counts = countSeverities(sorted);
|
|
5204
|
-
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
5205
|
-
}
|
|
5206
|
-
|
|
5207
|
-
// src/server/lint/outputs.ts
|
|
5208
|
-
init_adapters();
|
|
5209
|
-
import { readFileSync as readFileSync10, existsSync as existsSync8 } from "fs";
|
|
5210
|
-
import { resolve as resolve8 } from "path";
|
|
5211
|
-
function checkOutputs(state, root) {
|
|
5212
|
-
const out = [];
|
|
5213
|
-
for (const output of state.config.outputs) {
|
|
5214
|
-
const result = getAdapter(output.adapter).export(state, output);
|
|
5215
|
-
for (const file of result.files) {
|
|
5216
|
-
const abs = resolve8(root, file.path);
|
|
5217
|
-
const current = existsSync8(abs) ? readFileSync10(abs, "utf8") : null;
|
|
5218
|
-
if (current === null) {
|
|
5219
|
-
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
5220
|
-
} else if (current !== file.contents) {
|
|
5221
|
-
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
5222
|
-
}
|
|
5223
|
-
}
|
|
5224
|
-
}
|
|
5225
|
-
return out;
|
|
5226
|
-
}
|
|
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";
|
|
5227
5447
|
|
|
5228
5448
|
// src/server/lint/locate.ts
|
|
5229
5449
|
function locate(rawText, key) {
|
|
@@ -5366,7 +5586,7 @@ function watchTargetFor(statePath) {
|
|
|
5366
5586
|
return detectFormat(statePath) === "split" ? { path: splitDirFor(statePath), recursive: true } : { path: statePath, recursive: false };
|
|
5367
5587
|
}
|
|
5368
5588
|
async function runExport(args) {
|
|
5369
|
-
const root =
|
|
5589
|
+
const root = dirname5(resolve11(args.statePath));
|
|
5370
5590
|
const runOnce = () => {
|
|
5371
5591
|
const state = loadState(args.statePath);
|
|
5372
5592
|
const result = exportToDisk(state, root, args.adapter ? { adapter: args.adapter } : void 0);
|
|
@@ -5377,8 +5597,9 @@ async function runExport(args) {
|
|
|
5377
5597
|
return result;
|
|
5378
5598
|
};
|
|
5379
5599
|
if (!args.watch) {
|
|
5380
|
-
const { written, skipped } = runOnce();
|
|
5381
|
-
|
|
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(", ")})` : ""}.`);
|
|
5382
5603
|
return;
|
|
5383
5604
|
}
|
|
5384
5605
|
const { watch } = await import("fs");
|
|
@@ -5390,8 +5611,10 @@ async function runExport(args) {
|
|
|
5390
5611
|
clearTimeout(timer);
|
|
5391
5612
|
timer = setTimeout(() => {
|
|
5392
5613
|
try {
|
|
5393
|
-
const { written } = runOnce();
|
|
5394
|
-
if (written)
|
|
5614
|
+
const { written, deleted } = runOnce();
|
|
5615
|
+
if (written || deleted) {
|
|
5616
|
+
console.log(`Re-exported ${written} file(s)${deleted ? ` (${deleted} stale removed)` : ""}.`);
|
|
5617
|
+
}
|
|
5395
5618
|
} catch (e) {
|
|
5396
5619
|
console.error(e.message);
|
|
5397
5620
|
}
|
|
@@ -5402,7 +5625,7 @@ async function runExport(args) {
|
|
|
5402
5625
|
}
|
|
5403
5626
|
async function runTranslate(args) {
|
|
5404
5627
|
const state = loadState(args.statePath);
|
|
5405
|
-
const projectRoot =
|
|
5628
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5406
5629
|
const reqs = selectRequests(state, {
|
|
5407
5630
|
// Default to translating only empty values; --all forces a full re-translate
|
|
5408
5631
|
// (overwriting existing translations). --only missing stays as a no-op alias.
|
|
@@ -5472,7 +5695,7 @@ function printReport(report, format, rawText) {
|
|
|
5472
5695
|
}
|
|
5473
5696
|
async function runLintCmd(args) {
|
|
5474
5697
|
const state = loadState(args.statePath);
|
|
5475
|
-
const rawText =
|
|
5698
|
+
const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5476
5699
|
const report = await runLint(state, { locales: args.locales, ruleIds: args.ruleIds });
|
|
5477
5700
|
printReport(report, args.format, rawText);
|
|
5478
5701
|
const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
|
|
@@ -5492,8 +5715,8 @@ async function runCheck(args) {
|
|
|
5492
5715
|
process.exitCode = 1;
|
|
5493
5716
|
return;
|
|
5494
5717
|
}
|
|
5495
|
-
const rawText =
|
|
5496
|
-
const root =
|
|
5718
|
+
const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5719
|
+
const root = dirname5(resolve11(args.statePath));
|
|
5497
5720
|
const lint = await runLint(state, {});
|
|
5498
5721
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
5499
5722
|
const counts = countSeverities(findings);
|
|
@@ -5502,10 +5725,10 @@ async function runCheck(args) {
|
|
|
5502
5725
|
if (!report.ok) process.exitCode = 1;
|
|
5503
5726
|
}
|
|
5504
5727
|
async function runImportCmd(args) {
|
|
5505
|
-
const { runImport: runImport2 } = await Promise.resolve().then(() => (
|
|
5506
|
-
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));
|
|
5507
5730
|
const out = resolve11(projectRoot, "glotfile.json");
|
|
5508
|
-
if (
|
|
5731
|
+
if (existsSync12(out) && !args.importForce) {
|
|
5509
5732
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
5510
5733
|
process.exitCode = 1;
|
|
5511
5734
|
return;
|
|
@@ -5530,7 +5753,7 @@ async function runImportCmd(args) {
|
|
|
5530
5753
|
}
|
|
5531
5754
|
async function runBuildContext(args) {
|
|
5532
5755
|
const state = loadState(args.statePath);
|
|
5533
|
-
const projectRoot =
|
|
5756
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5534
5757
|
const cache2 = loadUsageCache(projectRoot);
|
|
5535
5758
|
if (!cache2) {
|
|
5536
5759
|
console.error("No usage index found. Run 'glotfile scan' first.");
|
|
@@ -5601,7 +5824,7 @@ async function runBuildContext(args) {
|
|
|
5601
5824
|
}
|
|
5602
5825
|
async function runScanCmd(args) {
|
|
5603
5826
|
const state = loadState(args.statePath);
|
|
5604
|
-
const projectRoot =
|
|
5827
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5605
5828
|
const existing = loadUsageCache(projectRoot);
|
|
5606
5829
|
const result = runScan(projectRoot, state.config.scan ?? {}, existing);
|
|
5607
5830
|
const fileCount2 = Object.keys(result.files).length;
|
|
@@ -5620,7 +5843,7 @@ async function runPrune(args) {
|
|
|
5620
5843
|
for (const k of findEmptySourceKeys(state)) toRemove.add(k);
|
|
5621
5844
|
}
|
|
5622
5845
|
if (args.unused) {
|
|
5623
|
-
const projectRoot =
|
|
5846
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5624
5847
|
const cache2 = runScan(projectRoot, state.config.scan ?? {}, loadUsageCache(projectRoot));
|
|
5625
5848
|
const used = new Set(computeUsedKeys(state, cache2));
|
|
5626
5849
|
for (const k of Object.keys(state.keys)) {
|