@verbaly/compiler 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +323 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +38 -3
- package/dist/index.js +289 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { glob } from "tinyglobby";
|
|
|
8
8
|
import { parse as parse$1 } from "@babel/parser";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import MagicString from "magic-string";
|
|
11
|
+
import "picomatch";
|
|
11
12
|
//#region src/catalog.ts
|
|
12
13
|
function catalogPath(cfg, locale) {
|
|
13
14
|
return join(cfg.dir, `${locale}.json`);
|
|
@@ -98,6 +99,57 @@ function formatCheckResult(result) {
|
|
|
98
99
|
function truncate(text, max) {
|
|
99
100
|
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
100
101
|
}
|
|
102
|
+
function githubCheckAnnotations(result, registry, root) {
|
|
103
|
+
const messages = registry.messages();
|
|
104
|
+
const contents = /* @__PURE__ */ new Map();
|
|
105
|
+
const readSource = (file) => {
|
|
106
|
+
if (!contents.has(file)) try {
|
|
107
|
+
contents.set(file, readFileSync(file, "utf8"));
|
|
108
|
+
} catch {
|
|
109
|
+
contents.set(file, void 0);
|
|
110
|
+
}
|
|
111
|
+
return contents.get(file);
|
|
112
|
+
};
|
|
113
|
+
const lines = [];
|
|
114
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const entry of result.missing) {
|
|
116
|
+
const grouped = byKey.get(entry.key);
|
|
117
|
+
if (grouped) grouped.locales.push(entry.locale);
|
|
118
|
+
else byKey.set(entry.key, {
|
|
119
|
+
...entry,
|
|
120
|
+
locales: [entry.locale]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (const entry of byKey.values()) {
|
|
124
|
+
const origin = messages.get(entry.key);
|
|
125
|
+
const hint = entry.source ? `: "${truncate(entry.source, 60)}"` : "";
|
|
126
|
+
const text = escapeData(`missing [${entry.locales.join(", ")}] ${entry.key}${hint}`);
|
|
127
|
+
if (origin) {
|
|
128
|
+
const file = relative(root, origin.file).replaceAll("\\", "/");
|
|
129
|
+
const content = readSource(origin.file);
|
|
130
|
+
const line = content === void 0 ? void 0 : lineAt(content, origin.start);
|
|
131
|
+
lines.push(`::error file=${escapeProperty(file)}${line ? `,line=${line}` : ""}::${text}`);
|
|
132
|
+
} else lines.push(`::error::${text}`);
|
|
133
|
+
}
|
|
134
|
+
for (const entry of result.unknown) {
|
|
135
|
+
const text = escapeData(`unknown key "${entry.key}" (not in any catalog)`);
|
|
136
|
+
const file = entry.files[0];
|
|
137
|
+
lines.push(file ? `::error file=${escapeProperty(relative(root, file).replaceAll("\\", "/"))}::${text}` : `::error::${text}`);
|
|
138
|
+
}
|
|
139
|
+
return lines;
|
|
140
|
+
}
|
|
141
|
+
function lineAt(content, offset) {
|
|
142
|
+
let line = 1;
|
|
143
|
+
const end = Math.min(offset, content.length);
|
|
144
|
+
for (let i = 0; i < end; i++) if (content[i] === "\n") line += 1;
|
|
145
|
+
return line;
|
|
146
|
+
}
|
|
147
|
+
function escapeData(text) {
|
|
148
|
+
return text.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
149
|
+
}
|
|
150
|
+
function escapeProperty(text) {
|
|
151
|
+
return escapeData(text).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
152
|
+
}
|
|
101
153
|
//#endregion
|
|
102
154
|
//#region src/params.ts
|
|
103
155
|
const PLURAL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -199,12 +251,13 @@ declare module 'virtual:verbaly/locale/*' {
|
|
|
199
251
|
}
|
|
200
252
|
`;
|
|
201
253
|
}
|
|
202
|
-
function writeDts(cfg, catalog) {
|
|
203
|
-
|
|
254
|
+
function writeDts(cfg, catalog, file) {
|
|
255
|
+
file ??= join(cfg.root, "verbaly.d.ts");
|
|
204
256
|
const content = generateDts(catalog);
|
|
205
257
|
try {
|
|
206
258
|
if (readFileSync(file, "utf8") === content) return;
|
|
207
259
|
} catch {}
|
|
260
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
208
261
|
writeFileSync(file, content);
|
|
209
262
|
}
|
|
210
263
|
//#endregion
|
|
@@ -341,8 +394,9 @@ function resolveConfig(config = {}) {
|
|
|
341
394
|
dir,
|
|
342
395
|
sourceLocale,
|
|
343
396
|
locales: [...locales],
|
|
344
|
-
include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
|
|
397
|
+
include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue,astro}"],
|
|
345
398
|
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
|
|
399
|
+
dts: typeof config.dts === "string" ? resolve(root, config.dts) : config.dts,
|
|
346
400
|
translate: config.translate ?? {},
|
|
347
401
|
render: config.render ?? {}
|
|
348
402
|
};
|
|
@@ -705,13 +759,14 @@ function isNode(value) {
|
|
|
705
759
|
}
|
|
706
760
|
//#endregion
|
|
707
761
|
//#region src/sfc.ts
|
|
708
|
-
const SFC_FILE_RE = /\.(?:svelte|vue)$/;
|
|
762
|
+
const SFC_FILE_RE = /\.(?:svelte|vue|astro)$/;
|
|
709
763
|
function analyzeFile(code, file) {
|
|
710
764
|
return SFC_FILE_RE.test(file) ? analyzeSfc(code, file) : analyze(code, file);
|
|
711
765
|
}
|
|
712
766
|
const SCRIPT_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
|
|
713
767
|
const STYLE_RE = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
|
|
714
768
|
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
769
|
+
const FRONTMATTER_RE = /^(\uFEFF?\s*---\r?\n)([\s\S]*?)\r?\n---(?=\r?\n|$)/;
|
|
715
770
|
const CANDIDATE_SVELTE = /(?<![\w$])\$?t(?=[`(]|\.id\()/g;
|
|
716
771
|
const CANDIDATE_VUE = /(?<![\w$])t(?=[`(]|\.id\()/g;
|
|
717
772
|
function analyzeSfc(code, file) {
|
|
@@ -719,12 +774,14 @@ function analyzeSfc(code, file) {
|
|
|
719
774
|
const options = svelte ? { tNames: ["t", "$t"] } : void 0;
|
|
720
775
|
const tagged = [];
|
|
721
776
|
const usedKeys = [];
|
|
777
|
+
const frontmatter = file.endsWith(".astro") ? FRONTMATTER_RE.exec(code) : null;
|
|
778
|
+
if (frontmatter?.[2]) merge(analyzeSegment(frontmatter[2], file, options), frontmatter[1].length, false);
|
|
722
779
|
for (const match of code.matchAll(SCRIPT_RE)) {
|
|
723
780
|
const content = match[2];
|
|
724
781
|
if (!content) continue;
|
|
725
782
|
merge(analyzeSegment(content, file, options), match.index + match[1].length, false);
|
|
726
783
|
}
|
|
727
|
-
const markup = blank(blank(blank(code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
|
|
784
|
+
const markup = blank(blank(blank(frontmatter ? " ".repeat(frontmatter[0].length) + code.slice(frontmatter[0].length) : code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
|
|
728
785
|
const candidates = svelte ? CANDIDATE_SVELTE : CANDIDATE_VUE;
|
|
729
786
|
let consumedTo = 0;
|
|
730
787
|
for (const match of markup.matchAll(candidates)) {
|
|
@@ -854,6 +911,7 @@ function pruneCatalogs(cfg, catalogs, registry) {
|
|
|
854
911
|
//#endregion
|
|
855
912
|
//#region src/init.ts
|
|
856
913
|
const BUNDLERS = [
|
|
914
|
+
"astro",
|
|
857
915
|
"vite",
|
|
858
916
|
"webpack",
|
|
859
917
|
"rollup",
|
|
@@ -907,7 +965,8 @@ function init(options = {}) {
|
|
|
907
965
|
}
|
|
908
966
|
const bundler = detectBundler(root);
|
|
909
967
|
const next = [];
|
|
910
|
-
if (bundler === "
|
|
968
|
+
if (bundler === "astro") next.push("install the integration: pnpm add -D @verbaly/astro", "add verbaly() to the integrations in astro.config");
|
|
969
|
+
else if (bundler === "vite") next.push("install the plugin: pnpm add -D @verbaly/vite", "add verbaly() to the plugins in vite.config");
|
|
911
970
|
else if (bundler) next.push("install the plugin: pnpm add -D @verbaly/unplugin", `add the verbaly ${bundler} plugin to your build config`);
|
|
912
971
|
else next.push("run \"verbaly extract\" after writing your first t`…` message");
|
|
913
972
|
return {
|
|
@@ -980,8 +1039,8 @@ async function doctor(cfg) {
|
|
|
980
1039
|
else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
|
|
981
1040
|
else if (bundler === "vite") warn("plugin", "vite detected but @verbaly/vite is not installed", "pnpm add -D @verbaly/vite and add verbaly() to the plugins in vite.config");
|
|
982
1041
|
else warn("plugin", `${bundler} detected but @verbaly/unplugin is not installed`, `pnpm add -D @verbaly/unplugin and add the verbaly ${bundler} plugin to your build config`);
|
|
983
|
-
if (source) {
|
|
984
|
-
const dtsPath = join(cfg.root, "verbaly.d.ts");
|
|
1042
|
+
if (source && cfg.dts !== false) {
|
|
1043
|
+
const dtsPath = cfg.dts ?? join(cfg.root, "verbaly.d.ts");
|
|
985
1044
|
if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
|
|
986
1045
|
else if (readFileSync(dtsPath, "utf8") !== generateDts(source)) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
|
|
987
1046
|
else ok("types", "verbaly.d.ts is up to date");
|
|
@@ -1644,7 +1703,7 @@ function formatStatusResult(result) {
|
|
|
1644
1703
|
}
|
|
1645
1704
|
return lines.join("\n");
|
|
1646
1705
|
}
|
|
1647
|
-
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|svelte|vue)$/;
|
|
1706
|
+
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|svelte|vue|astro)$/;
|
|
1648
1707
|
//#endregion
|
|
1649
1708
|
//#region src/watch.ts
|
|
1650
1709
|
function watchProject(cfg, run, options = {}) {
|
|
@@ -1687,12 +1746,225 @@ function watchProject(cfg, run, options = {}) {
|
|
|
1687
1746
|
};
|
|
1688
1747
|
}
|
|
1689
1748
|
//#endregion
|
|
1749
|
+
//#region src/wrap.ts
|
|
1750
|
+
const WRAP_ATTRS = /* @__PURE__ */ new Set([
|
|
1751
|
+
"title",
|
|
1752
|
+
"alt",
|
|
1753
|
+
"placeholder",
|
|
1754
|
+
"aria-label"
|
|
1755
|
+
]);
|
|
1756
|
+
const T_NAMES = /* @__PURE__ */ new Set(["t"]);
|
|
1757
|
+
const LETTER = /\p{L}/u;
|
|
1758
|
+
async function wrapProject(cfg, options = {}) {
|
|
1759
|
+
const files = (await glob(cfg.include, {
|
|
1760
|
+
cwd: cfg.root,
|
|
1761
|
+
ignore: cfg.exclude,
|
|
1762
|
+
absolute: true
|
|
1763
|
+
})).filter((file) => /\.[jt]sx$/.test(file));
|
|
1764
|
+
const result = {
|
|
1765
|
+
files: files.length,
|
|
1766
|
+
changed: [],
|
|
1767
|
+
wrapped: [],
|
|
1768
|
+
skipped: []
|
|
1769
|
+
};
|
|
1770
|
+
for (const file of files) {
|
|
1771
|
+
const code = readFileSync(file, "utf8");
|
|
1772
|
+
const label = relative(cfg.root, file).replaceAll("\\", "/");
|
|
1773
|
+
const out = wrapCode(code, label);
|
|
1774
|
+
result.wrapped.push(...out.wrapped);
|
|
1775
|
+
result.skipped.push(...out.skipped);
|
|
1776
|
+
if (out.code !== void 0) {
|
|
1777
|
+
result.changed.push(label);
|
|
1778
|
+
if (options.write) writeFileSync(file, out.code);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return result;
|
|
1782
|
+
}
|
|
1783
|
+
function wrapCode(code, file) {
|
|
1784
|
+
const wrapped = [];
|
|
1785
|
+
const skipped = [];
|
|
1786
|
+
let ast;
|
|
1787
|
+
try {
|
|
1788
|
+
ast = parse$1(code, {
|
|
1789
|
+
sourceType: "module",
|
|
1790
|
+
errorRecovery: true,
|
|
1791
|
+
plugins: ["typescript", "jsx"]
|
|
1792
|
+
}).program;
|
|
1793
|
+
} catch {
|
|
1794
|
+
return {
|
|
1795
|
+
wrapped,
|
|
1796
|
+
skipped
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const s = new MagicString(code);
|
|
1800
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1801
|
+
let changed = false;
|
|
1802
|
+
walk(ast, (node) => {
|
|
1803
|
+
if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
|
|
1804
|
+
if (visited.has(node)) return;
|
|
1805
|
+
processElement(node);
|
|
1806
|
+
});
|
|
1807
|
+
return changed ? {
|
|
1808
|
+
code: s.toString(),
|
|
1809
|
+
wrapped,
|
|
1810
|
+
skipped
|
|
1811
|
+
} : {
|
|
1812
|
+
wrapped,
|
|
1813
|
+
skipped
|
|
1814
|
+
};
|
|
1815
|
+
function processElement(node) {
|
|
1816
|
+
visited.add(node);
|
|
1817
|
+
if (node.type === "JSXElement") {
|
|
1818
|
+
const opening = node.openingElement;
|
|
1819
|
+
const name = opening.name;
|
|
1820
|
+
if (name.type === "JSXIdentifier" && name.name === "Trans") return markSubtree(node);
|
|
1821
|
+
if (hasVerbalyAttr(opening)) return markSubtree(node);
|
|
1822
|
+
wrapAttributes(opening);
|
|
1823
|
+
}
|
|
1824
|
+
const children = node.children ?? [];
|
|
1825
|
+
const hasElementChild = children.some((c) => c.type === "JSXElement" || c.type === "JSXFragment");
|
|
1826
|
+
const hasText = children.some((c) => c.type === "JSXText" && LETTER.test(c.value) || c.type === "JSXExpressionContainer" && c.expression.type === "StringLiteral" && LETTER.test(c.expression.value));
|
|
1827
|
+
if (hasElementChild && hasText) {
|
|
1828
|
+
skip(node, snippet(children), "mixed text and markup: wrap by hand or use <Trans>");
|
|
1829
|
+
return markSubtree(node);
|
|
1830
|
+
}
|
|
1831
|
+
if (hasText) {
|
|
1832
|
+
wrapSegment(node, children);
|
|
1833
|
+
return markSubtree(node);
|
|
1834
|
+
}
|
|
1835
|
+
for (const child of children) if (child.type === "JSXElement" || child.type === "JSXFragment") processElement(child);
|
|
1836
|
+
}
|
|
1837
|
+
function wrapSegment(parent, children) {
|
|
1838
|
+
const meaningful = children.filter((c) => c.type === "JSXText" && c.value.trim() !== "" || c.type === "JSXExpressionContainer" && c.expression.type !== "JSXEmptyExpression");
|
|
1839
|
+
if (meaningful.length === 0) return;
|
|
1840
|
+
const first = meaningful[0];
|
|
1841
|
+
const last = meaningful[meaningful.length - 1];
|
|
1842
|
+
let template = "";
|
|
1843
|
+
let report = "";
|
|
1844
|
+
for (let i = children.indexOf(first); i <= children.indexOf(last); i++) {
|
|
1845
|
+
const child = children[i];
|
|
1846
|
+
if (child.type === "JSXText") {
|
|
1847
|
+
let value = cleanJsxText(child.value);
|
|
1848
|
+
if (child === first) value = value.trimStart();
|
|
1849
|
+
if (child === last) value = value.trimEnd();
|
|
1850
|
+
template += escapeTemplate(value);
|
|
1851
|
+
report += value;
|
|
1852
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
1853
|
+
const expr = child.expression;
|
|
1854
|
+
if (expr.type === "JSXEmptyExpression") continue;
|
|
1855
|
+
if (usesT(expr)) {
|
|
1856
|
+
skip(parent, snippet(children), "already uses t: finish it by hand");
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (containsJsx(expr)) {
|
|
1860
|
+
skip(parent, snippet(children), "an expression renders markup: wrap by hand or use <Trans>");
|
|
1861
|
+
markSubtree(child);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (expr.type === "StringLiteral") {
|
|
1865
|
+
template += escapeTemplate(expr.value);
|
|
1866
|
+
report += expr.value;
|
|
1867
|
+
} else {
|
|
1868
|
+
const source = code.slice(expr.start, expr.end);
|
|
1869
|
+
template += "${" + source + "}";
|
|
1870
|
+
report += "${" + source + "}";
|
|
1871
|
+
}
|
|
1872
|
+
} else return;
|
|
1873
|
+
}
|
|
1874
|
+
if (!LETTER.test(report)) return;
|
|
1875
|
+
const start = first.type === "JSXText" ? first.start + leadingWs(first.value) : first.start;
|
|
1876
|
+
const end = last.type === "JSXText" ? last.end - trailingWs(last.value) : last.end;
|
|
1877
|
+
s.overwrite(start, end, "{t`" + template + "`}");
|
|
1878
|
+
changed = true;
|
|
1879
|
+
wrapped.push({
|
|
1880
|
+
file,
|
|
1881
|
+
line: line(first),
|
|
1882
|
+
text: report,
|
|
1883
|
+
kind: "text"
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
function wrapAttributes(opening) {
|
|
1887
|
+
for (const attr of opening.attributes) {
|
|
1888
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
1889
|
+
const name = attr.name;
|
|
1890
|
+
if (name.type !== "JSXIdentifier" || !WRAP_ATTRS.has(name.name)) continue;
|
|
1891
|
+
const value = attr.value;
|
|
1892
|
+
if (value?.type !== "StringLiteral") continue;
|
|
1893
|
+
const raw = value.value;
|
|
1894
|
+
if (!LETTER.test(raw)) continue;
|
|
1895
|
+
s.overwrite(value.start, value.end, "{t`" + escapeTemplate(raw) + "`}");
|
|
1896
|
+
changed = true;
|
|
1897
|
+
wrapped.push({
|
|
1898
|
+
file,
|
|
1899
|
+
line: line(value),
|
|
1900
|
+
text: raw,
|
|
1901
|
+
kind: "attribute",
|
|
1902
|
+
attribute: name.name
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
function skip(node, text, reason) {
|
|
1907
|
+
skipped.push({
|
|
1908
|
+
file,
|
|
1909
|
+
line: line(node),
|
|
1910
|
+
text,
|
|
1911
|
+
reason
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
function snippet(children) {
|
|
1915
|
+
const start = children[0]?.start ?? 0;
|
|
1916
|
+
const end = children[children.length - 1]?.end ?? start;
|
|
1917
|
+
const text = code.slice(start, end).replace(/\s+/g, " ").trim();
|
|
1918
|
+
return text.length > 60 ? text.slice(0, 59) + "…" : text;
|
|
1919
|
+
}
|
|
1920
|
+
function markSubtree(node) {
|
|
1921
|
+
walk(node, (n) => {
|
|
1922
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment") visited.add(n);
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
function hasVerbalyAttr(opening) {
|
|
1927
|
+
return opening.attributes.some((attr) => {
|
|
1928
|
+
if (attr.type !== "JSXAttribute") return false;
|
|
1929
|
+
const name = attr.name;
|
|
1930
|
+
return name.type === "JSXIdentifier" && name.name.startsWith("data-verbaly");
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
function usesT(node) {
|
|
1934
|
+
let found = false;
|
|
1935
|
+
walk(node, (n) => {
|
|
1936
|
+
if (n.type === "TaggedTemplateExpression" && isTReference(n.tag, T_NAMES)) found = true;
|
|
1937
|
+
if (n.type === "CallExpression" && isTReference(n.callee, T_NAMES)) found = true;
|
|
1938
|
+
});
|
|
1939
|
+
return found;
|
|
1940
|
+
}
|
|
1941
|
+
function containsJsx(node) {
|
|
1942
|
+
let found = false;
|
|
1943
|
+
walk(node, (n) => {
|
|
1944
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment") found = true;
|
|
1945
|
+
});
|
|
1946
|
+
return found;
|
|
1947
|
+
}
|
|
1948
|
+
function escapeTemplate(text) {
|
|
1949
|
+
return text.replace(/[\\`]/g, "\\$&").replace(/\$\{/g, "\\${");
|
|
1950
|
+
}
|
|
1951
|
+
function leadingWs(text) {
|
|
1952
|
+
return text.length - text.trimStart().length;
|
|
1953
|
+
}
|
|
1954
|
+
function trailingWs(text) {
|
|
1955
|
+
return text.length - text.trimEnd().length;
|
|
1956
|
+
}
|
|
1957
|
+
function line(node) {
|
|
1958
|
+
return node.loc?.start?.line ?? 1;
|
|
1959
|
+
}
|
|
1960
|
+
//#endregion
|
|
1690
1961
|
//#region src/run.ts
|
|
1691
1962
|
const HELP = `verbaly · i18n compiler
|
|
1692
1963
|
|
|
1693
1964
|
Usage:
|
|
1694
1965
|
verbaly init scaffold config + locale catalogs (detects your bundler)
|
|
1695
1966
|
verbaly doctor diagnose the setup (config, catalogs, plugin, types, keys)
|
|
1967
|
+
verbaly wrap find hardcoded JSX text and wrap it in t\`…\` (report; --write applies)
|
|
1696
1968
|
verbaly extract scan sources, update catalogs and types
|
|
1697
1969
|
verbaly status translation coverage per locale, at a glance
|
|
1698
1970
|
verbaly check verify translations are complete (CI)
|
|
@@ -1709,6 +1981,9 @@ Options:
|
|
|
1709
1981
|
--locales <csv> extra locales; for translate: target locales to fill
|
|
1710
1982
|
--prune drop keys no longer referenced (extract)
|
|
1711
1983
|
--watch keep extracting as source files change (extract)
|
|
1984
|
+
--write apply the rewrites instead of only reporting (wrap)
|
|
1985
|
+
--json machine-readable output (status)
|
|
1986
|
+
--reporter <name> failure format: text (default) or github annotations (check)
|
|
1712
1987
|
--model <id> model override for the claude provider (translate)
|
|
1713
1988
|
--dry-run list what would happen, write nothing (translate, import, extract)
|
|
1714
1989
|
--format <f> export format: xliff (default), csv, android-xml or ios-strings (export)
|
|
@@ -1736,6 +2011,9 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1736
2011
|
locales: { type: "string" },
|
|
1737
2012
|
prune: { type: "boolean" },
|
|
1738
2013
|
watch: { type: "boolean" },
|
|
2014
|
+
write: { type: "boolean" },
|
|
2015
|
+
json: { type: "boolean" },
|
|
2016
|
+
reporter: { type: "string" },
|
|
1739
2017
|
model: { type: "string" },
|
|
1740
2018
|
locale: { type: "string" },
|
|
1741
2019
|
site: { type: "string" },
|
|
@@ -1818,7 +2096,7 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1818
2096
|
const { added } = syncCatalogs(cfg, catalogs, registry);
|
|
1819
2097
|
if (!dryRun) {
|
|
1820
2098
|
for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});
|
|
1821
|
-
writeDts(cfg, catalogs[cfg.sourceLocale] ?? {});
|
|
2099
|
+
if (cfg.dts !== false) writeDts(cfg, catalogs[cfg.sourceLocale] ?? {}, cfg.dts);
|
|
1822
2100
|
}
|
|
1823
2101
|
const total = registry.messages().size;
|
|
1824
2102
|
console.log(`[verbaly] ${total} messages · locales: ${cfg.locales.join(", ")}${dryRun ? " (dry run, nothing written)" : ""}`);
|
|
@@ -1831,19 +2109,49 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1831
2109
|
}
|
|
1832
2110
|
return;
|
|
1833
2111
|
}
|
|
2112
|
+
if (command === "wrap") {
|
|
2113
|
+
const result = await wrapProject(cfg, { write: values.write });
|
|
2114
|
+
if (result.wrapped.length === 0 && result.skipped.length === 0) {
|
|
2115
|
+
console.log(`[verbaly] nothing to wrap (${result.files} files scanned) ✓`);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
const verb = values.write ? "wrapped" : "would wrap";
|
|
2119
|
+
const note = values.write ? "" : " (report only, use --write to apply)";
|
|
2120
|
+
console.log(`[verbaly] ${verb} ${result.wrapped.length} texts in ${result.changed.length} files${note}`);
|
|
2121
|
+
for (const entry of result.wrapped) {
|
|
2122
|
+
const attr = entry.kind === "attribute" ? `${entry.attribute} → ` : "";
|
|
2123
|
+
console.log(` ${entry.file}:${entry.line} ${attr}"${entry.text}"`);
|
|
2124
|
+
}
|
|
2125
|
+
if (result.skipped.length > 0) {
|
|
2126
|
+
console.log(" needs a human:");
|
|
2127
|
+
for (const entry of result.skipped) console.log(` ${entry.file}:${entry.line} "${entry.text}" (${entry.reason})`);
|
|
2128
|
+
}
|
|
2129
|
+
if (values.write && result.changed.length > 0) console.log(" next: make t available where TS complains (React: const t = useT()), then run verbaly extract");
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
1834
2132
|
if (command === "status") {
|
|
1835
2133
|
const registry = await extractProject(cfg);
|
|
1836
|
-
|
|
2134
|
+
const result = status(cfg, loadCatalogs(cfg), registry);
|
|
2135
|
+
console.log(values.json ? JSON.stringify(result) : formatStatusResult(result));
|
|
1837
2136
|
return;
|
|
1838
2137
|
}
|
|
1839
2138
|
if (command === "check") {
|
|
2139
|
+
const reporter = values.reporter ?? "text";
|
|
2140
|
+
if (reporter !== "text" && reporter !== "github") {
|
|
2141
|
+
console.error(`[verbaly] unknown reporter "${values.reporter}", use text or github`);
|
|
2142
|
+
process.exitCode = 1;
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
1840
2145
|
const registry = await extractProject(cfg);
|
|
1841
2146
|
const result = check(cfg, loadCatalogs(cfg), registry);
|
|
1842
2147
|
if (result.ok) {
|
|
1843
2148
|
console.log("[verbaly] all translations complete ✓");
|
|
1844
2149
|
return;
|
|
1845
2150
|
}
|
|
1846
|
-
|
|
2151
|
+
if (reporter === "github") {
|
|
2152
|
+
for (const line of githubCheckAnnotations(result, registry, cfg.root)) console.error(line);
|
|
2153
|
+
console.error(`[verbaly] check failed: ${result.missing.length} missing, ${result.unknown.length} unknown`);
|
|
2154
|
+
} else console.error(`[verbaly] check failed\n${formatCheckResult(result)}`);
|
|
1847
2155
|
process.exitCode = 1;
|
|
1848
2156
|
return;
|
|
1849
2157
|
}
|
|
@@ -1967,8 +2275,9 @@ const COMMAND_FLAGS = {
|
|
|
1967
2275
|
"dry-run",
|
|
1968
2276
|
"watch"
|
|
1969
2277
|
],
|
|
1970
|
-
|
|
1971
|
-
|
|
2278
|
+
wrap: ["write"],
|
|
2279
|
+
status: ["json"],
|
|
2280
|
+
check: ["reporter"],
|
|
1972
2281
|
translate: ["model", "dry-run"],
|
|
1973
2282
|
export: [
|
|
1974
2283
|
"format",
|