@verbaly/compiler 0.14.5 β†’ 0.16.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/README.md CHANGED
@@ -17,13 +17,15 @@ The compiler behind [Verbaly](https://github.com/AronSoto/verbaly): AST extracti
17
17
 
18
18
  ## 🧰 CLI
19
19
 
20
- ```
20
+ ```bash
21
21
  npx verbaly init # scaffold config + locale catalogs (detects your bundler)
22
22
  npx verbaly doctor # diagnose the setup (config, catalogs, plugin, types, keys)
23
23
  npx verbaly extract # sync catalogs + types
24
24
  npx verbaly check # exit 1 if anything is missing (CI)
25
25
  npx verbaly extract --prune # drop orphaned keys
26
26
  npx verbaly translate # fill missing translations via Claude (or your provider)
27
+ npx verbaly export # write translator-ready XLIFF 2.0 / CSV files per locale
28
+ npx verbaly import <files> # fill catalogs back from translated XLIFF/CSV files
27
29
  npx verbaly pseudo # generate a pseudo-locale catalog for i18n QA (en-XA)
28
30
  npx verbaly render # pre-fill data-verbaly HTML per locale (SSG β€” kills the FOUC)
29
31
  ```
@@ -34,19 +36,31 @@ Reads `verbaly.config.{js,mjs,ts,mts,json}` (TS configs need `esbuild` installed
34
36
 
35
37
  `verbaly translate` fills the `""` holes `check` reports. The default provider uses Claude via the official SDK β€” install it as a dev dependency (translation is a build-time step, not an app runtime dependency): `pnpm add -D @anthropic-ai/sdk` (or `npm i -D`), plus `ANTHROPIC_API_KEY`. Default model is `claude-sonnet-5` (balanced quality/cost); override with `translate.model` in config or `--model <id>`. Placeholders, variants and tags are validated after translation β€” anything not preserved verbatim stays `""` so `check` keeps failing. Plug your own provider in `verbaly.config.ts`:
36
38
 
37
- ```
39
+ ```ts
38
40
  translate: {
39
41
  provider: async ({ sourceLocale, targetLocale, messages }) => ({ ...translated });
40
42
  }
41
43
  ```
42
44
 
45
+ ## 🌍 Human translators & TMS
46
+
47
+ Catalogs are **flat JSON** β€” most TMS platforms (Crowdin, Lokalise, Phrase, …) ingest them natively; point the platform at `locales/` and you're done. For everything else there's a built-in round-trip:
48
+
49
+ ```bash
50
+ npx verbaly export # verbaly-export/<locale>.xlf (XLIFF 2.0, source + target per unit)
51
+ npx verbaly export --format csv # spreadsheet-friendly: key,source,target
52
+ npx verbaly import verbaly-export/es.xlf # fill the catalog back
53
+ ```
54
+
55
+ `export` writes one file per target locale with the source text alongside the current translation (`--missing` exports only the untranslated entries). `import` reads XLIFF 2.0/1.2 or CSV back and **validates every entry like `translate` does** β€” a translation that drops a `{param}`, a variant block or an `<em>` tag is rejected and reported, so a translator's typo can't break your UI. Existing translations are kept unless `--overwrite`; `--dry-run` previews everything.
56
+
43
57
  ## πŸ“„ Static rendering (SSG)
44
58
 
45
59
  `verbaly render` walks your built site (`dist/` by default, `--site <path>` to change) and pre-fills every `data-verbaly` element **per locale** using the real runtime β€” plurals, `Intl` formatting, `data-verbaly-args`, attribute translation and `data-verbaly-rich` (same whitelist, XSS-safe). The source locale is filled in place; every other locale is mirrored to `dist/<locale>/…` with `<html lang>` set. Static HTML ships already translated β€” **no flash of untranslated content** β€” and the runtime attributes stay put, so client-side locale switching keeps working.
46
60
 
47
61
  Named links in rich messages render as real `<a>` elements β€” hrefs come from config or markup, never from messages (`javascript:` blocked):
48
62
 
49
- ```
63
+ ```ts
50
64
  // verbaly.config.ts
51
65
  render: { links: { docs: { href: '/docs', target: '_blank', rel: 'noopener' } } }
52
66
  ```
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
- import { dirname, join, relative, resolve } from "node:path";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { RICH_TAGS, createVerbaly, parse, parseTags, safeHref } from "verbaly";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { glob } from "tinyglobby";
@@ -154,6 +154,12 @@ ${lines.join("\n")}
154
154
 
155
155
  export const verbaly: import('verbaly').Verbaly<VerbalyKey>;
156
156
 
157
+ export const sourceLocale: string;
158
+ export const locales: string[];
159
+ export function createInstance(
160
+ options?: import('verbaly').VerbalyOptions<VerbalyKey>,
161
+ ): import('verbaly').Verbaly<VerbalyKey>;
162
+
157
163
  export function t<K extends VerbalyKey>(
158
164
  key: K,
159
165
  ...args: [VerbalyMessages[K]] extends [never] ? [] : [VerbalyMessages[K]]
@@ -759,6 +765,247 @@ function readDeps(root) {
759
765
  }
760
766
  }
761
767
  //#endregion
768
+ //#region src/translate.ts
769
+ async function translateCatalogs(cfg, catalogs, provider, options = {}) {
770
+ const batchSize = options.batchSize ?? 20;
771
+ const targets = (options.locales ?? cfg.locales).filter((locale) => locale !== cfg.sourceLocale && cfg.locales.includes(locale));
772
+ const source = catalogs[cfg.sourceLocale] ?? {};
773
+ const result = {
774
+ translated: {},
775
+ invalid: {},
776
+ pending: {}
777
+ };
778
+ for (const locale of targets) {
779
+ const catalog = catalogs[locale] ??= {};
780
+ const missing = Object.keys(source).filter((key) => source[key] && !catalog[key]);
781
+ if (missing.length === 0) continue;
782
+ if (options.dryRun) {
783
+ result.pending[locale] = missing;
784
+ continue;
785
+ }
786
+ for (let i = 0; i < missing.length; i += batchSize) {
787
+ const keys = missing.slice(i, i + batchSize);
788
+ const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
789
+ const out = await provider({
790
+ sourceLocale: cfg.sourceLocale,
791
+ targetLocale: locale,
792
+ messages
793
+ });
794
+ for (const key of keys) {
795
+ const text = out[key];
796
+ if (typeof text === "string" && text.trim() && structureMatches(source[key], text)) {
797
+ catalog[key] = text;
798
+ (result.translated[locale] ??= []).push(key);
799
+ } else (result.invalid[locale] ??= []).push(key);
800
+ }
801
+ }
802
+ }
803
+ return result;
804
+ }
805
+ function structureMatches(source, translated) {
806
+ return sameMembers(paramNames(source), paramNames(translated)) && sameMembers(tagTokens(source), tagTokens(translated));
807
+ }
808
+ function paramNames(message) {
809
+ try {
810
+ return [...collectParams(message).keys()].sort();
811
+ } catch {
812
+ return ["\0invalid"];
813
+ }
814
+ }
815
+ const TAG = /<(\/?)([a-zA-Z][\w-]*)(\/?)>/g;
816
+ function tagTokens(message) {
817
+ const out = [];
818
+ for (const match of message.matchAll(TAG)) out.push(`${match[1]}${match[2]}${match[3]}`);
819
+ return out.sort();
820
+ }
821
+ function sameMembers(a, b) {
822
+ return a.length === b.length && a.every((value, i) => value === b[i]);
823
+ }
824
+ //#endregion
825
+ //#region src/exchange.ts
826
+ function exportCatalogs(cfg, catalogs, options = {}) {
827
+ const format = options.format ?? "xliff";
828
+ const dir = resolve(cfg.root, options.out ?? "verbaly-export");
829
+ const source = catalogs[cfg.sourceLocale] ?? {};
830
+ const targets = (options.locales ?? cfg.locales).filter((locale) => locale !== cfg.sourceLocale && cfg.locales.includes(locale));
831
+ const files = [];
832
+ mkdirSync(dir, { recursive: true });
833
+ for (const locale of targets) {
834
+ const catalog = catalogs[locale] ?? {};
835
+ let entries = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
836
+ key,
837
+ source: source[key],
838
+ target: catalog[key] ?? ""
839
+ }));
840
+ if (options.missing) entries = entries.filter((entry) => !entry.target);
841
+ const path = join(dir, `${locale}.${format === "csv" ? "csv" : "xlf"}`);
842
+ writeFileSync(path, format === "csv" ? toCsv(entries) : toXliff(cfg.sourceLocale, locale, entries));
843
+ files.push({
844
+ locale,
845
+ path,
846
+ total: entries.length,
847
+ untranslated: entries.filter((entry) => !entry.target).length
848
+ });
849
+ }
850
+ return {
851
+ format,
852
+ dir,
853
+ files
854
+ };
855
+ }
856
+ function importCatalogs(cfg, catalogs, files, options = {}) {
857
+ const source = catalogs[cfg.sourceLocale] ?? {};
858
+ const result = {
859
+ imported: {},
860
+ rejected: {},
861
+ skipped: {},
862
+ unknown: {}
863
+ };
864
+ for (const file of files) {
865
+ const parsed = parseExchangeFile(file, options.locale);
866
+ const locale = parsed.locale;
867
+ if (!/^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]+)*$/.test(locale)) throw new Error(`[verbaly] ${file}: "${locale}" doesn't look like a locale β€” pass --locale <id>.`);
868
+ if (locale === cfg.sourceLocale) throw new Error(`[verbaly] ${file} targets the source locale "${locale}" β€” import fills translations, not the source. Pass --locale if the detection is wrong.`);
869
+ const catalog = catalogs[locale] ??= {};
870
+ for (const [key, text] of Object.entries(parsed.entries)) {
871
+ if (!text.trim()) continue;
872
+ if (!source[key]) {
873
+ (result.unknown[locale] ??= []).push(key);
874
+ continue;
875
+ }
876
+ if (catalog[key] && !options.overwrite) {
877
+ (result.skipped[locale] ??= []).push(key);
878
+ continue;
879
+ }
880
+ if (!structureMatches(source[key], text)) {
881
+ (result.rejected[locale] ??= []).push(key);
882
+ continue;
883
+ }
884
+ if (!options.dryRun) catalog[key] = text;
885
+ (result.imported[locale] ??= []).push(key);
886
+ }
887
+ }
888
+ return result;
889
+ }
890
+ function parseExchangeFile(file, localeOverride) {
891
+ const content = readFileSync(file, "utf8");
892
+ if (/\.(xlf|xliff)$/i.test(file)) {
893
+ const parsed = parseXliff(content);
894
+ const locale = localeOverride ?? parsed.locale;
895
+ if (!locale) throw new Error(`[verbaly] ${file} has no trgLang/target-language β€” pass --locale <id>.`);
896
+ return {
897
+ locale,
898
+ entries: parsed.entries
899
+ };
900
+ }
901
+ if (/\.csv$/i.test(file)) return {
902
+ locale: localeOverride ?? basename(file).replace(/\.csv$/i, ""),
903
+ entries: parseCsv(content)
904
+ };
905
+ throw new Error(`[verbaly] ${file}: unsupported format β€” expected .xlf, .xliff or .csv.`);
906
+ }
907
+ function toXliff(sourceLocale, locale, entries) {
908
+ const units = entries.map(({ key, source, target }) => [
909
+ ` <unit id="${escapeXml(key)}">`,
910
+ ` <segment state="${target ? "translated" : "initial"}">`,
911
+ ` <source>${escapeXml(source)}</source>`,
912
+ ` <target>${escapeXml(target)}</target>`,
913
+ " </segment>",
914
+ " </unit>"
915
+ ].join("\n")).join("\n");
916
+ return [
917
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
918
+ `<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="${escapeXml(sourceLocale)}" trgLang="${escapeXml(locale)}">`,
919
+ ` <file id="verbaly" original="${escapeXml(`${locale}.json`)}">`,
920
+ units,
921
+ " </file>",
922
+ "</xliff>",
923
+ ""
924
+ ].join("\n");
925
+ }
926
+ function parseXliff(content) {
927
+ const locale = /\btrgLang\s*=\s*"([^"]+)"/.exec(content)?.[1] ?? /\btarget-language\s*=\s*"([^"]+)"/.exec(content)?.[1];
928
+ const entries = {};
929
+ for (const match of content.matchAll(/<(?:trans-)?unit\b([^>]*)>([\s\S]*?)<\/(?:trans-)?unit>/g)) {
930
+ const id = /\bid\s*=\s*"([^"]*)"/.exec(match[1])?.[1];
931
+ if (!id) continue;
932
+ const target = /<target\b[^>]*>([\s\S]*?)<\/target>/.exec(match[2])?.[1];
933
+ entries[unescapeXml(id)] = target === void 0 ? "" : unescapeXml(stripCdata(target));
934
+ }
935
+ return {
936
+ locale,
937
+ entries
938
+ };
939
+ }
940
+ function escapeXml(text) {
941
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
942
+ }
943
+ function stripCdata(text) {
944
+ return text.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1");
945
+ }
946
+ function unescapeXml(text) {
947
+ return text.replace(/&(lt|gt|amp|quot|apos|#x?[0-9a-fA-F]+);/g, (_, entity) => {
948
+ if (entity === "lt") return "<";
949
+ if (entity === "gt") return ">";
950
+ if (entity === "amp") return "&";
951
+ if (entity === "quot") return "\"";
952
+ if (entity === "apos") return "'";
953
+ const code = entity.startsWith("#x") ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
954
+ return String.fromCodePoint(code);
955
+ });
956
+ }
957
+ function toCsv(entries) {
958
+ return [
959
+ "key,source,target",
960
+ ...entries.map(({ key, source, target }) => `${csvField(key)},${csvField(source)},${csvField(target)}`),
961
+ ""
962
+ ].join("\r\n");
963
+ }
964
+ function csvField(text) {
965
+ return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, "\"\"")}"` : text;
966
+ }
967
+ function parseCsv(content) {
968
+ const rows = csvRows(content);
969
+ const header = rows[0]?.map((cell) => cell.trim().toLowerCase()) ?? [];
970
+ const keyCol = header.indexOf("key");
971
+ const targetCol = header.indexOf("target");
972
+ if (keyCol === -1 || targetCol === -1) throw new Error("[verbaly] CSV needs a header row with \"key\" and \"target\" columns.");
973
+ const entries = {};
974
+ for (const row of rows.slice(1)) {
975
+ const key = row[keyCol];
976
+ if (key) entries[key] = row[targetCol] ?? "";
977
+ }
978
+ return entries;
979
+ }
980
+ function csvRows(content) {
981
+ const rows = [];
982
+ let row = [];
983
+ let field = "";
984
+ let quoted = false;
985
+ for (let i = 0; i < content.length; i++) {
986
+ const char = content[i];
987
+ if (quoted) if (char === "\"") if (content[i + 1] === "\"") {
988
+ field += "\"";
989
+ i++;
990
+ } else quoted = false;
991
+ else field += char;
992
+ else if (char === "\"") quoted = true;
993
+ else if (char === ",") {
994
+ row.push(field);
995
+ field = "";
996
+ } else if (char === "\n" || char === "\r") {
997
+ if (char === "\r" && content[i + 1] === "\n") i++;
998
+ row.push(field);
999
+ field = "";
1000
+ if (row.some((cell) => cell !== "")) rows.push(row);
1001
+ row = [];
1002
+ } else field += char;
1003
+ }
1004
+ row.push(field);
1005
+ if (row.some((cell) => cell !== "")) rows.push(row);
1006
+ return rows;
1007
+ }
1008
+ //#endregion
762
1009
  //#region src/pseudo.ts
