glotfile 0.4.2 → 0.4.4
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 +971 -499
- package/dist/server/server.js +778 -84
- package/dist/ui/assets/index-3IIAIpZW.css +1 -0
- package/dist/ui/assets/{index-CJ_nmOjf.js → index-pl7PaD7b.js} +62 -19
- 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];
|
|
@@ -901,6 +901,10 @@ function toI18next(value) {
|
|
|
901
901
|
if (isIcuPluralOrSelect(value)) return value;
|
|
902
902
|
return value.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}");
|
|
903
903
|
}
|
|
904
|
+
function toRuby(value) {
|
|
905
|
+
if (isIcuPluralOrSelect(value)) return value;
|
|
906
|
+
return value.replace(/(?<!%)\{(\w+)\}/g, "%{$1}");
|
|
907
|
+
}
|
|
904
908
|
function placeholdersMatch(source, translation) {
|
|
905
909
|
const a = extractPlaceholders(source).sort();
|
|
906
910
|
const b = extractPlaceholders(translation).sort();
|
|
@@ -1457,6 +1461,203 @@ var init_vue_i18n_json = __esm({
|
|
|
1457
1461
|
}
|
|
1458
1462
|
});
|
|
1459
1463
|
|
|
1464
|
+
// src/server/adapters/angular-xliff.ts
|
|
1465
|
+
function xmlEscape2(s) {
|
|
1466
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1467
|
+
}
|
|
1468
|
+
function renderInterpolations(text, ids) {
|
|
1469
|
+
let out = "";
|
|
1470
|
+
let last = 0;
|
|
1471
|
+
for (const m of text.matchAll(/\{(\w+)\}/g)) {
|
|
1472
|
+
const name = m[1];
|
|
1473
|
+
let id = ids.get(name);
|
|
1474
|
+
if (id === void 0) {
|
|
1475
|
+
id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
|
|
1476
|
+
ids.set(name, id);
|
|
1477
|
+
}
|
|
1478
|
+
out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
1479
|
+
last = m.index + m[0].length;
|
|
1480
|
+
}
|
|
1481
|
+
return out + xmlEscape2(text.slice(last));
|
|
1482
|
+
}
|
|
1483
|
+
function renderPluralIcu(forms, ids) {
|
|
1484
|
+
const cats = [
|
|
1485
|
+
...Object.keys(forms).filter((c) => c.startsWith("=")),
|
|
1486
|
+
...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
|
|
1487
|
+
];
|
|
1488
|
+
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
|
|
1489
|
+
return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
|
|
1490
|
+
}
|
|
1491
|
+
function renderEmbeddedIcu(value) {
|
|
1492
|
+
const renamed = value.replace(
|
|
1493
|
+
/\{\s*\w+\s*,\s*(plural|select|selectordinal)\s*,/g,
|
|
1494
|
+
(_, type) => `{${type === "plural" ? "VAR_PLURAL" : "VAR_SELECT"}, ${type},`
|
|
1495
|
+
);
|
|
1496
|
+
return xmlEscape2(renamed);
|
|
1497
|
+
}
|
|
1498
|
+
function renderScalar(value, ids) {
|
|
1499
|
+
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
|
|
1500
|
+
}
|
|
1501
|
+
var DEFAULT_LOCALE_CASE7, angularXliff;
|
|
1502
|
+
var init_angular_xliff = __esm({
|
|
1503
|
+
"src/server/adapters/angular-xliff.ts"() {
|
|
1504
|
+
"use strict";
|
|
1505
|
+
init_adapters();
|
|
1506
|
+
init_options();
|
|
1507
|
+
init_placeholders();
|
|
1508
|
+
init_schema();
|
|
1509
|
+
DEFAULT_LOCALE_CASE7 = "bcp47-hyphen";
|
|
1510
|
+
angularXliff = {
|
|
1511
|
+
name: "angular-xliff",
|
|
1512
|
+
capabilities: {
|
|
1513
|
+
plural: "native",
|
|
1514
|
+
select: "native",
|
|
1515
|
+
nesting: "flat",
|
|
1516
|
+
metadata: true,
|
|
1517
|
+
placeholderStyle: "icu",
|
|
1518
|
+
fileGrouping: "per-locale"
|
|
1519
|
+
},
|
|
1520
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE7,
|
|
1521
|
+
export(state, output) {
|
|
1522
|
+
const files = [];
|
|
1523
|
+
const warnings = [];
|
|
1524
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
|
|
1525
|
+
const sourceLocale = state.config.sourceLocale;
|
|
1526
|
+
const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE7);
|
|
1527
|
+
const emptyAs = resolveEmptyAs(output, "source");
|
|
1528
|
+
const keys = Object.keys(state.keys).sort();
|
|
1529
|
+
for (const locale of state.config.locales) {
|
|
1530
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7);
|
|
1531
|
+
const units = [];
|
|
1532
|
+
for (const key of keys) {
|
|
1533
|
+
const entry = state.keys[key];
|
|
1534
|
+
let source;
|
|
1535
|
+
let target;
|
|
1536
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1537
|
+
if (entry.plural) {
|
|
1538
|
+
const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
|
|
1539
|
+
if (targetForms === null) continue;
|
|
1540
|
+
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
|
|
1541
|
+
target = renderPluralIcu(targetForms, ids);
|
|
1542
|
+
} else {
|
|
1543
|
+
const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
|
|
1544
|
+
if (targetValue === null) continue;
|
|
1545
|
+
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
|
|
1546
|
+
target = renderScalar(targetValue, ids);
|
|
1547
|
+
}
|
|
1548
|
+
units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
|
|
1549
|
+
units.push(` <source>${source}</source>`);
|
|
1550
|
+
units.push(` <target>${target}</target>`);
|
|
1551
|
+
if (entry.description) {
|
|
1552
|
+
units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
|
|
1553
|
+
}
|
|
1554
|
+
units.push(` </trans-unit>`);
|
|
1555
|
+
}
|
|
1556
|
+
const contents = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
1557
|
+
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
|
1558
|
+
<file source-language="${xmlEscape2(sourceToken)}" target-language="${xmlEscape2(token)}" datatype="plaintext" original="ng2.template">
|
|
1559
|
+
<body>
|
|
1560
|
+
` + (units.length ? units.join("\n") + "\n" : "") + ` </body>
|
|
1561
|
+
</file>
|
|
1562
|
+
</xliff>
|
|
1563
|
+
`;
|
|
1564
|
+
files.push({ path: resolvePath(output.path, token), contents });
|
|
1565
|
+
}
|
|
1566
|
+
return { files, warnings };
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
// src/server/adapters/rails-yaml.ts
|
|
1573
|
+
function yamlString(s) {
|
|
1574
|
+
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + '"';
|
|
1575
|
+
}
|
|
1576
|
+
function yamlKey(k) {
|
|
1577
|
+
if (RESERVED_KEYS.has(k.toLowerCase())) return yamlString(k);
|
|
1578
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(k) ? k : yamlString(k);
|
|
1579
|
+
}
|
|
1580
|
+
function yamlMap(node, indent, level) {
|
|
1581
|
+
const pad = " ".repeat(indent * level);
|
|
1582
|
+
const lines = [];
|
|
1583
|
+
for (const key of Object.keys(node).sort()) {
|
|
1584
|
+
const v = node[key];
|
|
1585
|
+
if (v && typeof v === "object") {
|
|
1586
|
+
lines.push(`${pad}${yamlKey(key)}:`);
|
|
1587
|
+
lines.push(...yamlMap(v, indent, level + 1));
|
|
1588
|
+
} else {
|
|
1589
|
+
lines.push(`${pad}${yamlKey(key)}: ${yamlString(String(v))}`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return lines;
|
|
1593
|
+
}
|
|
1594
|
+
var RESERVED_KEYS, DEFAULT_LOCALE_CASE8, railsYaml;
|
|
1595
|
+
var init_rails_yaml = __esm({
|
|
1596
|
+
"src/server/adapters/rails-yaml.ts"() {
|
|
1597
|
+
"use strict";
|
|
1598
|
+
init_adapters();
|
|
1599
|
+
init_shared();
|
|
1600
|
+
init_options();
|
|
1601
|
+
init_placeholders();
|
|
1602
|
+
init_schema();
|
|
1603
|
+
RESERVED_KEYS = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "on", "off", "null", "y", "n"]);
|
|
1604
|
+
DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
|
|
1605
|
+
railsYaml = {
|
|
1606
|
+
name: "rails-yaml",
|
|
1607
|
+
capabilities: {
|
|
1608
|
+
plural: "native",
|
|
1609
|
+
select: "lossy",
|
|
1610
|
+
nesting: "nested",
|
|
1611
|
+
metadata: false,
|
|
1612
|
+
placeholderStyle: "named",
|
|
1613
|
+
fileGrouping: "per-locale"
|
|
1614
|
+
},
|
|
1615
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE8,
|
|
1616
|
+
export(state, output) {
|
|
1617
|
+
const warnings = [];
|
|
1618
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
|
|
1619
|
+
const { indent, finalNewline } = resolveFormat(state, output);
|
|
1620
|
+
const emptyAs = resolveEmptyAs(output, "omit");
|
|
1621
|
+
const files = [];
|
|
1622
|
+
for (const locale of state.config.locales) {
|
|
1623
|
+
const flat = {};
|
|
1624
|
+
for (const [key, entry] of Object.entries(state.keys)) {
|
|
1625
|
+
if (entry.plural) {
|
|
1626
|
+
const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1627
|
+
if (!forms) continue;
|
|
1628
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
1629
|
+
const body2 = forms[cat];
|
|
1630
|
+
if (body2 !== void 0) flat[`${key}.${cat}`] = toRuby(body2);
|
|
1631
|
+
}
|
|
1632
|
+
} else {
|
|
1633
|
+
const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1634
|
+
if (raw === null) continue;
|
|
1635
|
+
if (raw && isIcuPluralOrSelect(raw)) {
|
|
1636
|
+
warnings.push({
|
|
1637
|
+
code: "lossy-plural",
|
|
1638
|
+
key,
|
|
1639
|
+
locale,
|
|
1640
|
+
message: "rails-yaml cannot represent ICU plural/select; written unconverted"
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
flat[key] = toRuby(raw);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
const { tree: nested, collisions } = nestKeys(flat);
|
|
1647
|
+
for (const c of collisions) {
|
|
1648
|
+
warnings.push({ code: "key-collision", key: c, locale, message: "key is both a leaf and a parent; dropped from nested output" });
|
|
1649
|
+
}
|
|
1650
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
|
|
1651
|
+
const body = [`${yamlKey(token)}:`, ...yamlMap(nested, indent, 1)].join("\n");
|
|
1652
|
+
files.push({ path: resolvePath(output.path, token), contents: finalNewline ? body + "\n" : body });
|
|
1653
|
+
}
|
|
1654
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
1655
|
+
return { files, warnings };
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1460
1661
|
// src/server/adapters/index.ts
|
|
1461
1662
|
function resolvePath(template, locale, namespace = "") {
|
|
1462
1663
|
return template.replaceAll("{locale}", locale).replaceAll("{namespace}", namespace);
|
|
@@ -1488,7 +1689,9 @@ function getRegistry() {
|
|
|
1488
1689
|
[i18nextJson.name]: i18nextJson,
|
|
1489
1690
|
[gettextPo.name]: gettextPo,
|
|
1490
1691
|
[appleStringsdict.name]: appleStringsdict,
|
|
1491
|
-
[vueI18nJson.name]: vueI18nJson
|
|
1692
|
+
[vueI18nJson.name]: vueI18nJson,
|
|
1693
|
+
[angularXliff.name]: angularXliff,
|
|
1694
|
+
[railsYaml.name]: railsYaml
|
|
1492
1695
|
};
|
|
1493
1696
|
}
|
|
1494
1697
|
function getAdapter(name) {
|
|
@@ -1507,12 +1710,14 @@ var init_adapters = __esm({
|
|
|
1507
1710
|
init_gettext_po();
|
|
1508
1711
|
init_apple_stringsdict();
|
|
1509
1712
|
init_vue_i18n_json();
|
|
1713
|
+
init_angular_xliff();
|
|
1714
|
+
init_rails_yaml();
|
|
1510
1715
|
}
|
|
1511
1716
|
});
|
|
1512
1717
|
|
|
1513
1718
|
// src/server/export-run.ts
|
|
1514
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
1515
|
-
import { resolve } from "path";
|
|
1719
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, rmdirSync, statSync, unlinkSync } from "fs";
|
|
1720
|
+
import { dirname as dirname2, resolve, sep } from "path";
|
|
1516
1721
|
function effectiveLocales(config) {
|
|
1517
1722
|
const limit = config.exportLocales;
|
|
1518
1723
|
if (!limit || limit.length === 0) return config.locales;
|
|
@@ -1523,14 +1728,81 @@ function narrowForExport(state) {
|
|
|
1523
1728
|
if (locales.length === state.config.locales.length) return state;
|
|
1524
1729
|
return { ...state, config: { ...state.config, locales } };
|
|
1525
1730
|
}
|
|
1731
|
+
function escapeRegExp(s) {
|
|
1732
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1733
|
+
}
|
|
1734
|
+
function segmentRegExp(segment) {
|
|
1735
|
+
const pattern = escapeRegExp(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
|
|
1736
|
+
return new RegExp(`^${pattern}$`);
|
|
1737
|
+
}
|
|
1738
|
+
function removeEmptyDirs(dir, stopAt) {
|
|
1739
|
+
let current = dir;
|
|
1740
|
+
while (current !== stopAt && current.startsWith(stopAt + sep)) {
|
|
1741
|
+
try {
|
|
1742
|
+
rmdirSync(current);
|
|
1743
|
+
} catch {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
current = dirname2(current);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
1750
|
+
const segments = output.path.split("/").filter(Boolean);
|
|
1751
|
+
if (!segments.some((s) => s.includes("{locale}"))) return 0;
|
|
1752
|
+
const root = resolve(projectRoot);
|
|
1753
|
+
let deleted = 0;
|
|
1754
|
+
const stale = (token) => token !== void 0 && !validTokens.has(token) && LOCALE_TOKEN.test(token);
|
|
1755
|
+
const visit = (dir, index, locale) => {
|
|
1756
|
+
const segment = segments[index];
|
|
1757
|
+
const isLast = index === segments.length - 1;
|
|
1758
|
+
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
1759
|
+
const next = resolve(dir, segment);
|
|
1760
|
+
if (isLast) {
|
|
1761
|
+
if (stale(locale) && existsSync3(next) && statSync(next).isFile()) {
|
|
1762
|
+
unlinkSync(next);
|
|
1763
|
+
deleted++;
|
|
1764
|
+
removeEmptyDirs(dir, root);
|
|
1765
|
+
}
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
visit(next, index + 1, locale);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
const re = segmentRegExp(segment);
|
|
1772
|
+
let entries;
|
|
1773
|
+
try {
|
|
1774
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1775
|
+
} catch {
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
for (const entry of entries) {
|
|
1779
|
+
const m = entry.name.match(re);
|
|
1780
|
+
if (!m) continue;
|
|
1781
|
+
const token = m.groups?.locale ?? locale;
|
|
1782
|
+
if (isLast) {
|
|
1783
|
+
if (!entry.isFile() || !stale(token)) continue;
|
|
1784
|
+
unlinkSync(resolve(dir, entry.name));
|
|
1785
|
+
deleted++;
|
|
1786
|
+
removeEmptyDirs(dir, root);
|
|
1787
|
+
} else if (entry.isDirectory()) {
|
|
1788
|
+
visit(resolve(dir, entry.name), index + 1, token);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
visit(root, 0, void 0);
|
|
1793
|
+
return deleted;
|
|
1794
|
+
}
|
|
1526
1795
|
function exportToDisk(state, projectRoot, opts) {
|
|
1796
|
+
const allLocales = state.config.locales;
|
|
1527
1797
|
state = narrowForExport(state);
|
|
1528
1798
|
const outputs = opts?.adapter ? state.config.outputs.filter((o) => o.adapter === opts.adapter) : state.config.outputs;
|
|
1529
1799
|
const warnings = [];
|
|
1530
1800
|
let written = 0;
|
|
1531
1801
|
let skipped = 0;
|
|
1802
|
+
let deleted = 0;
|
|
1532
1803
|
for (const output of outputs) {
|
|
1533
|
-
const
|
|
1804
|
+
const adapter = getAdapter(output.adapter);
|
|
1805
|
+
const result = adapter.export(state, output);
|
|
1534
1806
|
warnings.push(...result.warnings);
|
|
1535
1807
|
const writtenPaths = /* @__PURE__ */ new Set();
|
|
1536
1808
|
for (const f of result.files) {
|
|
@@ -1552,14 +1824,19 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
1552
1824
|
writeFileAtomic(abs, f.contents);
|
|
1553
1825
|
written++;
|
|
1554
1826
|
}
|
|
1827
|
+
const validTokens = new Set(allLocales.map((l) => resolveLocaleToken(output, l, adapter.defaultLocaleCase)));
|
|
1828
|
+
deleted += pruneStaleLocaleFiles(output, validTokens, projectRoot);
|
|
1555
1829
|
}
|
|
1556
|
-
return { written, skipped, warnings };
|
|
1830
|
+
return { written, skipped, deleted, warnings };
|
|
1557
1831
|
}
|
|
1832
|
+
var LOCALE_TOKEN;
|
|
1558
1833
|
var init_export_run = __esm({
|
|
1559
1834
|
"src/server/export-run.ts"() {
|
|
1560
1835
|
"use strict";
|
|
1561
1836
|
init_adapters();
|
|
1837
|
+
init_options();
|
|
1562
1838
|
init_atomic_write();
|
|
1839
|
+
LOCALE_TOKEN = /^[a-z]{2,3}([_-][a-z0-9]+)*$/i;
|
|
1563
1840
|
}
|
|
1564
1841
|
});
|
|
1565
1842
|
|
|
@@ -2232,13 +2509,13 @@ var init_ai = __esm({
|
|
|
2232
2509
|
});
|
|
2233
2510
|
|
|
2234
2511
|
// src/server/glotfile-dir.ts
|
|
2235
|
-
import { existsSync as
|
|
2512
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2236
2513
|
import { resolve as resolve2 } from "path";
|
|
2237
2514
|
function ensureGlotfileDir(projectRoot) {
|
|
2238
2515
|
const dir = resolve2(projectRoot, ".glotfile");
|
|
2239
2516
|
mkdirSync3(dir, { recursive: true });
|
|
2240
2517
|
const ignore = resolve2(dir, ".gitignore");
|
|
2241
|
-
if (!
|
|
2518
|
+
if (!existsSync4(ignore)) {
|
|
2242
2519
|
try {
|
|
2243
2520
|
writeFileSync2(ignore, "*\n");
|
|
2244
2521
|
} catch {
|
|
@@ -2275,7 +2552,9 @@ function coerceAi(raw) {
|
|
|
2275
2552
|
contextBatchSize: typeof a.contextBatchSize === "number" && a.contextBatchSize > 0 ? a.contextBatchSize : void 0,
|
|
2276
2553
|
contextConcurrency: typeof a.contextConcurrency === "number" && a.contextConcurrency > 0 ? a.contextConcurrency : void 0,
|
|
2277
2554
|
vision: typeof a.vision === "boolean" ? a.vision : void 0,
|
|
2278
|
-
promptStyle: PROMPT_STYLES.includes(a.promptStyle) ? a.promptStyle : void 0
|
|
2555
|
+
promptStyle: PROMPT_STYLES.includes(a.promptStyle) ? a.promptStyle : void 0,
|
|
2556
|
+
inputPricePerMTok: typeof a.inputPricePerMTok === "number" && a.inputPricePerMTok >= 0 ? a.inputPricePerMTok : void 0,
|
|
2557
|
+
outputPricePerMTok: typeof a.outputPricePerMTok === "number" && a.outputPricePerMTok >= 0 ? a.outputPricePerMTok : void 0
|
|
2279
2558
|
};
|
|
2280
2559
|
}
|
|
2281
2560
|
function coerceProfiles(raw) {
|
|
@@ -2314,6 +2593,10 @@ function aiConfigError(ai) {
|
|
|
2314
2593
|
if (!(a.endpoint === null || a.endpoint === void 0 || typeof a.endpoint === "string")) return "ai.endpoint must be a string or null";
|
|
2315
2594
|
if (!(a.region === void 0 || a.region === null || typeof a.region === "string")) return "ai.region must be a string or null";
|
|
2316
2595
|
if (typeof a.batchSize !== "number") return "ai.batchSize must be a number";
|
|
2596
|
+
for (const f of ["inputPricePerMTok", "outputPricePerMTok"]) {
|
|
2597
|
+
const v = a[f];
|
|
2598
|
+
if (!(v === void 0 || v === null || typeof v === "number" && v >= 0)) return `ai.${f} must be a non-negative number`;
|
|
2599
|
+
}
|
|
2317
2600
|
return null;
|
|
2318
2601
|
}
|
|
2319
2602
|
var EDITOR_IDS, isEditorId, DEFAULT_AI, DEFAULT_EDITOR, settingsPath;
|
|
@@ -2349,7 +2632,7 @@ var init_glob = __esm({
|
|
|
2349
2632
|
});
|
|
2350
2633
|
|
|
2351
2634
|
// src/server/ai/run.ts
|
|
2352
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
2635
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
2353
2636
|
import { resolve as resolve4, extname } from "path";
|
|
2354
2637
|
function selectRequests(state, opts) {
|
|
2355
2638
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
@@ -2432,7 +2715,7 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
2432
2715
|
if (!mediaType) continue;
|
|
2433
2716
|
if (!cache2.has(screenshot)) {
|
|
2434
2717
|
const abs = resolve4(projectRoot, screenshot);
|
|
2435
|
-
if (!
|
|
2718
|
+
if (!existsSync5(abs)) {
|
|
2436
2719
|
cache2.set(screenshot, null);
|
|
2437
2720
|
} else {
|
|
2438
2721
|
const buf = readFileSync5(abs);
|
|
@@ -2529,8 +2812,120 @@ var init_run = __esm({
|
|
|
2529
2812
|
}
|
|
2530
2813
|
});
|
|
2531
2814
|
|
|
2815
|
+
// src/server/ai/pricing.ts
|
|
2816
|
+
function bareModelId(model) {
|
|
2817
|
+
let id = model.trim().toLowerCase();
|
|
2818
|
+
const slash = id.lastIndexOf("/");
|
|
2819
|
+
if (slash !== -1) id = id.slice(slash + 1);
|
|
2820
|
+
const anth = id.lastIndexOf("anthropic.");
|
|
2821
|
+
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
2822
|
+
return id;
|
|
2823
|
+
}
|
|
2824
|
+
function resolvePricing(ai) {
|
|
2825
|
+
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
2826
|
+
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
2827
|
+
}
|
|
2828
|
+
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
2829
|
+
const id = bareModelId(ai.model);
|
|
2830
|
+
let best;
|
|
2831
|
+
for (const row of PRICE_TABLE) {
|
|
2832
|
+
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
2833
|
+
}
|
|
2834
|
+
return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
|
|
2835
|
+
}
|
|
2836
|
+
var PRICE_TABLE, FREE_PROVIDERS;
|
|
2837
|
+
var init_pricing = __esm({
|
|
2838
|
+
"src/server/ai/pricing.ts"() {
|
|
2839
|
+
"use strict";
|
|
2840
|
+
PRICE_TABLE = [
|
|
2841
|
+
["claude-fable-5", 10, 50],
|
|
2842
|
+
["claude-mythos-5", 10, 50],
|
|
2843
|
+
// Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
|
|
2844
|
+
// the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
|
|
2845
|
+
["claude-opus-4-1", 15, 75],
|
|
2846
|
+
["claude-opus-4-0", 15, 75],
|
|
2847
|
+
["claude-opus-4-2025", 15, 75],
|
|
2848
|
+
["claude-opus-4", 5, 25],
|
|
2849
|
+
["claude-sonnet-4", 3, 15],
|
|
2850
|
+
["claude-haiku-4", 1, 5],
|
|
2851
|
+
["claude-3-5-haiku", 0.8, 4],
|
|
2852
|
+
["gpt-5.5-pro", 30, 180],
|
|
2853
|
+
["gpt-5.5", 5, 30],
|
|
2854
|
+
["gpt-5.4-pro", 30, 180],
|
|
2855
|
+
["gpt-5.4-mini", 0.75, 4.5],
|
|
2856
|
+
["gpt-5.4-nano", 0.2, 1.25],
|
|
2857
|
+
["gpt-5.4", 2.5, 15],
|
|
2858
|
+
["gpt-5.3-codex", 1.75, 14]
|
|
2859
|
+
];
|
|
2860
|
+
FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
|
|
2864
|
+
// src/server/ai/estimate.ts
|
|
2865
|
+
function estimateTokens(text) {
|
|
2866
|
+
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
2867
|
+
return Math.ceil((text.length - cjk) / 4 + cjk / 2);
|
|
2868
|
+
}
|
|
2869
|
+
function estimateOutputTokens(req) {
|
|
2870
|
+
const translated = Math.ceil(estimateTokens(req.source) * EXPANSION);
|
|
2871
|
+
if (req.plural) {
|
|
2872
|
+
return ITEM_REPLY_OVERHEAD + req.plural.categories.length * (translated + FORM_REPLY_OVERHEAD);
|
|
2873
|
+
}
|
|
2874
|
+
return ITEM_REPLY_OVERHEAD + translated;
|
|
2875
|
+
}
|
|
2876
|
+
function estimateTranslation(state, ai, opts) {
|
|
2877
|
+
const reqs = selectRequests(state, opts);
|
|
2878
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
2879
|
+
for (const r of reqs) {
|
|
2880
|
+
let group = byLocale.get(r.targetLocale);
|
|
2881
|
+
if (!group) {
|
|
2882
|
+
group = [];
|
|
2883
|
+
byLocale.set(r.targetLocale, group);
|
|
2884
|
+
}
|
|
2885
|
+
group.push(r);
|
|
2886
|
+
}
|
|
2887
|
+
const perLocale = [];
|
|
2888
|
+
for (const [locale, group] of byLocale) {
|
|
2889
|
+
let inputTokens2 = 0;
|
|
2890
|
+
let outputTokens2 = 0;
|
|
2891
|
+
const batches = chunk(group, Math.max(1, ai.batchSize));
|
|
2892
|
+
for (const batch of batches) {
|
|
2893
|
+
const system = buildSystemPrompt(batch.some((r) => r.plural !== void 0));
|
|
2894
|
+
inputTokens2 += estimateTokens(system) + estimateTokens(buildBatchPrompt(batch));
|
|
2895
|
+
for (const r of batch) outputTokens2 += estimateOutputTokens(r);
|
|
2896
|
+
}
|
|
2897
|
+
perLocale.push({ locale, requests: group.length, batches: batches.length, inputTokens: inputTokens2, outputTokens: outputTokens2 });
|
|
2898
|
+
}
|
|
2899
|
+
const inputTokens = perLocale.reduce((n, l) => n + l.inputTokens, 0);
|
|
2900
|
+
const outputTokens = perLocale.reduce((n, l) => n + l.outputTokens, 0);
|
|
2901
|
+
const pricing = resolvePricing(ai);
|
|
2902
|
+
return {
|
|
2903
|
+
requests: reqs.length,
|
|
2904
|
+
batches: perLocale.reduce((n, l) => n + l.batches, 0),
|
|
2905
|
+
perLocale,
|
|
2906
|
+
inputTokens,
|
|
2907
|
+
outputTokens,
|
|
2908
|
+
pricing,
|
|
2909
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
var CJK_RE, EXPANSION, ITEM_REPLY_OVERHEAD, FORM_REPLY_OVERHEAD;
|
|
2913
|
+
var init_estimate = __esm({
|
|
2914
|
+
"src/server/ai/estimate.ts"() {
|
|
2915
|
+
"use strict";
|
|
2916
|
+
init_run();
|
|
2917
|
+
init_provider();
|
|
2918
|
+
init_batch();
|
|
2919
|
+
init_pricing();
|
|
2920
|
+
CJK_RE = /[ -鿿가-豈-]/g;
|
|
2921
|
+
EXPANSION = 1.2;
|
|
2922
|
+
ITEM_REPLY_OVERHEAD = 16;
|
|
2923
|
+
FORM_REPLY_OVERHEAD = 8;
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2926
|
+
|
|
2532
2927
|
// src/server/log.ts
|
|
2533
|
-
import { appendFileSync, readFileSync as readFileSync6, existsSync as
|
|
2928
|
+
import { appendFileSync, readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
2534
2929
|
import { resolve as resolve5 } from "path";
|
|
2535
2930
|
function logPath(projectRoot) {
|
|
2536
2931
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -2541,7 +2936,7 @@ function appendLog(projectRoot, entry) {
|
|
|
2541
2936
|
}
|
|
2542
2937
|
function readLog(projectRoot, limit = 100) {
|
|
2543
2938
|
const path = logPath(projectRoot);
|
|
2544
|
-
if (!
|
|
2939
|
+
if (!existsSync6(path)) return [];
|
|
2545
2940
|
const lines = readFileSync6(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
2546
2941
|
const entries = lines.map((l) => JSON.parse(l));
|
|
2547
2942
|
return entries.reverse().slice(0, limit);
|
|
@@ -2554,11 +2949,11 @@ var init_log = __esm({
|
|
|
2554
2949
|
});
|
|
2555
2950
|
|
|
2556
2951
|
// src/server/scan.ts
|
|
2557
|
-
import { existsSync as
|
|
2952
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
2558
2953
|
import { resolve as resolve6 } from "path";
|
|
2559
2954
|
function loadUsageCache(projectRoot) {
|
|
2560
2955
|
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
2561
|
-
if (!
|
|
2956
|
+
if (!existsSync7(path)) return null;
|
|
2562
2957
|
try {
|
|
2563
2958
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2564
2959
|
} catch {
|
|
@@ -2574,8 +2969,10 @@ function findMissing(state) {
|
|
|
2574
2969
|
const targets = state.config.locales.filter((l) => l !== state.config.sourceLocale).sort();
|
|
2575
2970
|
const out = [];
|
|
2576
2971
|
for (const key of Object.keys(state.keys).sort()) {
|
|
2972
|
+
const entry = state.keys[key];
|
|
2973
|
+
if (entry.skipTranslate) continue;
|
|
2577
2974
|
for (const locale of targets) {
|
|
2578
|
-
const v =
|
|
2975
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
|
|
2579
2976
|
if (!v) out.push({ key, locale });
|
|
2580
2977
|
}
|
|
2581
2978
|
}
|
|
@@ -2601,7 +2998,7 @@ var init_scan = __esm({
|
|
|
2601
2998
|
});
|
|
2602
2999
|
|
|
2603
3000
|
// src/server/scanner.ts
|
|
2604
|
-
import { readdirSync as
|
|
3001
|
+
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync8 } from "fs";
|
|
2605
3002
|
import { join as join3, extname as extname2, relative } from "path";
|
|
2606
3003
|
function scannerForExt(ext) {
|
|
2607
3004
|
return EXT_SCANNER[ext] ?? null;
|
|
@@ -2722,7 +3119,7 @@ function isIncluded(relPath, includePatterns) {
|
|
|
2722
3119
|
function* walkFiles(dir, root, exclude) {
|
|
2723
3120
|
let entries;
|
|
2724
3121
|
try {
|
|
2725
|
-
entries =
|
|
3122
|
+
entries = readdirSync3(dir);
|
|
2726
3123
|
} catch {
|
|
2727
3124
|
return;
|
|
2728
3125
|
}
|
|
@@ -2732,7 +3129,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
2732
3129
|
const rel = relative(root, abs);
|
|
2733
3130
|
let st;
|
|
2734
3131
|
try {
|
|
2735
|
-
st =
|
|
3132
|
+
st = statSync2(abs);
|
|
2736
3133
|
} catch {
|
|
2737
3134
|
continue;
|
|
2738
3135
|
}
|
|
@@ -2761,7 +3158,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
2761
3158
|
const abs = join3(projectRoot, relPath);
|
|
2762
3159
|
let st;
|
|
2763
3160
|
try {
|
|
2764
|
-
st =
|
|
3161
|
+
st = statSync2(abs);
|
|
2765
3162
|
} catch {
|
|
2766
3163
|
continue;
|
|
2767
3164
|
}
|
|
@@ -2879,7 +3276,7 @@ var init_scanner = __esm({
|
|
|
2879
3276
|
});
|
|
2880
3277
|
|
|
2881
3278
|
// src/server/ai/context.ts
|
|
2882
|
-
import { existsSync as
|
|
3279
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
2883
3280
|
import { resolve as resolve7 } from "path";
|
|
2884
3281
|
function globToRegExp2(glob) {
|
|
2885
3282
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -2894,7 +3291,7 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
2894
3291
|
for (const ref of selected) {
|
|
2895
3292
|
const absPath = resolve7(projectRoot, ref.file);
|
|
2896
3293
|
if (!fileCache.has(ref.file)) {
|
|
2897
|
-
if (!
|
|
3294
|
+
if (!existsSync8(absPath)) continue;
|
|
2898
3295
|
const content = readFileSync9(absPath, "utf8");
|
|
2899
3296
|
fileCache.set(ref.file, content.split("\n"));
|
|
2900
3297
|
}
|
|
@@ -3039,103 +3436,446 @@ var init_context = __esm({
|
|
|
3039
3436
|
}
|
|
3040
3437
|
});
|
|
3041
3438
|
|
|
3042
|
-
// src/server/
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
function safeIsDir(p) {
|
|
3046
|
-
try {
|
|
3047
|
-
return statSync2(p).isDirectory();
|
|
3048
|
-
} catch {
|
|
3049
|
-
return false;
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
function listDirs(dir) {
|
|
3053
|
-
return readdirSync3(dir).filter((e) => safeIsDir(join4(dir, e)));
|
|
3054
|
-
}
|
|
3055
|
-
function fileCount(dir) {
|
|
3056
|
-
try {
|
|
3057
|
-
return readdirSync3(dir).length;
|
|
3058
|
-
} catch {
|
|
3059
|
-
return 0;
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
function pickSource(locales, sizeOf) {
|
|
3063
|
-
if (locales.includes("en")) return "en";
|
|
3064
|
-
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
3065
|
-
}
|
|
3066
|
-
function detectLaravel(root) {
|
|
3067
|
-
const localeRoot = [join4(root, "resources", "lang"), join4(root, "lang")].find(safeIsDir);
|
|
3068
|
-
if (!localeRoot) return null;
|
|
3069
|
-
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
3070
|
-
if (locales.length === 0) return null;
|
|
3071
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
3072
|
-
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
3073
|
-
}
|
|
3074
|
-
function detectVue(root) {
|
|
3075
|
-
for (const rel of VUE_DIR_CANDIDATES) {
|
|
3076
|
-
const localeRoot = join4(root, rel);
|
|
3077
|
-
if (!safeIsDir(localeRoot)) continue;
|
|
3078
|
-
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
3079
|
-
if (locales.length >= 2) {
|
|
3080
|
-
const sourceLocale = pickSource(locales, (loc) => {
|
|
3081
|
-
try {
|
|
3082
|
-
return statSync2(join4(localeRoot, `${loc}.json`)).size;
|
|
3083
|
-
} catch {
|
|
3084
|
-
return 0;
|
|
3085
|
-
}
|
|
3086
|
-
});
|
|
3087
|
-
return { format: "vue-i18n-json", localeRoot, locales, sourceLocale };
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
return null;
|
|
3091
|
-
}
|
|
3092
|
-
function detectArb(root) {
|
|
3093
|
-
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
3094
|
-
const localeRoot = join4(root, rel);
|
|
3095
|
-
if (!safeIsDir(localeRoot)) continue;
|
|
3096
|
-
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
3097
|
-
if (locales.length >= 1) {
|
|
3098
|
-
return { format: "flutter-arb", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
return null;
|
|
3439
|
+
// src/server/lint/spelling.ts
|
|
3440
|
+
function tokenize(text) {
|
|
3441
|
+
return text.match(new RegExp("\\p{L}[\\p{L}'']*", "gu")) ?? [];
|
|
3102
3442
|
}
|
|
3103
|
-
function
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
const
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
const d = fn(root);
|
|
3112
|
-
if (d) return d;
|
|
3113
|
-
}
|
|
3114
|
-
return null;
|
|
3443
|
+
function buildAllowWords(glossary, dictionary2 = []) {
|
|
3444
|
+
const set = /* @__PURE__ */ new Set();
|
|
3445
|
+
const add = (s) => {
|
|
3446
|
+
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
3447
|
+
};
|
|
3448
|
+
for (const g of glossary) add(g.term);
|
|
3449
|
+
for (const w of dictionary2) add(w);
|
|
3450
|
+
return set;
|
|
3115
3451
|
}
|
|
3116
|
-
var
|
|
3117
|
-
var
|
|
3118
|
-
"src/server/
|
|
3452
|
+
var spellingRule, defaultLoader;
|
|
3453
|
+
var init_spelling = __esm({
|
|
3454
|
+
"src/server/lint/spelling.ts"() {
|
|
3119
3455
|
"use strict";
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3456
|
+
init_placeholders();
|
|
3457
|
+
spellingRule = {
|
|
3458
|
+
id: "spelling",
|
|
3459
|
+
run(state, ctx) {
|
|
3460
|
+
const out = [];
|
|
3461
|
+
for (const key of Object.keys(state.keys)) {
|
|
3462
|
+
const entry = state.keys[key];
|
|
3463
|
+
for (const locale of ctx.targetLocales) {
|
|
3464
|
+
const speller = ctx.spellers.get(locale);
|
|
3465
|
+
if (!speller) continue;
|
|
3466
|
+
const value = entry.values[locale]?.value;
|
|
3467
|
+
if (!value) continue;
|
|
3468
|
+
const placeholders = new Set(extractPlaceholders(value).map((p) => p.toLowerCase()));
|
|
3469
|
+
for (const word of tokenize(value)) {
|
|
3470
|
+
const lower = word.toLowerCase();
|
|
3471
|
+
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
3472
|
+
if (!speller.correct(word)) {
|
|
3473
|
+
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
return out;
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
defaultLoader = async (dictId) => {
|
|
3482
|
+
try {
|
|
3483
|
+
const nspellMod = await import("nspell");
|
|
3484
|
+
const nspell2 = nspellMod.default ?? nspellMod;
|
|
3485
|
+
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
3486
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
3487
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
3488
|
+
return nspell2(dict);
|
|
3489
|
+
} catch {
|
|
3490
|
+
return null;
|
|
3491
|
+
}
|
|
3127
3492
|
};
|
|
3128
3493
|
}
|
|
3129
3494
|
});
|
|
3130
3495
|
|
|
3131
|
-
// src/server/
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3496
|
+
// src/server/lint/rules.ts
|
|
3497
|
+
var emptySourceRule, emptyTranslationRule, identicalToSourceRule, whitespaceRule, placeholderMismatchRule, icuMismatchRule, maxLengthRule, glossaryViolationRule, ALL_RULES;
|
|
3498
|
+
var init_rules = __esm({
|
|
3499
|
+
"src/server/lint/rules.ts"() {
|
|
3500
|
+
"use strict";
|
|
3501
|
+
init_scan();
|
|
3502
|
+
init_placeholders();
|
|
3503
|
+
init_run();
|
|
3504
|
+
init_spelling();
|
|
3505
|
+
emptySourceRule = {
|
|
3506
|
+
id: "empty-source",
|
|
3507
|
+
run(state, ctx) {
|
|
3508
|
+
const out = [];
|
|
3509
|
+
for (const key of Object.keys(state.keys)) {
|
|
3510
|
+
const entry = state.keys[key];
|
|
3511
|
+
const v = entry.plural ? entry.values[ctx.sourceLocale]?.forms?.other : entry.values[ctx.sourceLocale]?.value;
|
|
3512
|
+
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
3513
|
+
}
|
|
3514
|
+
return out;
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
emptyTranslationRule = {
|
|
3518
|
+
id: "empty-translation",
|
|
3519
|
+
run(state, ctx) {
|
|
3520
|
+
const out = [];
|
|
3521
|
+
for (const m of findMissing(state)) {
|
|
3522
|
+
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
3523
|
+
}
|
|
3524
|
+
for (const key of Object.keys(state.keys)) {
|
|
3525
|
+
for (const locale of ctx.targetLocales) {
|
|
3526
|
+
const v = state.keys[key].values[locale]?.value;
|
|
3527
|
+
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
return out;
|
|
3531
|
+
}
|
|
3532
|
+
};
|
|
3533
|
+
identicalToSourceRule = {
|
|
3534
|
+
id: "identical-to-source",
|
|
3535
|
+
run(state, ctx) {
|
|
3536
|
+
const out = [];
|
|
3537
|
+
for (const key of Object.keys(state.keys)) {
|
|
3538
|
+
const entry = state.keys[key];
|
|
3539
|
+
if (entry.skipTranslate) continue;
|
|
3540
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3541
|
+
if (!src) continue;
|
|
3542
|
+
for (const locale of ctx.targetLocales) {
|
|
3543
|
+
const v = entry.values[locale]?.value;
|
|
3544
|
+
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
return out;
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3550
|
+
whitespaceRule = {
|
|
3551
|
+
id: "whitespace",
|
|
3552
|
+
run(state, ctx) {
|
|
3553
|
+
const out = [];
|
|
3554
|
+
for (const key of Object.keys(state.keys)) {
|
|
3555
|
+
const entry = state.keys[key];
|
|
3556
|
+
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
3557
|
+
const srcEdge = src !== src.trim();
|
|
3558
|
+
for (const locale of ctx.targetLocales) {
|
|
3559
|
+
const v = entry.values[locale]?.value;
|
|
3560
|
+
if (!v) continue;
|
|
3561
|
+
if (v !== v.trim() !== srcEdge) {
|
|
3562
|
+
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
return out;
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
placeholderMismatchRule = {
|
|
3570
|
+
id: "placeholder-mismatch",
|
|
3571
|
+
run(state, ctx) {
|
|
3572
|
+
const out = [];
|
|
3573
|
+
for (const key of Object.keys(state.keys)) {
|
|
3574
|
+
const entry = state.keys[key];
|
|
3575
|
+
if (entry.plural) {
|
|
3576
|
+
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
3577
|
+
if (!srcForm) continue;
|
|
3578
|
+
for (const locale of ctx.targetLocales) {
|
|
3579
|
+
const forms = entry.values[locale]?.forms;
|
|
3580
|
+
if (!forms) continue;
|
|
3581
|
+
const bad = Object.entries(forms).some(
|
|
3582
|
+
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
3583
|
+
);
|
|
3584
|
+
if (bad) {
|
|
3585
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
continue;
|
|
3589
|
+
}
|
|
3590
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3591
|
+
if (!src) continue;
|
|
3592
|
+
for (const locale of ctx.targetLocales) {
|
|
3593
|
+
const v = entry.values[locale]?.value;
|
|
3594
|
+
if (!v) continue;
|
|
3595
|
+
if (!placeholdersMatch(src, v)) {
|
|
3596
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
return out;
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
icuMismatchRule = {
|
|
3604
|
+
id: "icu-mismatch",
|
|
3605
|
+
run(state, ctx) {
|
|
3606
|
+
const out = [];
|
|
3607
|
+
for (const key of Object.keys(state.keys)) {
|
|
3608
|
+
const entry = state.keys[key];
|
|
3609
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3610
|
+
if (!src) continue;
|
|
3611
|
+
const srcIcu = isIcuPluralOrSelect(src);
|
|
3612
|
+
for (const locale of ctx.targetLocales) {
|
|
3613
|
+
const v = entry.values[locale]?.value;
|
|
3614
|
+
if (!v) continue;
|
|
3615
|
+
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
3616
|
+
out.push({
|
|
3617
|
+
ruleId: "icu-mismatch",
|
|
3618
|
+
key,
|
|
3619
|
+
locale,
|
|
3620
|
+
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return out;
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
maxLengthRule = {
|
|
3629
|
+
id: "max-length",
|
|
3630
|
+
run(state, ctx) {
|
|
3631
|
+
const out = [];
|
|
3632
|
+
for (const key of Object.keys(state.keys)) {
|
|
3633
|
+
const entry = state.keys[key];
|
|
3634
|
+
const max = entry.maxLength;
|
|
3635
|
+
if (max == null) continue;
|
|
3636
|
+
for (const locale of ctx.targetLocales) {
|
|
3637
|
+
const v = entry.values[locale]?.value;
|
|
3638
|
+
if (v && v.length > max) {
|
|
3639
|
+
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
return out;
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
glossaryViolationRule = {
|
|
3647
|
+
id: "glossary-violation",
|
|
3648
|
+
run(state, ctx) {
|
|
3649
|
+
const out = [];
|
|
3650
|
+
for (const key of Object.keys(state.keys)) {
|
|
3651
|
+
const entry = state.keys[key];
|
|
3652
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
3653
|
+
if (!src) continue;
|
|
3654
|
+
for (const locale of ctx.targetLocales) {
|
|
3655
|
+
const v = entry.values[locale]?.value;
|
|
3656
|
+
if (!v) continue;
|
|
3657
|
+
for (const hint of relevantGlossary(src, locale, ctx.glossary)) {
|
|
3658
|
+
if (hint.doNotTranslate && !v.includes(hint.term)) {
|
|
3659
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `do-not-translate term "${hint.term}" is missing or altered` });
|
|
3660
|
+
}
|
|
3661
|
+
if (hint.forced && !v.includes(hint.forced)) {
|
|
3662
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `expected glossary translation "${hint.forced}" for "${hint.term}"` });
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
return out;
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
ALL_RULES = [
|
|
3671
|
+
emptySourceRule,
|
|
3672
|
+
emptyTranslationRule,
|
|
3673
|
+
placeholderMismatchRule,
|
|
3674
|
+
icuMismatchRule,
|
|
3675
|
+
glossaryViolationRule,
|
|
3676
|
+
maxLengthRule,
|
|
3677
|
+
identicalToSourceRule,
|
|
3678
|
+
whitespaceRule,
|
|
3679
|
+
spellingRule
|
|
3680
|
+
];
|
|
3681
|
+
}
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3684
|
+
// src/server/lint/run.ts
|
|
3685
|
+
function resolveSeverity(id, config) {
|
|
3686
|
+
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
3687
|
+
}
|
|
3688
|
+
function sortFindings(findings) {
|
|
3689
|
+
return [...findings].sort(
|
|
3690
|
+
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
function countSeverities(findings) {
|
|
3694
|
+
let error = 0, warn = 0;
|
|
3695
|
+
for (const f of findings) f.severity === "error" ? error++ : warn++;
|
|
3696
|
+
return { error, warn };
|
|
3697
|
+
}
|
|
3698
|
+
async function loadSpellers(locales, config, load, warn) {
|
|
3699
|
+
const map = /* @__PURE__ */ new Map();
|
|
3700
|
+
for (const locale of locales) {
|
|
3701
|
+
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
3702
|
+
const speller = await load(dictId);
|
|
3703
|
+
if (speller) map.set(locale, speller);
|
|
3704
|
+
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
3705
|
+
}
|
|
3706
|
+
return map;
|
|
3707
|
+
}
|
|
3708
|
+
async function runLint(state, options = {}) {
|
|
3709
|
+
const config = state.config.lint ?? {};
|
|
3710
|
+
const rules = options.rules ?? ALL_RULES;
|
|
3711
|
+
const warn = options.warn ?? ((m) => console.warn(m));
|
|
3712
|
+
const load = options.loadSpeller ?? defaultLoader;
|
|
3713
|
+
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
3714
|
+
const isActive = (rule) => {
|
|
3715
|
+
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
3716
|
+
return resolveSeverity(rule.id, config) !== "off";
|
|
3717
|
+
};
|
|
3718
|
+
const active = rules.filter(isActive);
|
|
3719
|
+
const spellingOn = active.some((r) => r.id === "spelling");
|
|
3720
|
+
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
3721
|
+
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
|
|
3722
|
+
const ctx = {
|
|
3723
|
+
config,
|
|
3724
|
+
sourceLocale: state.config.sourceLocale,
|
|
3725
|
+
targetLocales,
|
|
3726
|
+
glossary: state.glossary,
|
|
3727
|
+
spellers,
|
|
3728
|
+
allowWords
|
|
3729
|
+
};
|
|
3730
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp);
|
|
3731
|
+
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
3732
|
+
const findings = [];
|
|
3733
|
+
for (const rule of active) {
|
|
3734
|
+
const severity = resolveSeverity(rule.id, config);
|
|
3735
|
+
for (const raw of rule.run(state, ctx)) {
|
|
3736
|
+
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
3737
|
+
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
3738
|
+
findings.push({ ...raw, severity });
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
const sorted = sortFindings(findings);
|
|
3742
|
+
const counts = countSeverities(sorted);
|
|
3743
|
+
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
3744
|
+
}
|
|
3745
|
+
var init_run2 = __esm({
|
|
3746
|
+
"src/server/lint/run.ts"() {
|
|
3747
|
+
"use strict";
|
|
3748
|
+
init_glob();
|
|
3749
|
+
init_registry();
|
|
3750
|
+
init_rules();
|
|
3751
|
+
init_spelling();
|
|
3752
|
+
}
|
|
3753
|
+
});
|
|
3754
|
+
|
|
3755
|
+
// src/server/lint/outputs.ts
|
|
3756
|
+
import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
|
|
3757
|
+
import { resolve as resolve8 } from "path";
|
|
3758
|
+
function checkOutputs(state, root) {
|
|
3759
|
+
const out = [];
|
|
3760
|
+
for (const output of state.config.outputs) {
|
|
3761
|
+
const result = getAdapter(output.adapter).export(state, output);
|
|
3762
|
+
for (const file of result.files) {
|
|
3763
|
+
const abs = resolve8(root, file.path);
|
|
3764
|
+
const current = existsSync9(abs) ? readFileSync10(abs, "utf8") : null;
|
|
3765
|
+
if (current === null) {
|
|
3766
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
3767
|
+
} else if (current !== file.contents) {
|
|
3768
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
return out;
|
|
3773
|
+
}
|
|
3774
|
+
var init_outputs = __esm({
|
|
3775
|
+
"src/server/lint/outputs.ts"() {
|
|
3776
|
+
"use strict";
|
|
3777
|
+
init_adapters();
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
|
|
3781
|
+
// src/server/import/detect.ts
|
|
3782
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
3783
|
+
import { join as join4 } from "path";
|
|
3784
|
+
function safeIsDir(p) {
|
|
3785
|
+
try {
|
|
3786
|
+
return statSync3(p).isDirectory();
|
|
3787
|
+
} catch {
|
|
3788
|
+
return false;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
function listDirs(dir) {
|
|
3792
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join4(dir, e)));
|
|
3793
|
+
}
|
|
3794
|
+
function fileCount(dir) {
|
|
3795
|
+
try {
|
|
3796
|
+
return readdirSync4(dir).length;
|
|
3797
|
+
} catch {
|
|
3798
|
+
return 0;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
function pickSource(locales, sizeOf) {
|
|
3802
|
+
if (locales.includes("en")) return "en";
|
|
3803
|
+
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
3804
|
+
}
|
|
3805
|
+
function detectLaravel(root) {
|
|
3806
|
+
const localeRoot = [join4(root, "resources", "lang"), join4(root, "lang")].find(safeIsDir);
|
|
3807
|
+
if (!localeRoot) return null;
|
|
3808
|
+
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
3809
|
+
if (locales.length === 0) return null;
|
|
3810
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
3811
|
+
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
3812
|
+
}
|
|
3813
|
+
function detectVue(root, forced = false) {
|
|
3814
|
+
for (const rel of VUE_DIR_CANDIDATES) {
|
|
3815
|
+
const localeRoot = join4(root, rel);
|
|
3816
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
3817
|
+
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
3818
|
+
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
3819
|
+
if (enough) {
|
|
3820
|
+
const sourceLocale = pickSource(locales, (loc) => {
|
|
3821
|
+
try {
|
|
3822
|
+
return statSync3(join4(localeRoot, `${loc}.json`)).size;
|
|
3823
|
+
} catch {
|
|
3824
|
+
return 0;
|
|
3825
|
+
}
|
|
3826
|
+
});
|
|
3827
|
+
return { format: "vue-i18n-json", localeRoot, locales, sourceLocale };
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
return null;
|
|
3831
|
+
}
|
|
3832
|
+
function detectArb(root) {
|
|
3833
|
+
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
3834
|
+
const localeRoot = join4(root, rel);
|
|
3835
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
3836
|
+
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
3837
|
+
if (locales.length >= 1) {
|
|
3838
|
+
return { format: "flutter-arb", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
return null;
|
|
3842
|
+
}
|
|
3843
|
+
function detect(root, formatOverride) {
|
|
3844
|
+
if (!existsSync10(root)) return null;
|
|
3845
|
+
if (formatOverride) {
|
|
3846
|
+
const fn = BY_FORMAT[formatOverride];
|
|
3847
|
+
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
3848
|
+
return fn(root);
|
|
3849
|
+
}
|
|
3850
|
+
for (const fn of DETECTORS) {
|
|
3851
|
+
const d = fn(root);
|
|
3852
|
+
if (d) return d;
|
|
3853
|
+
}
|
|
3854
|
+
return null;
|
|
3855
|
+
}
|
|
3856
|
+
var LOCALE_RE, VUE_DIR_CANDIDATES, DETECTORS, BY_FORMAT;
|
|
3857
|
+
var init_detect = __esm({
|
|
3858
|
+
"src/server/import/detect.ts"() {
|
|
3859
|
+
"use strict";
|
|
3860
|
+
LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
3861
|
+
VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
3862
|
+
DETECTORS = [detectLaravel, detectVue, detectArb];
|
|
3863
|
+
BY_FORMAT = {
|
|
3864
|
+
"laravel-php": detectLaravel,
|
|
3865
|
+
"vue-i18n-json": (root) => detectVue(root, true),
|
|
3866
|
+
"flutter-arb": detectArb
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
});
|
|
3870
|
+
|
|
3871
|
+
// src/server/import/flatten.ts
|
|
3872
|
+
function flattenObject(value, prefix, warnings) {
|
|
3873
|
+
const out = {};
|
|
3874
|
+
const walk = (node, path) => {
|
|
3875
|
+
if (typeof node === "string") {
|
|
3876
|
+
out[path] = node;
|
|
3877
|
+
} else if (typeof node === "number" || typeof node === "boolean") {
|
|
3878
|
+
out[path] = String(node);
|
|
3139
3879
|
} else if (Array.isArray(node)) {
|
|
3140
3880
|
node.forEach((el, i) => walk(el, path ? `${path}.${i}` : String(i)));
|
|
3141
3881
|
} else if (node && typeof node === "object") {
|
|
@@ -3156,7 +3896,7 @@ var init_flatten = __esm({
|
|
|
3156
3896
|
});
|
|
3157
3897
|
|
|
3158
3898
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
3159
|
-
import { readdirSync as
|
|
3899
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
|
|
3160
3900
|
import { join as join5 } from "path";
|
|
3161
3901
|
var LOCALE_RE2, vueI18nJson2;
|
|
3162
3902
|
var init_vue_i18n_json2 = __esm({
|
|
@@ -3170,7 +3910,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
3170
3910
|
const warnings = [];
|
|
3171
3911
|
const keys = {};
|
|
3172
3912
|
const locales = [];
|
|
3173
|
-
for (const file of
|
|
3913
|
+
for (const file of readdirSync5(localeRoot).sort()) {
|
|
3174
3914
|
if (!file.endsWith(".json")) continue;
|
|
3175
3915
|
const locale = file.slice(0, -".json".length);
|
|
3176
3916
|
if (!LOCALE_RE2.test(locale)) continue;
|
|
@@ -3204,18 +3944,18 @@ var init_placeholders2 = __esm({
|
|
|
3204
3944
|
});
|
|
3205
3945
|
|
|
3206
3946
|
// src/server/import/parsers/laravel-php.ts
|
|
3207
|
-
import { readdirSync as
|
|
3947
|
+
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
3208
3948
|
import { join as join6, relative as relative2 } from "path";
|
|
3209
3949
|
import { execFileSync } from "child_process";
|
|
3210
3950
|
function listDirs2(dir) {
|
|
3211
|
-
return
|
|
3951
|
+
return readdirSync6(dir).filter((e) => statSync4(join6(dir, e)).isDirectory());
|
|
3212
3952
|
}
|
|
3213
3953
|
function listPhpFiles(dir) {
|
|
3214
3954
|
const out = [];
|
|
3215
3955
|
const walk = (d) => {
|
|
3216
|
-
for (const e of
|
|
3956
|
+
for (const e of readdirSync6(d)) {
|
|
3217
3957
|
const full = join6(d, e);
|
|
3218
|
-
if (
|
|
3958
|
+
if (statSync4(full).isDirectory()) walk(full);
|
|
3219
3959
|
else if (e.endsWith(".php")) out.push(full);
|
|
3220
3960
|
}
|
|
3221
3961
|
};
|
|
@@ -3282,7 +4022,7 @@ var init_laravel_php2 = __esm({
|
|
|
3282
4022
|
});
|
|
3283
4023
|
|
|
3284
4024
|
// src/server/import/parsers/flutter-arb.ts
|
|
3285
|
-
import { readdirSync as
|
|
4025
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync12 } from "fs";
|
|
3286
4026
|
import { join as join7 } from "path";
|
|
3287
4027
|
function localeFromArbName(file) {
|
|
3288
4028
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -3316,7 +4056,7 @@ var init_flutter_arb2 = __esm({
|
|
|
3316
4056
|
const warnings = [];
|
|
3317
4057
|
const keys = {};
|
|
3318
4058
|
const locales = [];
|
|
3319
|
-
for (const file of
|
|
4059
|
+
for (const file of readdirSync7(localeRoot).sort()) {
|
|
3320
4060
|
if (!file.endsWith(".arb")) continue;
|
|
3321
4061
|
const locale = localeFromArbName(file);
|
|
3322
4062
|
if (!locale) continue;
|
|
@@ -3502,7 +4242,7 @@ function runImport(opts) {
|
|
|
3502
4242
|
localeCount: state.config.locales.length
|
|
3503
4243
|
};
|
|
3504
4244
|
}
|
|
3505
|
-
var
|
|
4245
|
+
var init_run3 = __esm({
|
|
3506
4246
|
"src/server/import/run.ts"() {
|
|
3507
4247
|
"use strict";
|
|
3508
4248
|
init_detect();
|
|
@@ -3888,11 +4628,11 @@ var init_ui_prefs = __esm({
|
|
|
3888
4628
|
// src/server/api.ts
|
|
3889
4629
|
import { Hono } from "hono";
|
|
3890
4630
|
import { streamSSE } from "hono/streaming";
|
|
3891
|
-
import { readFileSync as readFileSync14, existsSync as
|
|
3892
|
-
import { dirname as
|
|
4631
|
+
import { readFileSync as readFileSync14, existsSync as existsSync11, readdirSync as readdirSync8, statSync as statSync5, rmSync as rmSync4 } from "fs";
|
|
4632
|
+
import { dirname as dirname3, resolve as resolve9, basename, relative as relative3, sep as sep2 } from "path";
|
|
3893
4633
|
function projectName(root) {
|
|
3894
4634
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
3895
|
-
if (
|
|
4635
|
+
if (existsSync11(nameFile)) {
|
|
3896
4636
|
try {
|
|
3897
4637
|
const name = readFileSync14(nameFile, "utf8").trim();
|
|
3898
4638
|
if (name) return name;
|
|
@@ -3904,7 +4644,7 @@ function projectName(root) {
|
|
|
3904
4644
|
function createApi(deps) {
|
|
3905
4645
|
const app = new Hono();
|
|
3906
4646
|
const load = () => loadState(deps.statePath);
|
|
3907
|
-
const projectRoot =
|
|
4647
|
+
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
3908
4648
|
let translateQueue = Promise.resolve();
|
|
3909
4649
|
const withTranslateLock = (fn) => {
|
|
3910
4650
|
const next = translateQueue.then(fn, fn);
|
|
@@ -4002,13 +4742,13 @@ function createApi(deps) {
|
|
|
4002
4742
|
found.set(deps.statePath, {
|
|
4003
4743
|
name: basename(deps.statePath),
|
|
4004
4744
|
path: deps.statePath,
|
|
4005
|
-
relDir: activeRel !== basename(activeRel) ?
|
|
4745
|
+
relDir: activeRel !== basename(activeRel) ? dirname3(activeRel) : void 0
|
|
4006
4746
|
});
|
|
4007
4747
|
function walk(dir, depth) {
|
|
4008
4748
|
if (depth > 4) return;
|
|
4009
4749
|
let entries = [];
|
|
4010
4750
|
try {
|
|
4011
|
-
entries =
|
|
4751
|
+
entries = readdirSync8(dir);
|
|
4012
4752
|
} catch {
|
|
4013
4753
|
return;
|
|
4014
4754
|
}
|
|
@@ -4016,13 +4756,13 @@ function createApi(deps) {
|
|
|
4016
4756
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4017
4757
|
const abs = resolve9(dir, name);
|
|
4018
4758
|
let filePath = null;
|
|
4019
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
4759
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync11(resolve9(abs, "config.json"))) {
|
|
4020
4760
|
filePath = resolve9(dir, `${name}.json`);
|
|
4021
4761
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
4022
4762
|
filePath = abs;
|
|
4023
4763
|
} else {
|
|
4024
4764
|
try {
|
|
4025
|
-
if (
|
|
4765
|
+
if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
|
|
4026
4766
|
} catch {
|
|
4027
4767
|
}
|
|
4028
4768
|
continue;
|
|
@@ -4031,7 +4771,7 @@ function createApi(deps) {
|
|
|
4031
4771
|
try {
|
|
4032
4772
|
loadState(filePath);
|
|
4033
4773
|
const rel = relative3(projectRoot, filePath);
|
|
4034
|
-
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ?
|
|
4774
|
+
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
|
|
4035
4775
|
} catch {
|
|
4036
4776
|
}
|
|
4037
4777
|
}
|
|
@@ -4048,9 +4788,9 @@ function createApi(deps) {
|
|
|
4048
4788
|
const { path } = await c.req.json();
|
|
4049
4789
|
if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
|
|
4050
4790
|
const resolved = resolve9(projectRoot, path);
|
|
4051
|
-
const inside = resolved === projectRoot || resolved.startsWith(projectRoot +
|
|
4791
|
+
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
4052
4792
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
4053
|
-
if (!
|
|
4793
|
+
if (!existsSync11(resolved)) return c.json({ error: "file not found" }, 400);
|
|
4054
4794
|
loadState(resolved);
|
|
4055
4795
|
deps.statePath = resolved;
|
|
4056
4796
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -4107,11 +4847,11 @@ function createApi(deps) {
|
|
|
4107
4847
|
function removeOrphanScreenshot(s, screenshot) {
|
|
4108
4848
|
if (!screenshot) return;
|
|
4109
4849
|
for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
|
|
4110
|
-
const root =
|
|
4850
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4111
4851
|
const abs = resolve9(root, screenshot);
|
|
4112
4852
|
const rel = relative3(root, abs);
|
|
4113
|
-
const seg0 = rel.split(
|
|
4114
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
4853
|
+
const seg0 = rel.split(sep2)[0] ?? "";
|
|
4854
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
|
|
4115
4855
|
try {
|
|
4116
4856
|
rmSync4(abs);
|
|
4117
4857
|
} catch {
|
|
@@ -4363,7 +5103,7 @@ function createApi(deps) {
|
|
|
4363
5103
|
const body = await c.req.parseBody();
|
|
4364
5104
|
const file = body["file"];
|
|
4365
5105
|
if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
|
|
4366
|
-
const root =
|
|
5106
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4367
5107
|
const dirName = screenshotDirName(deps.statePath);
|
|
4368
5108
|
const dir = resolve9(root, dirName);
|
|
4369
5109
|
const filename = `${sanitize(key)}__${sanitize(file.name)}`;
|
|
@@ -4399,6 +5139,23 @@ function createApi(deps) {
|
|
|
4399
5139
|
return c.json({ files, warnings });
|
|
4400
5140
|
});
|
|
4401
5141
|
app.get("/scan/missing", (c) => c.json(findMissing(load())));
|
|
5142
|
+
const spellerCache = /* @__PURE__ */ new Map();
|
|
5143
|
+
const cachedLoader = (dictId) => {
|
|
5144
|
+
let p = spellerCache.get(dictId);
|
|
5145
|
+
if (!p) {
|
|
5146
|
+
p = defaultLoader(dictId);
|
|
5147
|
+
spellerCache.set(dictId, p);
|
|
5148
|
+
}
|
|
5149
|
+
return p;
|
|
5150
|
+
};
|
|
5151
|
+
app.get("/lint", async (c) => {
|
|
5152
|
+
const state = load();
|
|
5153
|
+
const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
|
|
5154
|
+
} });
|
|
5155
|
+
const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
|
|
5156
|
+
const counts = countSeverities(findings);
|
|
5157
|
+
return c.json({ findings, counts, ok: counts.error === 0 });
|
|
5158
|
+
});
|
|
4402
5159
|
app.get("/checks", (c) => {
|
|
4403
5160
|
const param = c.req.query("checks");
|
|
4404
5161
|
const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
|
|
@@ -4434,22 +5191,12 @@ function createApi(deps) {
|
|
|
4434
5191
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
4435
5192
|
});
|
|
4436
5193
|
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 });
|
|
5194
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
5195
|
+
const { written, skipped, deleted, warnings } = exportToDisk(load(), root);
|
|
5196
|
+
console.log(
|
|
5197
|
+
`[export] ${written + skipped} file(s)${deleted ? `, ${deleted} removed` : ""}${warnings.length ? `, ${warnings.length} warning(s)` : ""}`
|
|
5198
|
+
);
|
|
5199
|
+
return c.json({ files: written + skipped, warnings });
|
|
4453
5200
|
});
|
|
4454
5201
|
app.post("/translate/stream", async (c) => {
|
|
4455
5202
|
const signal = c.req.raw.signal;
|
|
@@ -4471,7 +5218,7 @@ function createApi(deps) {
|
|
|
4471
5218
|
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4472
5219
|
return;
|
|
4473
5220
|
}
|
|
4474
|
-
const { skipped } = attachScreenshotsForProvider(reqs, s,
|
|
5221
|
+
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4475
5222
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4476
5223
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4477
5224
|
let totalWritten = 0;
|
|
@@ -4549,7 +5296,7 @@ function createApi(deps) {
|
|
|
4549
5296
|
} catch (e) {
|
|
4550
5297
|
return c.json({ error: e.message }, 400);
|
|
4551
5298
|
}
|
|
4552
|
-
const { skipped } = attachScreenshotsForProvider(toTranslate, s,
|
|
5299
|
+
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4553
5300
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4554
5301
|
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4555
5302
|
const latest = load();
|
|
@@ -4577,6 +5324,13 @@ function createApi(deps) {
|
|
|
4577
5324
|
}
|
|
4578
5325
|
return c.json({ requested: reqs.length, written, errors });
|
|
4579
5326
|
}));
|
|
5327
|
+
app.post("/translate/estimate", async (c) => {
|
|
5328
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5329
|
+
const keys = Array.isArray(body.keys) && body.keys.length ? body.keys.filter(Boolean) : void 0;
|
|
5330
|
+
const locales = Array.isArray(body.locales) && body.locales.length ? body.locales.filter(Boolean) : void 0;
|
|
5331
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
5332
|
+
return c.json(estimateTranslation(load(), ai, { onlyMissing: body.onlyMissing ?? true, keys, locales }));
|
|
5333
|
+
});
|
|
4580
5334
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
4581
5335
|
app.post("/scan", async (c) => {
|
|
4582
5336
|
const s = load();
|
|
@@ -4736,13 +5490,17 @@ var init_api = __esm({
|
|
|
4736
5490
|
init_context();
|
|
4737
5491
|
init_stats();
|
|
4738
5492
|
init_checks();
|
|
5493
|
+
init_run2();
|
|
5494
|
+
init_outputs();
|
|
5495
|
+
init_spelling();
|
|
4739
5496
|
init_adapters();
|
|
4740
5497
|
init_ai();
|
|
4741
5498
|
init_run();
|
|
4742
5499
|
init_provider();
|
|
5500
|
+
init_estimate();
|
|
4743
5501
|
init_log();
|
|
4744
5502
|
init_schema();
|
|
4745
|
-
|
|
5503
|
+
init_run3();
|
|
4746
5504
|
init_export_run();
|
|
4747
5505
|
init_ui_prefs();
|
|
4748
5506
|
init_local_settings();
|
|
@@ -4761,7 +5519,7 @@ __export(server_exports, {
|
|
|
4761
5519
|
import { Hono as Hono2 } from "hono";
|
|
4762
5520
|
import { serve } from "@hono/node-server";
|
|
4763
5521
|
import { fileURLToPath } from "url";
|
|
4764
|
-
import { dirname as
|
|
5522
|
+
import { dirname as dirname4, join as join9, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
4765
5523
|
import { readFile, stat } from "fs/promises";
|
|
4766
5524
|
import { createServer } from "net";
|
|
4767
5525
|
import open from "open";
|
|
@@ -4783,11 +5541,11 @@ function buildApp(opts) {
|
|
|
4783
5541
|
app.get("/:dir/*", async (c, next) => {
|
|
4784
5542
|
const dirSeg = c.req.param("dir");
|
|
4785
5543
|
if (!dirSeg.endsWith("-screenshots")) return next();
|
|
4786
|
-
const shotsRoot = resolve10(
|
|
5544
|
+
const shotsRoot = resolve10(dirname4(resolve10(apiDeps.statePath)), dirSeg);
|
|
4787
5545
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4788
5546
|
const rest = pathname.slice(`/${dirSeg}`.length);
|
|
4789
5547
|
const target = resolve10(shotsRoot, "." + rest);
|
|
4790
|
-
const inside = target === shotsRoot || target.startsWith(shotsRoot +
|
|
5548
|
+
const inside = target === shotsRoot || target.startsWith(shotsRoot + sep3);
|
|
4791
5549
|
if (inside) {
|
|
4792
5550
|
const file = await readFileResponse(target);
|
|
4793
5551
|
if (file) return file;
|
|
@@ -4799,7 +5557,7 @@ function buildApp(opts) {
|
|
|
4799
5557
|
app.get("/*", async (c) => {
|
|
4800
5558
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4801
5559
|
const target = resolve10(root, "." + pathname);
|
|
4802
|
-
const inside = target === root || target.startsWith(root +
|
|
5560
|
+
const inside = target === root || target.startsWith(root + sep3);
|
|
4803
5561
|
if (inside && pathname !== "/") {
|
|
4804
5562
|
const file = await readFileResponse(target);
|
|
4805
5563
|
if (file) return file;
|
|
@@ -4841,7 +5599,7 @@ async function startServer(opts) {
|
|
|
4841
5599
|
});
|
|
4842
5600
|
}
|
|
4843
5601
|
function backgroundScan(statePath) {
|
|
4844
|
-
const projectRoot =
|
|
5602
|
+
const projectRoot = dirname4(resolve10(statePath));
|
|
4845
5603
|
Promise.resolve().then(() => {
|
|
4846
5604
|
const state = loadState(statePath);
|
|
4847
5605
|
const existing = loadUsageCache(projectRoot);
|
|
@@ -4861,7 +5619,7 @@ var init_server = __esm({
|
|
|
4861
5619
|
init_state();
|
|
4862
5620
|
init_scan();
|
|
4863
5621
|
init_scanner();
|
|
4864
|
-
here =
|
|
5622
|
+
here = dirname4(fileURLToPath(import.meta.url));
|
|
4865
5623
|
DEFAULT_UI_DIR = join9(here, "..", "ui");
|
|
4866
5624
|
MIME = {
|
|
4867
5625
|
".html": "text/html; charset=utf-8",
|
|
@@ -4899,331 +5657,15 @@ init_ai();
|
|
|
4899
5657
|
init_local_settings();
|
|
4900
5658
|
init_run();
|
|
4901
5659
|
init_provider();
|
|
5660
|
+
init_estimate();
|
|
4902
5661
|
init_log();
|
|
4903
5662
|
init_scan();
|
|
4904
5663
|
init_scanner();
|
|
4905
5664
|
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
|
-
}
|
|
5665
|
+
init_run2();
|
|
5666
|
+
init_outputs();
|
|
5667
|
+
import { resolve as resolve11, dirname as dirname5 } from "path";
|
|
5668
|
+
import { readFileSync as readFileSync15, existsSync as existsSync12 } from "fs";
|
|
5227
5669
|
|
|
5228
5670
|
// src/server/lint/locate.ts
|
|
5229
5671
|
function locate(rawText, key) {
|
|
@@ -5353,6 +5795,7 @@ function parseArgs(argv) {
|
|
|
5353
5795
|
} else if (flag === "--empty-source") args.emptySource = true;
|
|
5354
5796
|
else if (flag === "--unused") args.unused = true;
|
|
5355
5797
|
else if (flag === "--write") args.write = true;
|
|
5798
|
+
else if (flag === "--estimate") args.estimate = true;
|
|
5356
5799
|
}
|
|
5357
5800
|
return args;
|
|
5358
5801
|
}
|
|
@@ -5366,7 +5809,7 @@ function watchTargetFor(statePath) {
|
|
|
5366
5809
|
return detectFormat(statePath) === "split" ? { path: splitDirFor(statePath), recursive: true } : { path: statePath, recursive: false };
|
|
5367
5810
|
}
|
|
5368
5811
|
async function runExport(args) {
|
|
5369
|
-
const root =
|
|
5812
|
+
const root = dirname5(resolve11(args.statePath));
|
|
5370
5813
|
const runOnce = () => {
|
|
5371
5814
|
const state = loadState(args.statePath);
|
|
5372
5815
|
const result = exportToDisk(state, root, args.adapter ? { adapter: args.adapter } : void 0);
|
|
@@ -5377,8 +5820,9 @@ async function runExport(args) {
|
|
|
5377
5820
|
return result;
|
|
5378
5821
|
};
|
|
5379
5822
|
if (!args.watch) {
|
|
5380
|
-
const { written, skipped } = runOnce();
|
|
5381
|
-
|
|
5823
|
+
const { written, skipped, deleted } = runOnce();
|
|
5824
|
+
const extras = [skipped ? `${skipped} unchanged` : "", deleted ? `${deleted} stale removed` : ""].filter(Boolean);
|
|
5825
|
+
console.log(`Exported ${written} file(s)${extras.length ? ` (${extras.join(", ")})` : ""}.`);
|
|
5382
5826
|
return;
|
|
5383
5827
|
}
|
|
5384
5828
|
const { watch } = await import("fs");
|
|
@@ -5390,8 +5834,10 @@ async function runExport(args) {
|
|
|
5390
5834
|
clearTimeout(timer);
|
|
5391
5835
|
timer = setTimeout(() => {
|
|
5392
5836
|
try {
|
|
5393
|
-
const { written } = runOnce();
|
|
5394
|
-
if (written)
|
|
5837
|
+
const { written, deleted } = runOnce();
|
|
5838
|
+
if (written || deleted) {
|
|
5839
|
+
console.log(`Re-exported ${written} file(s)${deleted ? ` (${deleted} stale removed)` : ""}.`);
|
|
5840
|
+
}
|
|
5395
5841
|
} catch (e) {
|
|
5396
5842
|
console.error(e.message);
|
|
5397
5843
|
}
|
|
@@ -5402,7 +5848,32 @@ async function runExport(args) {
|
|
|
5402
5848
|
}
|
|
5403
5849
|
async function runTranslate(args) {
|
|
5404
5850
|
const state = loadState(args.statePath);
|
|
5405
|
-
const projectRoot =
|
|
5851
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5852
|
+
if (args.estimate) {
|
|
5853
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
5854
|
+
const est = estimateTranslation(state, ai, {
|
|
5855
|
+
onlyMissing: args.all ? false : args.onlyMissing ?? true,
|
|
5856
|
+
locales: args.locales,
|
|
5857
|
+
keyGlob: args.keyGlob
|
|
5858
|
+
});
|
|
5859
|
+
if (!est.requests) {
|
|
5860
|
+
console.log("Nothing to translate.");
|
|
5861
|
+
return;
|
|
5862
|
+
}
|
|
5863
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
5864
|
+
console.log(`Estimate for ${fmt(est.requests)} request(s) in ${fmt(est.batches)} batch(es) \u2014 ${ai.provider} \xB7 ${ai.model}`);
|
|
5865
|
+
for (const l of est.perLocale) {
|
|
5866
|
+
console.log(` ${l.locale.padEnd(8)} ${fmt(l.requests).padStart(7)} req ${fmt(l.batches).padStart(5)} batch(es) ~${fmt(l.inputTokens)} in / ~${fmt(l.outputTokens)} out tokens`);
|
|
5867
|
+
}
|
|
5868
|
+
console.log(`Totals: ~${fmt(est.inputTokens)} input / ~${fmt(est.outputTokens)} output tokens`);
|
|
5869
|
+
if (est.pricing) {
|
|
5870
|
+
const cost = est.estimatedCost;
|
|
5871
|
+
console.log(`Estimated cost: ~$${cost >= 0.1 ? cost.toFixed(2) : cost.toFixed(4)} (\xB120%, ${est.pricing.source} pricing $${est.pricing.inputPerMTok}/$${est.pricing.outputPerMTok} per MTok)`);
|
|
5872
|
+
} else {
|
|
5873
|
+
console.log("No pricing known for this model \u2014 set inputPricePerMTok/outputPricePerMTok in your AI settings for a dollar estimate.");
|
|
5874
|
+
}
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5406
5877
|
const reqs = selectRequests(state, {
|
|
5407
5878
|
// Default to translating only empty values; --all forces a full re-translate
|
|
5408
5879
|
// (overwriting existing translations). --only missing stays as a no-op alias.
|
|
@@ -5472,7 +5943,7 @@ function printReport(report, format, rawText) {
|
|
|
5472
5943
|
}
|
|
5473
5944
|
async function runLintCmd(args) {
|
|
5474
5945
|
const state = loadState(args.statePath);
|
|
5475
|
-
const rawText =
|
|
5946
|
+
const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5476
5947
|
const report = await runLint(state, { locales: args.locales, ruleIds: args.ruleIds });
|
|
5477
5948
|
printReport(report, args.format, rawText);
|
|
5478
5949
|
const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
|
|
@@ -5492,8 +5963,8 @@ async function runCheck(args) {
|
|
|
5492
5963
|
process.exitCode = 1;
|
|
5493
5964
|
return;
|
|
5494
5965
|
}
|
|
5495
|
-
const rawText =
|
|
5496
|
-
const root =
|
|
5966
|
+
const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
|
|
5967
|
+
const root = dirname5(resolve11(args.statePath));
|
|
5497
5968
|
const lint = await runLint(state, {});
|
|
5498
5969
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
5499
5970
|
const counts = countSeverities(findings);
|
|
@@ -5502,10 +5973,10 @@ async function runCheck(args) {
|
|
|
5502
5973
|
if (!report.ok) process.exitCode = 1;
|
|
5503
5974
|
}
|
|
5504
5975
|
async function runImportCmd(args) {
|
|
5505
|
-
const { runImport: runImport2 } = await Promise.resolve().then(() => (
|
|
5506
|
-
const projectRoot = args.importSource ? resolve11(args.importSource) :
|
|
5976
|
+
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
5977
|
+
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
|
|
5507
5978
|
const out = resolve11(projectRoot, "glotfile.json");
|
|
5508
|
-
if (
|
|
5979
|
+
if (existsSync12(out) && !args.importForce) {
|
|
5509
5980
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
5510
5981
|
process.exitCode = 1;
|
|
5511
5982
|
return;
|
|
@@ -5530,7 +6001,7 @@ async function runImportCmd(args) {
|
|
|
5530
6001
|
}
|
|
5531
6002
|
async function runBuildContext(args) {
|
|
5532
6003
|
const state = loadState(args.statePath);
|
|
5533
|
-
const projectRoot =
|
|
6004
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5534
6005
|
const cache2 = loadUsageCache(projectRoot);
|
|
5535
6006
|
if (!cache2) {
|
|
5536
6007
|
console.error("No usage index found. Run 'glotfile scan' first.");
|
|
@@ -5601,7 +6072,7 @@ async function runBuildContext(args) {
|
|
|
5601
6072
|
}
|
|
5602
6073
|
async function runScanCmd(args) {
|
|
5603
6074
|
const state = loadState(args.statePath);
|
|
5604
|
-
const projectRoot =
|
|
6075
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5605
6076
|
const existing = loadUsageCache(projectRoot);
|
|
5606
6077
|
const result = runScan(projectRoot, state.config.scan ?? {}, existing);
|
|
5607
6078
|
const fileCount2 = Object.keys(result.files).length;
|
|
@@ -5620,7 +6091,7 @@ async function runPrune(args) {
|
|
|
5620
6091
|
for (const k of findEmptySourceKeys(state)) toRemove.add(k);
|
|
5621
6092
|
}
|
|
5622
6093
|
if (args.unused) {
|
|
5623
|
-
const projectRoot =
|
|
6094
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
5624
6095
|
const cache2 = runScan(projectRoot, state.config.scan ?? {}, loadUsageCache(projectRoot));
|
|
5625
6096
|
const used = new Set(computeUsedKeys(state, cache2));
|
|
5626
6097
|
for (const k of Object.keys(state.keys)) {
|
|
@@ -5673,9 +6144,10 @@ var COMMAND_HELP = {
|
|
|
5673
6144
|
},
|
|
5674
6145
|
translate: {
|
|
5675
6146
|
summary: "AI-translate missing strings into your target locales (writes back to the state file).",
|
|
5676
|
-
usage: "glotfile translate [--all] [--locale <list>] [--key <glob>]",
|
|
6147
|
+
usage: "glotfile translate [--all] [--estimate] [--locale <list>] [--key <glob>]",
|
|
5677
6148
|
options: [
|
|
5678
6149
|
["--all", "Re-translate every string, not just empty values"],
|
|
6150
|
+
["--estimate", "Print batches, tokens and estimated cost without translating"],
|
|
5679
6151
|
["--locale <list>", "Comma-separated target locales (alias: --locales)"],
|
|
5680
6152
|
["--key <glob>", "Only keys matching this glob"]
|
|
5681
6153
|
]
|