763
1010
  const PSEUDO_LOCALE = "en-XA";
764
1011
  const ACCENTS = {
@@ -1148,63 +1395,6 @@ function decodeEntities(text) {
1148
1395
  return text.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
1149
1396
  }
1150
1397
  //#endregion
1151
- //#region src/translate.ts
1152
- async function translateCatalogs(cfg, catalogs, provider, options = {}) {
1153
- const batchSize = options.batchSize ?? 20;
1154
- const targets = (options.locales ?? cfg.locales).filter((locale) => locale !== cfg.sourceLocale && cfg.locales.includes(locale));
1155
- const source = catalogs[cfg.sourceLocale] ?? {};
1156
- const result = {
1157
- translated: {},
1158
- invalid: {},
1159
- pending: {}
1160
- };
1161
- for (const locale of targets) {
1162
- const catalog = catalogs[locale] ??= {};
1163
- const missing = Object.keys(source).filter((key) => source[key] && !catalog[key]);
1164
- if (missing.length === 0) continue;
1165
- if (options.dryRun) {
1166
- result.pending[locale] = missing;
1167
- continue;
1168
- }
1169
- for (let i = 0; i < missing.length; i += batchSize) {
1170
- const keys = missing.slice(i, i + batchSize);
1171
- const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
1172
- const out = await provider({
1173
- sourceLocale: cfg.sourceLocale,
1174
- targetLocale: locale,
1175
- messages
1176
- });
1177
- for (const key of keys) {
1178
- const text = out[key];
1179
- if (typeof text === "string" && text.trim() && structureMatches(source[key], text)) {
1180
- catalog[key] = text;
1181
- (result.translated[locale] ??= []).push(key);
1182
- } else (result.invalid[locale] ??= []).push(key);
1183
- }
1184
- }
1185
- }
1186
- return result;
1187
- }
1188
- function structureMatches(source, translated) {
1189
- return sameMembers(paramNames(source), paramNames(translated)) && sameMembers(tagTokens(source), tagTokens(translated));
1190
- }
1191
- function paramNames(message) {
1192
- try {
1193
- return [...collectParams(message).keys()].sort();
1194
- } catch {
1195
- return ["\0invalid"];
1196
- }
1197
- }
1198
- const TAG = /<(\/?)([a-zA-Z][\w-]*)(\/?)>/g;
1199
- function tagTokens(message) {
1200
- const out = [];
1201
- for (const match of message.matchAll(TAG)) out.push(`${match[1]}${match[2]}${match[3]}`);
1202
- return out.sort();
1203
- }
1204
- function sameMembers(a, b) {
1205
- return a.length === b.length && a.every((value, i) => value === b[i]);
1206
- }
1207
- //#endregion
1208
1398
  //#region src/cli.ts
1209
1399
  const HELP = `verbaly β€” i18n compiler
1210
1400
 
@@ -1214,6 +1404,8 @@ Usage:
1214
1404
  verbaly extract scan sources, update catalogs and types
1215
1405
  verbaly check verify translations are complete (CI)
1216
1406
  verbaly translate fill missing translations via a provider (default: claude)
1407
+ verbaly export write translator-ready files per locale (XLIFF 2.0 or CSV)
1408
+ verbaly import <files…> fill catalogs back from translated XLIFF/CSV files
1217
1409
  verbaly pseudo generate a pseudo-locale catalog for i18n QA (default: en-XA)
1218
1410
  verbaly render pre-fill data-verbaly HTML per locale (SSG, kills the FOUC)
1219
1411
 
@@ -1224,8 +1416,12 @@ Options:
1224
1416
  --locales <csv> extra locales; for translate: target locales to fill
1225
1417
  --prune drop keys no longer referenced (extract)
1226
1418
  --model <id> model override for the claude provider (translate)
1227
- --dry-run list what would be translated, write nothing (translate)
1228
- --locale <id> pseudo-locale id (pseudo, default: en-XA)
1419
+ --dry-run list what would happen, write nothing (translate, import)
1420
+ --format <f> export format: xliff (default) or csv (export)
1421
+ --out <path> export directory (export, default: verbaly-export)
1422
+ --missing export only untranslated entries (export)
1423
+ --overwrite replace existing translations on import (import)
1424
+ --locale <id> pseudo-locale id (pseudo) / target-locale override (import)
1229
1425
  --site <path> built site directory (render, default: dist)
1230
1426
  --attribute <name> base data attribute (render, default: data-verbaly)
1231
1427
  --base-url <url> site origin β€” enables hreflang alternates (render)
@@ -1252,6 +1448,10 @@ async function main() {
1252
1448
  sitemap: { type: "boolean" },
1253
1449
  clean: { type: "boolean" },
1254
1450
  "dry-run": { type: "boolean" },
1451
+ format: { type: "string" },
1452
+ out: { type: "string" },
1453
+ missing: { type: "boolean" },
1454
+ overwrite: { type: "boolean" },
1255
1455
  help: {
1256
1456
  type: "boolean",
1257
1457
  short: "h"
@@ -1354,6 +1554,51 @@ async function main() {
1354
1554
  if (Object.keys(result.translated).length === 0 && Object.keys(result.invalid).length === 0) console.log("[verbaly] nothing to translate βœ“");
1355
1555
  return;
1356
1556
  }
1557
+ if (command === "export") {
1558
+ const format = values.format ?? "xliff";
1559
+ if (format !== "xliff" && format !== "csv") {
1560
+ console.error(`[verbaly] unknown format "${values.format}" β€” use xliff or csv`);
1561
+ process.exitCode = 1;
1562
+ return;
1563
+ }
1564
+ const result = exportCatalogs(cfg, loadCatalogs(cfg), {
1565
+ locales: values.locales?.split(","),
1566
+ format,
1567
+ out: values.out,
1568
+ missing: values.missing
1569
+ });
1570
+ if (result.files.length === 0) {
1571
+ console.log("[verbaly] no target locales to export (add locales to your config)");
1572
+ return;
1573
+ }
1574
+ console.log(`[verbaly] exported ${result.files.length} locales (${result.format}) β†’ ${result.dir}`);
1575
+ for (const file of result.files) console.log(` ${file.locale}: ${file.total} messages (${file.untranslated} untranslated) β†’ ${file.path}`);
1576
+ return;
1577
+ }
1578
+ if (command === "import") {
1579
+ const files = positionals.slice(1);
1580
+ if (files.length === 0) {
1581
+ console.error("[verbaly] import needs at least one file: verbaly import verbaly-export/es.xlf");
1582
+ process.exitCode = 1;
1583
+ return;
1584
+ }
1585
+ const catalogs = loadCatalogs(cfg);
1586
+ const result = importCatalogs(cfg, catalogs, files, {
1587
+ locale: values.locale,
1588
+ overwrite: values.overwrite,
1589
+ dryRun: values["dry-run"]
1590
+ });
1591
+ for (const [locale, keys] of Object.entries(result.imported)) {
1592
+ if (!values["dry-run"]) writeCatalog(cfg, locale, catalogs[locale] ?? {});
1593
+ const verb = values["dry-run"] ? "would import" : "imported";
1594
+ console.log(` ${locale}: +${keys.length} ${verb}`);
1595
+ }
1596
+ for (const [locale, keys] of Object.entries(result.skipped)) console.log(` ${locale}: ${keys.length} already translated, kept (use --overwrite to replace)`);
1597
+ for (const [locale, keys] of Object.entries(result.rejected)) console.warn(` ${locale}: ${keys.length} rejected (params/tags not preserved): ${keys.join(", ")}`);
1598
+ for (const [locale, keys] of Object.entries(result.unknown)) console.warn(` ${locale}: ${keys.length} unknown keys ignored (not in the source catalog): ${keys.join(", ")}`);
1599
+ if (Object.keys(result.imported).length === 0) console.log("[verbaly] nothing to import βœ“");
1600
+ return;
1601
+ }
1357
1602
  if (command === "render") {
1358
1603
  const result = await renderSite(cfg, {
1359
1604
  site: values.site,