@verbaly/compiler 0.11.0 → 0.13.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 CHANGED
@@ -1,145 +1,151 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { parseArgs as parseArgs2 } from "util";
5
-
6
- // src/catalog.ts
7
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
8
- import { join } from "path";
2
+ import { parseArgs } from "node:util";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+ import { RICH_TAGS, createVerbaly, parse, parseTags, safeHref } from "verbaly";
6
+ import { pathToFileURL } from "node:url";
7
+ import { glob } from "tinyglobby";
8
+ import { parse as parse$1 } from "@babel/parser";
9
+ import { createHash } from "node:crypto";
10
+ import MagicString from "magic-string";
11
+ //#region src/catalog.ts
9
12
  function catalogPath(cfg, locale) {
10
- return join(cfg.dir, `${locale}.json`);
13
+ return join(cfg.dir, `${locale}.json`);
11
14
  }
12
15
  function readCatalog(cfg, locale) {
13
- try {
14
- return JSON.parse(readFileSync(catalogPath(cfg, locale), "utf8"));
15
- } catch {
16
- return {};
17
- }
16
+ try {
17
+ return JSON.parse(readFileSync(catalogPath(cfg, locale), "utf8"));
18
+ } catch {
19
+ return {};
20
+ }
18
21
  }
19
22
  function loadCatalogs(cfg) {
20
- const catalogs = {};
21
- for (const locale of cfg.locales) catalogs[locale] = readCatalog(cfg, locale);
22
- return catalogs;
23
+ const catalogs = {};
24
+ for (const locale of cfg.locales) catalogs[locale] = readCatalog(cfg, locale);
25
+ return catalogs;
23
26
  }
24
27
  function serializeCatalog(catalog) {
25
- const sorted = {};
26
- for (const key of Object.keys(catalog).sort()) {
27
- const value = catalog[key];
28
- if (value !== void 0) sorted[key] = value;
29
- }
30
- return JSON.stringify(sorted, null, 2) + "\n";
28
+ const sorted = {};
29
+ for (const key of Object.keys(catalog).sort()) {
30
+ const value = catalog[key];
31
+ if (value !== void 0) sorted[key] = value;
32
+ }
33
+ return JSON.stringify(sorted, null, 2) + "\n";
31
34
  }
32
35
  function writeCatalog(cfg, locale, catalog) {
33
- const serialized = serializeCatalog(catalog);
34
- mkdirSync(cfg.dir, { recursive: true });
35
- writeFileSync(catalogPath(cfg, locale), serialized);
36
- return serialized;
36
+ const serialized = serializeCatalog(catalog);
37
+ mkdirSync(cfg.dir, { recursive: true });
38
+ writeFileSync(catalogPath(cfg, locale), serialized);
39
+ return serialized;
37
40
  }
38
-
39
- // src/check.ts
41
+ //#endregion
42
+ //#region src/check.ts
40
43
  function check(cfg, catalogs, registry) {
41
- const source = catalogs[cfg.sourceLocale] ?? {};
42
- const extracted = registry.messages();
43
- const unknown = [];
44
- for (const [key, files] of registry.usedKeys()) {
45
- const known = extracted.has(key) || cfg.locales.some((locale) => catalogs[locale]?.[key] !== void 0);
46
- if (!known) unknown.push({ key, files });
47
- }
48
- const needed = /* @__PURE__ */ new Set([...extracted.keys(), ...Object.keys(source)]);
49
- const missing = [];
50
- for (const key of extracted.keys()) {
51
- if (!source[key]) {
52
- missing.push({ locale: cfg.sourceLocale, key, source: extracted.get(key)?.message });
53
- }
54
- }
55
- for (const locale of cfg.locales) {
56
- if (locale === cfg.sourceLocale) continue;
57
- for (const key of needed) {
58
- if (!catalogs[locale]?.[key]) {
59
- missing.push({ locale, key, source: source[key] ?? extracted.get(key)?.message });
60
- }
61
- }
62
- }
63
- return { ok: missing.length === 0 && unknown.length === 0, missing, unknown };
44
+ const source = catalogs[cfg.sourceLocale] ?? {};
45
+ const extracted = registry.messages();
46
+ const unknown = [];
47
+ for (const [key, files] of registry.usedKeys()) if (!(extracted.has(key) || cfg.locales.some((locale) => catalogs[locale]?.[key] !== void 0))) unknown.push({
48
+ key,
49
+ files
50
+ });
51
+ const needed = /* @__PURE__ */ new Set([...extracted.keys(), ...Object.keys(source)]);
52
+ const missing = [];
53
+ for (const key of extracted.keys()) if (!source[key]) missing.push({
54
+ locale: cfg.sourceLocale,
55
+ key,
56
+ source: extracted.get(key)?.message
57
+ });
58
+ for (const locale of cfg.locales) {
59
+ if (locale === cfg.sourceLocale) continue;
60
+ for (const key of needed) if (!catalogs[locale]?.[key]) missing.push({
61
+ locale,
62
+ key,
63
+ source: source[key] ?? extracted.get(key)?.message
64
+ });
65
+ }
66
+ return {
67
+ ok: missing.length === 0 && unknown.length === 0,
68
+ missing,
69
+ unknown
70
+ };
64
71
  }
65
72
  function formatCheckResult(result) {
66
- const lines = [];
67
- if (result.missing.length > 0) {
68
- lines.push("missing translations:");
69
- for (const entry of result.missing) {
70
- const hint = entry.source ? ` \u2014 "${truncate(entry.source, 40)}"` : "";
71
- lines.push(` [${entry.locale}] ${entry.key}${hint}`);
72
- }
73
- }
74
- if (result.unknown.length > 0) {
75
- lines.push("unknown keys (not in any catalog):");
76
- for (const entry of result.unknown) {
77
- lines.push(` ${entry.key} \u2014 used in ${entry.files.join(", ")}`);
78
- }
79
- }
80
- return lines.join("\n");
73
+ const lines = [];
74
+ if (result.missing.length > 0) {
75
+ lines.push("missing translations:");
76
+ for (const entry of result.missing) {
77
+ const hint = entry.source ? ` "${truncate(entry.source, 40)}"` : "";
78
+ lines.push(` [${entry.locale}] ${entry.key}${hint}`);
79
+ }
80
+ }
81
+ if (result.unknown.length > 0) {
82
+ lines.push("unknown keys (not in any catalog):");
83
+ for (const entry of result.unknown) lines.push(` ${entry.key} — used in ${entry.files.join(", ")}`);
84
+ }
85
+ return lines.join("\n");
81
86
  }
82
87
  function truncate(text, max) {
83
- return text.length > max ? text.slice(0, max - 1) + "\u2026" : text;
88
+ return text.length > max ? text.slice(0, max - 1) + "" : text;
84
89
  }
85
-
86
- // src/codegen.ts
87
- import { writeFileSync as writeFileSync2 } from "fs";
88
- import { join as join2 } from "path";
89
-
90
- // src/params.ts
91
- import { parse } from "verbaly";
92
- var PLURAL_KEYS = /* @__PURE__ */ new Set(["zero", "one", "two", "few", "many"]);
93
- var NUMBER_FORMATS = /* @__PURE__ */ new Set(["number", "integer", "percent", "currency"]);
94
- var DATE_FORMATS = /* @__PURE__ */ new Set(["date", "time"]);
90
+ //#endregion
91
+ //#region src/params.ts
92
+ const PLURAL_KEYS = /* @__PURE__ */ new Set([
93
+ "zero",
94
+ "one",
95
+ "two",
96
+ "few",
97
+ "many"
98
+ ]);
99
+ const NUMBER_FORMATS = /* @__PURE__ */ new Set([
100
+ "number",
101
+ "integer",
102
+ "percent",
103
+ "currency"
104
+ ]);
105
+ const DATE_FORMATS = /* @__PURE__ */ new Set(["date", "time"]);
95
106
  function collectParams(message) {
96
- const out = /* @__PURE__ */ new Map();
97
- visit(parse(message), out);
98
- return out;
107
+ const out = /* @__PURE__ */ new Map();
108
+ visit(parse(message), out);
109
+ return out;
99
110
  }
100
111
  function visit(nodes, out) {
101
- for (const node of nodes) {
102
- if (node.kind !== "param") continue;
103
- const types = out.get(node.name) ?? /* @__PURE__ */ new Set();
104
- out.set(node.name, types);
105
- if (node.variants) {
106
- for (const [key, body] of node.variants) {
107
- if (PLURAL_KEYS.has(key) || /^=\d+$/.test(key)) types.add("number");
108
- else if (key !== "other") types.add("string");
109
- visit(body, out);
110
- }
111
- if (types.size === 0) types.add("string");
112
- } else if (node.format && NUMBER_FORMATS.has(node.format)) {
113
- types.add("number");
114
- } else if (node.format && DATE_FORMATS.has(node.format)) {
115
- types.add("date");
116
- } else {
117
- types.add("unknown");
118
- }
119
- }
112
+ for (const node of nodes) {
113
+ if (node.kind !== "param") continue;
114
+ const types = out.get(node.name) ?? /* @__PURE__ */ new Set();
115
+ out.set(node.name, types);
116
+ if (node.variants) {
117
+ for (const [key, body] of node.variants) {
118
+ if (PLURAL_KEYS.has(key) || /^=\d+$/.test(key)) types.add("number");
119
+ else if (key !== "other") types.add("string");
120
+ visit(body, out);
121
+ }
122
+ if (types.size === 0) types.add("string");
123
+ } else if (node.format && NUMBER_FORMATS.has(node.format)) types.add("number");
124
+ else if (node.format && DATE_FORMATS.has(node.format)) types.add("date");
125
+ else types.add("unknown");
126
+ }
120
127
  }
121
128
  function renderParamType(types) {
122
- if (types.has("unknown") || types.size === 0) return "unknown";
123
- const parts = [];
124
- if (types.has("number")) parts.push("number");
125
- if (types.has("string")) parts.push("string");
126
- if (types.has("date")) parts.push("Date | number | string");
127
- return parts.join(" | ");
129
+ if (types.has("unknown") || types.size === 0) return "unknown";
130
+ const parts = [];
131
+ if (types.has("number")) parts.push("number");
132
+ if (types.has("string")) parts.push("string");
133
+ if (types.has("date")) parts.push("Date | number | string");
134
+ return parts.join(" | ");
128
135
  }
129
-
130
- // src/codegen.ts
136
+ //#endregion
137
+ //#region src/codegen.ts
131
138
  function generateDts(entries) {
132
- const lines = [];
133
- for (const [key, message] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
134
- const params = collectParams(message);
135
- if (params.size === 0) {
136
- lines.push(` ${JSON.stringify(key)}: never;`);
137
- } else {
138
- const fields = [...params.entries()].map(([name, types]) => `${JSON.stringify(name)}: ${renderParamType(types)}`).join("; ");
139
- lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
140
- }
141
- }
142
- return `// generated by verbaly \u2014 do not edit
139
+ const lines = [];
140
+ for (const [key, message] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
141
+ const params = collectParams(message);
142
+ if (params.size === 0) lines.push(` ${JSON.stringify(key)}: never;`);
143
+ else {
144
+ const fields = [...params.entries()].map(([name, types]) => `${JSON.stringify(name)}: ${renderParamType(types)}`).join("; ");
145
+ lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
146
+ }
147
+ }
148
+ return `// generated by verbaly — do not edit
143
149
  declare module 'virtual:verbaly' {
144
150
  export interface VerbalyMessages {
145
151
  ${lines.join("\n")}
@@ -171,830 +177,979 @@ declare module 'virtual:verbaly/locale/*' {
171
177
  `;
172
178
  }
173
179
  function writeDts(cfg, entries) {
174
- writeFileSync2(join2(cfg.root, "verbaly.d.ts"), generateDts(entries));
180
+ writeFileSync(join(cfg.root, "verbaly.d.ts"), generateDts(entries));
175
181
  }
176
-
177
- // src/config.ts
178
- import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
179
- import { join as join3, resolve } from "path";
180
- import { pathToFileURL } from "url";
182
+ //#endregion
183
+ //#region src/config.ts
181
184
  function resolveConfig(config = {}) {
182
- const root = resolve(config.root ?? process.cwd());
183
- const dir = resolve(root, config.dir ?? "locales");
184
- const sourceLocale = config.sourceLocale ?? "en";
185
- const locales = /* @__PURE__ */ new Set([sourceLocale, ...config.locales ?? []]);
186
- if (existsSync(dir)) {
187
- for (const file of readdirSync(dir)) {
188
- if (file.endsWith(".json")) locales.add(file.slice(0, -5));
189
- }
190
- }
191
- return {
192
- root,
193
- dir,
194
- sourceLocale,
195
- locales: [...locales],
196
- include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
197
- exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
198
- translate: config.translate ?? {},
199
- render: config.render ?? {}
200
- };
185
+ const root = resolve(config.root ?? process.cwd());
186
+ const dir = resolve(root, config.dir ?? "locales");
187
+ const sourceLocale = config.sourceLocale ?? "en";
188
+ const locales = /* @__PURE__ */ new Set([sourceLocale, ...config.locales ?? []]);
189
+ if (existsSync(dir)) {
190
+ for (const file of readdirSync(dir)) if (file.endsWith(".json")) locales.add(file.slice(0, -5));
191
+ }
192
+ return {
193
+ root,
194
+ dir,
195
+ sourceLocale,
196
+ locales: [...locales],
197
+ include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
198
+ exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
199
+ translate: config.translate ?? {},
200
+ render: config.render ?? {}
201
+ };
202
+ }
203
+ const CONFIG_FILES = [
204
+ "verbaly.config.js",
205
+ "verbaly.config.mjs",
206
+ "verbaly.config.ts",
207
+ "verbaly.config.mts",
208
+ "verbaly.config.json"
209
+ ];
210
+ function findConfigFile(root) {
211
+ return CONFIG_FILES.find((name) => existsSync(join(root, name)));
201
212
  }
202
213
  async function loadConfigFile(root) {
203
- for (const name of ["verbaly.config.js", "verbaly.config.mjs"]) {
204
- const path = join3(root, name);
205
- if (existsSync(path)) {
206
- const mod = await import(pathToFileURL(path).href);
207
- return mod.default ?? {};
208
- }
209
- }
210
- for (const name of ["verbaly.config.ts", "verbaly.config.mts"]) {
211
- const path = join3(root, name);
212
- if (existsSync(path)) return loadTsConfig(path);
213
- }
214
- const jsonPath = join3(root, "verbaly.config.json");
215
- if (existsSync(jsonPath)) {
216
- return JSON.parse(readFileSync2(jsonPath, "utf8"));
217
- }
218
- return {};
214
+ for (const name of ["verbaly.config.js", "verbaly.config.mjs"]) {
215
+ const path = join(root, name);
216
+ if (existsSync(path)) return (await import(pathToFileURL(path).href)).default ?? {};
217
+ }
218
+ for (const name of ["verbaly.config.ts", "verbaly.config.mts"]) {
219
+ const path = join(root, name);
220
+ if (existsSync(path)) return loadTsConfig(path);
221
+ }
222
+ const jsonPath = join(root, "verbaly.config.json");
223
+ if (existsSync(jsonPath)) return JSON.parse(readFileSync(jsonPath, "utf8"));
224
+ return {};
219
225
  }
220
226
  async function loadTsConfig(path) {
221
- try {
222
- const { bundleRequire } = await import("bundle-require");
223
- const { mod } = await bundleRequire({ filepath: path });
224
- return mod.default ?? {};
225
- } catch (error) {
226
- if (isModuleNotFound(error, "esbuild")) {
227
- throw new Error(`[verbaly] ${path} needs esbuild \u2014 install it: pnpm add -D esbuild`, {
228
- cause: error
229
- });
230
- }
231
- throw error;
232
- }
227
+ try {
228
+ const { bundleRequire } = await import("bundle-require");
229
+ const { mod } = await bundleRequire({ filepath: path });
230
+ return mod.default ?? {};
231
+ } catch (error) {
232
+ if (isModuleNotFound(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild — install it: pnpm add -D esbuild`, { cause: error });
233
+ throw error;
234
+ }
233
235
  }
234
236
  function isModuleNotFound(error, name) {
235
- return error instanceof Error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes(name);
237
+ return error instanceof Error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes(name);
236
238
  }
237
239
  async function loadConfig(root, overrides = {}) {
238
- const fileConfig = await loadConfigFile(root);
239
- return resolveConfig({ ...fileConfig, ...definedOnly(overrides), root });
240
+ return resolveConfig({
241
+ ...await loadConfigFile(root),
242
+ ...definedOnly(overrides),
243
+ root
244
+ });
240
245
  }
241
246
  function definedOnly(config) {
242
- return Object.fromEntries(
243
- Object.entries(config).filter(([, value]) => value !== void 0)
244
- );
247
+ return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
245
248
  }
246
-
247
- // src/extract.ts
248
- import { readFileSync as readFileSync3 } from "fs";
249
- import { glob } from "tinyglobby";
250
-
251
- // src/analyze.ts
252
- import { parse as parse2 } from "@babel/parser";
253
-
254
- // src/key.ts
255
- import { createHash } from "crypto";
249
+ //#endregion
250
+ //#region src/key.ts
256
251
  function stableKey(message) {
257
- return createHash("sha256").update(message).digest("base64url").slice(0, 8);
252
+ return createHash("sha256").update(message).digest("base64url").slice(0, 8);
258
253
  }
259
-
260
- // src/analyze.ts
261
- var SKIP_KEYS = /* @__PURE__ */ new Set(["loc", "leadingComments", "trailingComments", "innerComments", "extra"]);
254
+ //#endregion
255
+ //#region src/analyze.ts
256
+ const SKIP_KEYS = /* @__PURE__ */ new Set([
257
+ "loc",
258
+ "leadingComments",
259
+ "trailingComments",
260
+ "innerComments",
261
+ "extra"
262
+ ]);
262
263
  function analyze(code, file) {
263
- const jsx = /\.[jt]sx$/.test(file);
264
- const ast = parse2(code, {
265
- sourceType: "module",
266
- errorRecovery: true,
267
- plugins: jsx ? ["typescript", "jsx"] : ["typescript"]
268
- });
269
- const tagged = [];
270
- const usedKeys = [];
271
- walk(ast.program, (node) => {
272
- if (node.type === "TaggedTemplateExpression") {
273
- const tag = node.tag;
274
- const explicit = explicitId(tag);
275
- if (!explicit && !isTReference(tag)) return;
276
- const quasi = node.quasi;
277
- const message = buildMessage(code, quasi);
278
- if (!message) return;
279
- tagged.push({
280
- key: explicit ? explicit.key : stableKey(message.text),
281
- message: message.text,
282
- params: message.params,
283
- start: node.start,
284
- end: node.end,
285
- tagStart: explicit ? explicit.refStart : tag.start,
286
- tagEnd: explicit ? explicit.refEnd : tag.end,
287
- file
288
- });
289
- } else if (node.type === "CallExpression") {
290
- const callee = node.callee;
291
- if (!isTReference(callee)) return;
292
- const args = node.arguments;
293
- const first = args[0];
294
- if (first?.type === "StringLiteral") {
295
- usedKeys.push({ key: first.value, file });
296
- }
297
- } else if (node.type === "JSXElement") {
298
- handleTrans(code, node, file, tagged, usedKeys);
299
- }
300
- });
301
- return { tagged, usedKeys };
264
+ const ast = parse$1(code, {
265
+ sourceType: "module",
266
+ errorRecovery: true,
267
+ plugins: /\.[jt]sx$/.test(file) ? ["typescript", "jsx"] : ["typescript"]
268
+ });
269
+ const tagged = [];
270
+ const usedKeys = [];
271
+ walk(ast.program, (node) => {
272
+ if (node.type === "TaggedTemplateExpression") {
273
+ const tag = node.tag;
274
+ const explicit = explicitId(tag);
275
+ if (!explicit && !isTReference(tag)) return;
276
+ const quasi = node.quasi;
277
+ const message = buildMessage(code, quasi);
278
+ if (!message) return;
279
+ tagged.push({
280
+ key: explicit ? explicit.key : stableKey(message.text),
281
+ message: message.text,
282
+ params: message.params,
283
+ start: node.start,
284
+ end: node.end,
285
+ tagStart: explicit ? explicit.refStart : tag.start,
286
+ tagEnd: explicit ? explicit.refEnd : tag.end,
287
+ file
288
+ });
289
+ } else if (node.type === "CallExpression") {
290
+ const callee = node.callee;
291
+ if (!isTReference(callee)) return;
292
+ const first = node.arguments[0];
293
+ if (first?.type === "StringLiteral") usedKeys.push({
294
+ key: first.value,
295
+ file
296
+ });
297
+ } else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys);
298
+ });
299
+ return {
300
+ tagged,
301
+ usedKeys
302
+ };
302
303
  }
303
304
  function handleTrans(code, node, file, tagged, usedKeys) {
304
- const opening = node.openingElement;
305
- const nameNode = opening.name;
306
- if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
307
- const attrs = opening.attributes;
308
- const idAttr = attrs.find((a) => a.type === "JSXAttribute" && a.name.name === "id");
309
- const children = node.children;
310
- if (idAttr) {
311
- const value = idAttr.value;
312
- if (value?.type !== "StringLiteral") return;
313
- const id = value.value;
314
- if (attrs.length === 1 && children?.length) {
315
- const built2 = buildTransMessage(code, children);
316
- if (built2?.text.trim()) {
317
- tagged.push({
318
- key: id,
319
- message: built2.text,
320
- params: built2.params,
321
- start: node.start,
322
- end: node.end,
323
- tagStart: nameNode.start,
324
- tagEnd: nameNode.end,
325
- file,
326
- jsx: { name: nameNode.name, components: built2.components }
327
- });
328
- return;
329
- }
330
- }
331
- usedKeys.push({ key: id, file });
332
- return;
333
- }
334
- if (attrs.length > 0) return;
335
- if (!children?.length) return;
336
- const built = buildTransMessage(code, children);
337
- if (!built || !built.text.trim()) return;
338
- tagged.push({
339
- key: stableKey(built.text),
340
- message: built.text,
341
- params: built.params,
342
- start: node.start,
343
- end: node.end,
344
- tagStart: nameNode.start,
345
- tagEnd: nameNode.end,
346
- file,
347
- jsx: { name: nameNode.name, components: built.components }
348
- });
305
+ const opening = node.openingElement;
306
+ const nameNode = opening.name;
307
+ if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
308
+ const attrs = opening.attributes;
309
+ const idAttr = attrs.find((a) => a.type === "JSXAttribute" && a.name.name === "id");
310
+ const children = node.children;
311
+ if (idAttr) {
312
+ const value = idAttr.value;
313
+ if (value?.type !== "StringLiteral") return;
314
+ const id = value.value;
315
+ if (attrs.length === 1 && children?.length) {
316
+ const built = buildTransMessage(code, children);
317
+ if (built?.text.trim()) {
318
+ tagged.push({
319
+ key: id,
320
+ message: built.text,
321
+ params: built.params,
322
+ start: node.start,
323
+ end: node.end,
324
+ tagStart: nameNode.start,
325
+ tagEnd: nameNode.end,
326
+ file,
327
+ jsx: {
328
+ name: nameNode.name,
329
+ components: built.components
330
+ }
331
+ });
332
+ return;
333
+ }
334
+ }
335
+ usedKeys.push({
336
+ key: id,
337
+ file
338
+ });
339
+ return;
340
+ }
341
+ if (attrs.length > 0) return;
342
+ if (!children?.length) return;
343
+ const built = buildTransMessage(code, children);
344
+ if (!built || !built.text.trim()) return;
345
+ tagged.push({
346
+ key: stableKey(built.text),
347
+ message: built.text,
348
+ params: built.params,
349
+ start: node.start,
350
+ end: node.end,
351
+ tagStart: nameNode.start,
352
+ tagEnd: nameNode.end,
353
+ file,
354
+ jsx: {
355
+ name: nameNode.name,
356
+ components: built.components
357
+ }
358
+ });
349
359
  }
350
360
  function explicitId(tag) {
351
- if (tag.type !== "CallExpression") return void 0;
352
- const callee = tag.callee;
353
- if (callee.type !== "MemberExpression" || callee.computed) return void 0;
354
- const prop = callee.property;
355
- if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
356
- const obj = callee.object;
357
- if (!isTReference(obj)) return void 0;
358
- const args = tag.arguments;
359
- const first = args[0];
360
- if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
361
- return { key: first.value, refStart: obj.start, refEnd: obj.end };
361
+ if (tag.type !== "CallExpression") return void 0;
362
+ const callee = tag.callee;
363
+ if (callee.type !== "MemberExpression" || callee.computed) return void 0;
364
+ const prop = callee.property;
365
+ if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
366
+ const obj = callee.object;
367
+ if (!isTReference(obj)) return void 0;
368
+ const args = tag.arguments;
369
+ const first = args[0];
370
+ if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
371
+ return {
372
+ key: first.value,
373
+ refStart: obj.start,
374
+ refEnd: obj.end
375
+ };
362
376
  }
363
377
  function buildTransMessage(code, children) {
364
- const params = [];
365
- const components = [];
366
- const takenParams = /* @__PURE__ */ new Map();
367
- const takenTags = /* @__PURE__ */ new Map();
368
- function walkChildren(nodes) {
369
- let text2 = "";
370
- for (const child of nodes) {
371
- if (child.type === "JSXText") {
372
- text2 += escapeText(cleanJsxText(child.value));
373
- } else if (child.type === "JSXExpressionContainer") {
374
- const expr = child.expression;
375
- if (expr.type === "JSXEmptyExpression") continue;
376
- if (expr.type === "StringLiteral") {
377
- text2 += escapeText(expr.value);
378
- continue;
379
- }
380
- if (expr.type === "TaggedTemplateExpression") return void 0;
381
- const source = code.slice(expr.start, expr.end);
382
- const name = uniqueName(deriveName(expr, params.length), source, takenParams);
383
- params.push({ name, start: expr.start, end: expr.end });
384
- text2 += `{${name}}`;
385
- } else if (child.type === "JSXElement") {
386
- const opening = child.openingElement;
387
- const nameNode = opening.name;
388
- if (nameNode.type !== "JSXIdentifier" || nameNode.name === "Trans") return void 0;
389
- const source = selfClosedSource(code, opening);
390
- const tag = uniqueName(nameNode.name.toLowerCase(), source, takenTags);
391
- if (!components.some((c) => c.name === tag)) components.push({ name: tag, source });
392
- if (!child.closingElement) {
393
- text2 += `<${tag}/>`;
394
- } else {
395
- const inner = walkChildren(child.children);
396
- if (inner === void 0) return void 0;
397
- text2 += `<${tag}>${inner}</${tag}>`;
398
- }
399
- } else {
400
- return void 0;
401
- }
402
- }
403
- return text2;
404
- }
405
- const text = walkChildren(children);
406
- if (text === void 0) return void 0;
407
- return { text, params, components };
378
+ const params = [];
379
+ const components = [];
380
+ const takenParams = /* @__PURE__ */ new Map();
381
+ const takenTags = /* @__PURE__ */ new Map();
382
+ function walkChildren(nodes) {
383
+ let text = "";
384
+ for (const child of nodes) if (child.type === "JSXText") text += escapeText(cleanJsxText(child.value));
385
+ else if (child.type === "JSXExpressionContainer") {
386
+ const expr = child.expression;
387
+ if (expr.type === "JSXEmptyExpression") continue;
388
+ if (expr.type === "StringLiteral") {
389
+ text += escapeText(expr.value);
390
+ continue;
391
+ }
392
+ if (expr.type === "TaggedTemplateExpression") return void 0;
393
+ const source = code.slice(expr.start, expr.end);
394
+ const name = uniqueName(deriveName(expr, params.length), source, takenParams);
395
+ params.push({
396
+ name,
397
+ start: expr.start,
398
+ end: expr.end
399
+ });
400
+ text += `{${name}}`;
401
+ } else if (child.type === "JSXElement") {
402
+ const opening = child.openingElement;
403
+ const nameNode = opening.name;
404
+ if (nameNode.type !== "JSXIdentifier" || nameNode.name === "Trans") return void 0;
405
+ const source = selfClosedSource(code, opening);
406
+ const tag = uniqueName(nameNode.name.toLowerCase(), source, takenTags);
407
+ if (!components.some((c) => c.name === tag)) components.push({
408
+ name: tag,
409
+ source
410
+ });
411
+ if (!child.closingElement) text += `<${tag}/>`;
412
+ else {
413
+ const inner = walkChildren(child.children);
414
+ if (inner === void 0) return void 0;
415
+ text += `<${tag}>${inner}</${tag}>`;
416
+ }
417
+ } else return;
418
+ return text;
419
+ }
420
+ const text = walkChildren(children);
421
+ if (text === void 0) return void 0;
422
+ return {
423
+ text,
424
+ params,
425
+ components
426
+ };
408
427
  }
409
428
  function cleanJsxText(raw) {
410
- const lines = raw.split(/\r\n|[\r\n]/);
411
- let out = "";
412
- for (let i = 0; i < lines.length; i++) {
413
- let line = lines[i].replace(/\t/g, " ");
414
- if (i !== 0) line = line.replace(/^ +/, "");
415
- if (i !== lines.length - 1) line = line.replace(/ +$/, "");
416
- if (!line) continue;
417
- if (out && i !== 0) out += " ";
418
- out += line;
419
- }
420
- return out;
429
+ const lines = raw.split(/\r\n|[\r\n]/);
430
+ let out = "";
431
+ for (let i = 0; i < lines.length; i++) {
432
+ let line = lines[i].replace(/\t/g, " ");
433
+ if (i !== 0) line = line.replace(/^ +/, "");
434
+ if (i !== lines.length - 1) line = line.replace(/ +$/, "");
435
+ if (!line) continue;
436
+ if (out && i !== 0) out += " ";
437
+ out += line;
438
+ }
439
+ return out;
421
440
  }
422
441
  function selfClosedSource(code, opening) {
423
- const src = code.slice(opening.start, opening.end);
424
- return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
442
+ const src = code.slice(opening.start, opening.end);
443
+ return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
425
444
  }
426
445
  function isTReference(node) {
427
- if (node.type === "Identifier") return node.name === "t";
428
- if (node.type === "MemberExpression" && !node.computed) {
429
- const prop = node.property;
430
- return prop.type === "Identifier" && prop.name === "t";
431
- }
432
- return false;
446
+ if (node.type === "Identifier") return node.name === "t";
447
+ if (node.type === "MemberExpression" && !node.computed) {
448
+ const prop = node.property;
449
+ return prop.type === "Identifier" && prop.name === "t";
450
+ }
451
+ return false;
433
452
  }
434
453
  function buildMessage(code, quasi) {
435
- const quasis = quasi.quasis;
436
- const expressions = quasi.expressions;
437
- const params = [];
438
- const taken = /* @__PURE__ */ new Map();
439
- let text = escapeText(cookedValue(quasis[0]));
440
- for (let i = 0; i < expressions.length; i++) {
441
- const expr = expressions[i];
442
- if (!expr) return void 0;
443
- const source = code.slice(expr.start, expr.end);
444
- const name = uniqueName(deriveName(expr, i), source, taken);
445
- params.push({ name, start: expr.start, end: expr.end });
446
- text += `{${name}}` + escapeText(cookedValue(quasis[i + 1]));
447
- }
448
- return { text, params };
454
+ const quasis = quasi.quasis;
455
+ const expressions = quasi.expressions;
456
+ const params = [];
457
+ const taken = /* @__PURE__ */ new Map();
458
+ let text = escapeText(cookedValue(quasis[0]));
459
+ for (let i = 0; i < expressions.length; i++) {
460
+ const expr = expressions[i];
461
+ if (!expr) return void 0;
462
+ const source = code.slice(expr.start, expr.end);
463
+ const name = uniqueName(deriveName(expr, i), source, taken);
464
+ params.push({
465
+ name,
466
+ start: expr.start,
467
+ end: expr.end
468
+ });
469
+ text += `{${name}}` + escapeText(cookedValue(quasis[i + 1]));
470
+ }
471
+ return {
472
+ text,
473
+ params
474
+ };
449
475
  }
450
476
  function cookedValue(element) {
451
- if (!element) return "";
452
- const value = element.value;
453
- return value.cooked ?? value.raw;
477
+ if (!element) return "";
478
+ const value = element.value;
479
+ return value.cooked ?? value.raw;
454
480
  }
455
481
  function escapeText(text) {
456
- return text.replace(/[{}]/g, (m) => m + m);
482
+ return text.replace(/[{}]/g, (m) => m + m);
457
483
  }
458
484
  function deriveName(expr, index) {
459
- if (expr.type === "Identifier") return expr.name;
460
- if (expr.type === "MemberExpression" && !expr.computed) {
461
- const prop = expr.property;
462
- if (prop.type === "Identifier") return prop.name;
463
- }
464
- return `_${index}`;
485
+ if (expr.type === "Identifier") return expr.name;
486
+ if (expr.type === "MemberExpression" && !expr.computed) {
487
+ const prop = expr.property;
488
+ if (prop.type === "Identifier") return prop.name;
489
+ }
490
+ return `_${index}`;
465
491
  }
466
492
  function uniqueName(base, source, taken) {
467
- let name = base;
468
- let n = 2;
469
- while (taken.has(name) && taken.get(name) !== source) {
470
- name = `${base}${n}`;
471
- n += 1;
472
- }
473
- taken.set(name, source);
474
- return name;
475
- }
476
- function walk(node, visit2) {
477
- visit2(node);
478
- for (const [key, value] of Object.entries(node)) {
479
- if (SKIP_KEYS.has(key)) continue;
480
- if (Array.isArray(value)) {
481
- for (const item of value) {
482
- if (isNode(item)) walk(item, visit2);
483
- }
484
- } else if (isNode(value)) {
485
- walk(value, visit2);
486
- }
487
- }
493
+ let name = base;
494
+ let n = 2;
495
+ while (taken.has(name) && taken.get(name) !== source) {
496
+ name = `${base}${n}`;
497
+ n += 1;
498
+ }
499
+ taken.set(name, source);
500
+ return name;
501
+ }
502
+ function walk(node, visit) {
503
+ visit(node);
504
+ for (const [key, value] of Object.entries(node)) {
505
+ if (SKIP_KEYS.has(key)) continue;
506
+ if (Array.isArray(value)) {
507
+ for (const item of value) if (isNode(item)) walk(item, visit);
508
+ } else if (isNode(value)) walk(value, visit);
509
+ }
488
510
  }
489
511
  function isNode(value) {
490
- return typeof value === "object" && value !== null && typeof value.type === "string";
512
+ return typeof value === "object" && value !== null && typeof value.type === "string";
491
513
  }
492
-
493
- // src/registry.ts
514
+ //#endregion
515
+ //#region src/registry.ts
494
516
  var MessageRegistry = class {
495
- files = /* @__PURE__ */ new Map();
496
- update(file, analysis) {
497
- this.files.set(file, analysis);
498
- }
499
- remove(file) {
500
- this.files.delete(file);
501
- }
502
- messages() {
503
- const out = /* @__PURE__ */ new Map();
504
- for (const analysis of this.files.values()) {
505
- for (const msg of analysis.tagged) {
506
- const existing = out.get(msg.key);
507
- if (existing) {
508
- if (existing.message !== msg.message) {
509
- console.warn(
510
- `[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} \u2014 second dropped.`
511
- );
512
- }
513
- continue;
514
- }
515
- out.set(msg.key, msg);
516
- }
517
- }
518
- return out;
519
- }
520
- usedKeys() {
521
- const out = /* @__PURE__ */ new Map();
522
- for (const analysis of this.files.values()) {
523
- for (const used of analysis.usedKeys) {
524
- const files = out.get(used.key) ?? [];
525
- if (!files.includes(used.file)) files.push(used.file);
526
- out.set(used.key, files);
527
- }
528
- }
529
- return out;
530
- }
517
+ files = /* @__PURE__ */ new Map();
518
+ update(file, analysis) {
519
+ this.files.set(file, analysis);
520
+ }
521
+ remove(file) {
522
+ this.files.delete(file);
523
+ }
524
+ messages() {
525
+ const out = /* @__PURE__ */ new Map();
526
+ for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
527
+ const existing = out.get(msg.key);
528
+ if (existing) {
529
+ if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} — second dropped.`);
530
+ continue;
531
+ }
532
+ out.set(msg.key, msg);
533
+ }
534
+ return out;
535
+ }
536
+ usedKeys() {
537
+ const out = /* @__PURE__ */ new Map();
538
+ for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
539
+ const files = out.get(used.key) ?? [];
540
+ if (!files.includes(used.file)) files.push(used.file);
541
+ out.set(used.key, files);
542
+ }
543
+ return out;
544
+ }
531
545
  };
532
-
533
- // src/extract.ts
546
+ //#endregion
547
+ //#region src/extract.ts
534
548
  async function extractProject(cfg) {
535
- const files = await glob(cfg.include, {
536
- cwd: cfg.root,
537
- ignore: cfg.exclude,
538
- absolute: true
539
- });
540
- const registry = new MessageRegistry();
541
- for (const file of files) {
542
- registry.update(file, analyze(readFileSync3(file, "utf8"), file));
543
- }
544
- return registry;
549
+ const files = await glob(cfg.include, {
550
+ cwd: cfg.root,
551
+ ignore: cfg.exclude,
552
+ absolute: true
553
+ });
554
+ const registry = new MessageRegistry();
555
+ for (const file of files) registry.update(file, analyze(readFileSync(file, "utf8"), file));
556
+ return registry;
545
557
  }
546
558
  function syncCatalogs(cfg, catalogs, registry) {
547
- const added = {};
548
- const source = catalogs[cfg.sourceLocale] ??= {};
549
- for (const [key, msg] of registry.messages()) {
550
- if (source[key] !== msg.message) {
551
- source[key] = msg.message;
552
- (added[cfg.sourceLocale] ??= []).push(key);
553
- }
554
- }
555
- const needed = Object.keys(source);
556
- for (const locale of cfg.locales) {
557
- if (locale === cfg.sourceLocale) continue;
558
- const catalog = catalogs[locale] ??= {};
559
- for (const key of needed) {
560
- if (catalog[key] === void 0) {
561
- catalog[key] = "";
562
- (added[locale] ??= []).push(key);
563
- }
564
- }
565
- }
566
- return { added };
559
+ const added = {};
560
+ const source = catalogs[cfg.sourceLocale] ??= {};
561
+ for (const [key, msg] of registry.messages()) if (source[key] !== msg.message) {
562
+ source[key] = msg.message;
563
+ (added[cfg.sourceLocale] ??= []).push(key);
564
+ }
565
+ const needed = Object.keys(source);
566
+ for (const locale of cfg.locales) {
567
+ if (locale === cfg.sourceLocale) continue;
568
+ const catalog = catalogs[locale] ??= {};
569
+ for (const key of needed) if (catalog[key] === void 0) {
570
+ catalog[key] = "";
571
+ (added[locale] ??= []).push(key);
572
+ }
573
+ }
574
+ return { added };
567
575
  }
568
576
  function pruneCatalogs(cfg, catalogs, registry) {
569
- const keep = /* @__PURE__ */ new Set([...registry.messages().keys(), ...registry.usedKeys().keys()]);
570
- const removed = {};
571
- for (const locale of cfg.locales) {
572
- const catalog = catalogs[locale];
573
- if (!catalog) continue;
574
- for (const key of Object.keys(catalog)) {
575
- if (!keep.has(key)) {
576
- delete catalog[key];
577
- (removed[locale] ??= []).push(key);
578
- }
579
- }
580
- }
581
- return removed;
577
+ const keep = /* @__PURE__ */ new Set([...registry.messages().keys(), ...registry.usedKeys().keys()]);
578
+ const removed = {};
579
+ for (const locale of cfg.locales) {
580
+ const catalog = catalogs[locale];
581
+ if (!catalog) continue;
582
+ for (const key of Object.keys(catalog)) if (!keep.has(key)) {
583
+ delete catalog[key];
584
+ (removed[locale] ??= []).push(key);
585
+ }
586
+ }
587
+ return removed;
582
588
  }
583
-
584
- // src/pseudo.ts
585
- var PSEUDO_LOCALE = "en-XA";
586
- var ACCENTS = {
587
- a: "\xE1",
588
- b: "\u0180",
589
- c: "\xE7",
590
- d: "\u0111",
591
- e: "\xE9",
592
- f: "\u0192",
593
- g: "\u011F",
594
- h: "\u0125",
595
- i: "\xED",
596
- j: "\u0135",
597
- k: "\u0137",
598
- l: "\u013A",
599
- m: "\u0271",
600
- n: "\xF1",
601
- o: "\xF3",
602
- p: "\xFE",
603
- q: "\u01EB",
604
- r: "\u0155",
605
- s: "\u0161",
606
- t: "\u0163",
607
- u: "\xFA",
608
- v: "\u1E7D",
609
- w: "\u0175",
610
- x: "\u1E8B",
611
- y: "\xFD",
612
- z: "\u017E",
613
- A: "\xC1",
614
- B: "\u0181",
615
- C: "\xC7",
616
- D: "\u0110",
617
- E: "\xC9",
618
- F: "\u0191",
619
- G: "\u011E",
620
- H: "\u0124",
621
- I: "\xCD",
622
- J: "\u0134",
623
- K: "\u0136",
624
- L: "\u0139",
625
- M: "\u1E3E",
626
- N: "\xD1",
627
- O: "\xD3",
628
- P: "\xDE",
629
- Q: "\u01EA",
630
- R: "\u0154",
631
- S: "\u0160",
632
- T: "\u0162",
633
- U: "\xDA",
634
- V: "\u1E7C",
635
- W: "\u0174",
636
- X: "\u1E8C",
637
- Y: "\xDD",
638
- Z: "\u017D"
589
+ //#endregion
590
+ //#region src/init.ts
591
+ const BUNDLERS = [
592
+ "vite",
593
+ "webpack",
594
+ "rollup",
595
+ "rspack",
596
+ "esbuild"
597
+ ];
598
+ function detectBundler(root) {
599
+ const path = join(root, "package.json");
600
+ if (!existsSync(path)) return void 0;
601
+ try {
602
+ const pkg = JSON.parse(readFileSync(path, "utf8"));
603
+ const deps = {
604
+ ...pkg.dependencies,
605
+ ...pkg.devDependencies
606
+ };
607
+ return BUNDLERS.find((name) => deps[name]);
608
+ } catch {
609
+ return;
610
+ }
611
+ }
612
+ function configSource(options, typescript) {
613
+ const fields = [` sourceLocale: '${options.sourceLocale ?? "en"}',`];
614
+ if (options.locales?.length) fields.push(` locales: [${options.locales.map((l) => `'${l}'`).join(", ")}],`);
615
+ if (options.dir) fields.push(` dir: '${options.dir}',`);
616
+ const body = `export default {\n${fields.join("\n")}\n}`;
617
+ if (typescript) return `import type { VerbalyConfig } from '@verbaly/compiler';\n\n${body} satisfies VerbalyConfig;\n`;
618
+ return `/** @type {import('@verbaly/compiler').VerbalyConfig} */\n${body};\n`;
619
+ }
620
+ function init(options = {}) {
621
+ const root = options.root ?? process.cwd();
622
+ const created = [];
623
+ const skipped = [];
624
+ const existing = findConfigFile(root);
625
+ const typescript = existsSync(join(root, "tsconfig.json"));
626
+ const configFile = existing ?? (typescript ? "verbaly.config.ts" : "verbaly.config.mjs");
627
+ if (existing) skipped.push(existing);
628
+ else {
629
+ writeFileSync(join(root, configFile), configSource(options, typescript));
630
+ created.push(configFile);
631
+ }
632
+ const dir = join(root, options.dir ?? "locales");
633
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
634
+ for (const locale of /* @__PURE__ */ new Set([options.sourceLocale ?? "en", ...options.locales ?? []])) {
635
+ const file = join(dir, `${locale}.json`);
636
+ const label = relative(root, file).replaceAll("\\", "/");
637
+ if (existsSync(file)) skipped.push(label);
638
+ else {
639
+ writeFileSync(file, "{}\n");
640
+ created.push(label);
641
+ }
642
+ }
643
+ const bundler = detectBundler(root);
644
+ const next = [];
645
+ if (bundler === "vite") next.push("install the plugin: pnpm add -D @verbaly/vite", "add verbaly() to the plugins in vite.config");
646
+ else if (bundler) next.push("install the plugin: pnpm add -D @verbaly/unplugin", `add the verbaly ${bundler} plugin to your build config`);
647
+ else next.push("run \"verbaly extract\" after writing your first t`…` message");
648
+ return {
649
+ created,
650
+ skipped,
651
+ bundler,
652
+ configFile,
653
+ next
654
+ };
655
+ }
656
+ //#endregion
657
+ //#region src/doctor.ts
658
+ const PREVIEW = 5;
659
+ async function doctor(cfg) {
660
+ const entries = [];
661
+ const ok = (check, message) => entries.push({
662
+ level: "ok",
663
+ check,
664
+ message
665
+ });
666
+ const warn = (check, message, fix) => entries.push({
667
+ level: "warn",
668
+ check,
669
+ message,
670
+ fix
671
+ });
672
+ const error = (check, message, fix) => entries.push({
673
+ level: "error",
674
+ check,
675
+ message,
676
+ fix
677
+ });
678
+ const rel = (path) => relative(cfg.root, path).replaceAll("\\", "/");
679
+ const configFile = findConfigFile(cfg.root);
680
+ if (configFile) ok("config", `${configFile} found`);
681
+ else warn("config", "no config file — running on defaults", "run `npx verbaly init`");
682
+ const catalogs = {};
683
+ let catalogsHealthy = true;
684
+ if (!existsSync(cfg.dir)) {
685
+ catalogsHealthy = false;
686
+ error("catalogs", `catalogs directory ${rel(cfg.dir)}/ does not exist`, "run `npx verbaly init` to scaffold it");
687
+ } else {
688
+ for (const locale of cfg.locales) {
689
+ const file = join(cfg.dir, `${locale}.json`);
690
+ if (!existsSync(file)) {
691
+ catalogsHealthy = false;
692
+ error(`locale ${locale}`, `${rel(file)} is missing`, "run `npx verbaly extract` to create it");
693
+ continue;
694
+ }
695
+ try {
696
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
697
+ const bad = Object.entries(parsed).find(([, value]) => typeof value !== "string");
698
+ if (bad) {
699
+ catalogsHealthy = false;
700
+ error(`locale ${locale}`, `${rel(file)} has a non-string value at "${bad[0]}"`, "catalogs are flat key → string JSON; fix the value");
701
+ } else catalogs[locale] = parsed;
702
+ } catch {
703
+ catalogsHealthy = false;
704
+ error(`locale ${locale}`, `${rel(file)} is not valid JSON`, "repair the file (or delete it and run `npx verbaly extract`)");
705
+ }
706
+ }
707
+ if (catalogsHealthy) ok("catalogs", `${cfg.locales.length} locales (${cfg.locales.join(", ")}) in ${rel(cfg.dir)}/`);
708
+ }
709
+ const source = catalogs[cfg.sourceLocale];
710
+ if (source && Object.keys(source).length === 0) warn("source", `source catalog ${cfg.sourceLocale}.json is empty`, "write your first t`…` message and run `npx verbaly extract`");
711
+ const deps = readDeps(cfg.root);
712
+ const bundler = detectBundler(cfg.root);
713
+ const wired = deps["@verbaly/vite"] || deps["@verbaly/unplugin"];
714
+ if (!bundler) ok("plugin", "no bundler detected — CLI flow (extract/check) applies");
715
+ else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
716
+ 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");
717
+ 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`);
718
+ if (source) {
719
+ const dtsPath = join(cfg.root, "verbaly.d.ts");
720
+ if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
721
+ else if (readFileSync(dtsPath, "utf8") !== generateDts(new Map(Object.entries(source)))) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
722
+ else ok("types", "verbaly.d.ts is up to date");
723
+ }
724
+ const registry = await extractProject(cfg);
725
+ if (source) {
726
+ const extracted = registry.messages();
727
+ const used = registry.usedKeys();
728
+ const orphans = Object.keys(source).filter((key) => !extracted.has(key) && !used.has(key));
729
+ if (orphans.length > 0) warn("orphans", `${orphans.length} catalog ${orphans.length === 1 ? "key is" : "keys are"} no longer referenced (${preview(orphans)})`, "run `npx verbaly extract --prune` to drop them");
730
+ else ok("orphans", "no orphan keys");
731
+ }
732
+ if (catalogsHealthy) {
733
+ const result = check(cfg, catalogs, registry);
734
+ if (result.unknown.length > 0) error("keys", `${result.unknown.length} unknown ${result.unknown.length === 1 ? "key" : "keys"} used in code (${preview(result.unknown.map((u) => u.key))})`, "fix the key or add it to the catalogs — `npx verbaly check` for details");
735
+ if (result.missing.length > 0) {
736
+ const locales = [...new Set(result.missing.map((m) => m.locale))];
737
+ warn("translations", `${result.missing.length} missing ${result.missing.length === 1 ? "translation" : "translations"} (${locales.join(", ")})`, "run `npx verbaly translate` or fill the catalogs — `npx verbaly check` for details");
738
+ }
739
+ if (result.ok) ok("translations", "all translations complete");
740
+ }
741
+ return {
742
+ ok: entries.every((entry) => entry.level !== "error"),
743
+ entries
744
+ };
745
+ }
746
+ function preview(keys) {
747
+ const head = keys.slice(0, PREVIEW).join(", ");
748
+ return keys.length > PREVIEW ? `${head}, …` : head;
749
+ }
750
+ function readDeps(root) {
751
+ try {
752
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
753
+ return {
754
+ ...pkg.dependencies,
755
+ ...pkg.devDependencies
756
+ };
757
+ } catch {
758
+ return {};
759
+ }
760
+ }
761
+ //#endregion
762
+ //#region src/pseudo.ts
763
+ const PSEUDO_LOCALE = "en-XA";
764
+ const ACCENTS = {
765
+ a: "á",
766
+ b: "ƀ",
767
+ c: "ç",
768
+ d: "đ",
769
+ e: "é",
770
+ f: "ƒ",
771
+ g: "ğ",
772
+ h: "ĥ",
773
+ i: "í",
774
+ j: "ĵ",
775
+ k: "ķ",
776
+ l: "ĺ",
777
+ m: "ɱ",
778
+ n: "ñ",
779
+ o: "ó",
780
+ p: "þ",
781
+ q: "ǫ",
782
+ r: "ŕ",
783
+ s: "š",
784
+ t: "ţ",
785
+ u: "ú",
786
+ v: "ṽ",
787
+ w: "ŵ",
788
+ x: "ẋ",
789
+ y: "ý",
790
+ z: "ž",
791
+ A: "Á",
792
+ B: "Ɓ",
793
+ C: "Ç",
794
+ D: "Đ",
795
+ E: "É",
796
+ F: "Ƒ",
797
+ G: "Ğ",
798
+ H: "Ĥ",
799
+ I: "Í",
800
+ J: "Ĵ",
801
+ K: "Ķ",
802
+ L: "Ĺ",
803
+ M: "Ḿ",
804
+ N: "Ñ",
805
+ O: "Ó",
806
+ P: "Þ",
807
+ Q: "Ǫ",
808
+ R: "Ŕ",
809
+ S: "Š",
810
+ T: "Ţ",
811
+ U: "Ú",
812
+ V: "Ṽ",
813
+ W: "Ŵ",
814
+ X: "Ẍ",
815
+ Y: "Ý",
816
+ Z: "Ž"
639
817
  };
640
- var TAG_AT = /<\/?[a-zA-Z][\w-]*\/?>/y;
818
+ const TAG_AT = /<\/?[a-zA-Z][\w-]*\/?>/y;
641
819
  function pseudoLocalize(message) {
642
- let out = "";
643
- let letters = 0;
644
- let i = 0;
645
- while (i < message.length) {
646
- const two = message.slice(i, i + 2);
647
- if (two === "{{" || two === "}}" || two === "||" || two === "##") {
648
- out += two;
649
- i += 2;
650
- continue;
651
- }
652
- const ch = message[i];
653
- if (ch === "{") {
654
- const end = matchBrace(message, i);
655
- out += message.slice(i, end);
656
- i = end;
657
- continue;
658
- }
659
- if (ch === "<") {
660
- TAG_AT.lastIndex = i;
661
- const m = TAG_AT.exec(message);
662
- if (m) {
663
- out += m[0];
664
- i += m[0].length;
665
- continue;
666
- }
667
- }
668
- const mapped = ACCENTS[ch];
669
- if (mapped) {
670
- out += mapped;
671
- letters += 1;
672
- } else {
673
- out += ch;
674
- }
675
- i += 1;
676
- }
677
- const pad = "~".repeat(Math.ceil(letters / 3));
678
- return `\u27E6${out}${pad ? " " + pad : ""}\u27E7`;
820
+ let out = "";
821
+ let letters = 0;
822
+ let i = 0;
823
+ while (i < message.length) {
824
+ const two = message.slice(i, i + 2);
825
+ if (two === "{{" || two === "}}" || two === "||" || two === "##") {
826
+ out += two;
827
+ i += 2;
828
+ continue;
829
+ }
830
+ const ch = message[i];
831
+ if (ch === "{") {
832
+ const end = matchBrace(message, i);
833
+ out += message.slice(i, end);
834
+ i = end;
835
+ continue;
836
+ }
837
+ if (ch === "<") {
838
+ TAG_AT.lastIndex = i;
839
+ const m = TAG_AT.exec(message);
840
+ if (m) {
841
+ out += m[0];
842
+ i += m[0].length;
843
+ continue;
844
+ }
845
+ }
846
+ const mapped = ACCENTS[ch];
847
+ if (mapped) {
848
+ out += mapped;
849
+ letters += 1;
850
+ } else out += ch;
851
+ i += 1;
852
+ }
853
+ const pad = "~".repeat(Math.ceil(letters / 3));
854
+ return `⟦${out}${pad ? " " + pad : ""}⟧`;
679
855
  }
680
856
  function matchBrace(message, start) {
681
- let depth = 0;
682
- for (let i = start; i < message.length; i++) {
683
- const two = message.slice(i, i + 2);
684
- if (two === "{{" || two === "}}") {
685
- i += 1;
686
- continue;
687
- }
688
- const ch = message[i];
689
- if (ch === "{") depth += 1;
690
- else if (ch === "}") {
691
- depth -= 1;
692
- if (depth === 0) return i + 1;
693
- }
694
- }
695
- return message.length;
857
+ let depth = 0;
858
+ for (let i = start; i < message.length; i++) {
859
+ const two = message.slice(i, i + 2);
860
+ if (two === "{{" || two === "}}") {
861
+ i += 1;
862
+ continue;
863
+ }
864
+ const ch = message[i];
865
+ if (ch === "{") depth += 1;
866
+ else if (ch === "}") {
867
+ depth -= 1;
868
+ if (depth === 0) return i + 1;
869
+ }
870
+ }
871
+ return message.length;
696
872
  }
697
873
  function pseudoCatalogs(cfg, catalogs, locale = PSEUDO_LOCALE) {
698
- const source = catalogs[cfg.sourceLocale] ?? {};
699
- const target = {};
700
- for (const [key, msg] of Object.entries(source)) {
701
- target[key] = msg ? pseudoLocalize(msg) : "";
702
- }
703
- catalogs[locale] = target;
704
- return Object.keys(target).filter((key) => target[key]);
874
+ const source = catalogs[cfg.sourceLocale] ?? {};
875
+ const target = {};
876
+ for (const [key, msg] of Object.entries(source)) target[key] = msg ? pseudoLocalize(msg) : "";
877
+ catalogs[locale] = target;
878
+ return Object.keys(target).filter((key) => target[key]);
705
879
  }
706
-
707
- // src/render.ts
708
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
709
- import { dirname, join as join4, relative } from "path";
710
- import MagicString from "magic-string";
711
- import { glob as glob2 } from "tinyglobby";
712
- import {
713
- createVerbaly,
714
- parseTags,
715
- RICH_TAGS,
716
- safeHref
717
- } from "verbaly";
718
- var VOID_TAGS = /* @__PURE__ */ new Set([
719
- "area",
720
- "base",
721
- "br",
722
- "col",
723
- "embed",
724
- "hr",
725
- "img",
726
- "input",
727
- "link",
728
- "meta",
729
- "param",
730
- "source",
731
- "track",
732
- "wbr"
880
+ //#endregion
881
+ //#region src/render.ts
882
+ const VOID_TAGS = /* @__PURE__ */ new Set([
883
+ "area",
884
+ "base",
885
+ "br",
886
+ "col",
887
+ "embed",
888
+ "hr",
889
+ "img",
890
+ "input",
891
+ "link",
892
+ "meta",
893
+ "param",
894
+ "source",
895
+ "track",
896
+ "wbr"
733
897
  ]);
734
- var START_TAG = /<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^"'>])*)>/g;
735
- var ATTR = /([^\s=/"'<>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
898
+ const START_TAG = /<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^"'>])*)>/g;
899
+ const ATTR = /([^\s=/"'<>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
736
900
  function renderHtml(html, options) {
737
- const attr = options.attribute ?? "data-verbaly";
738
- const argsAttr = `${attr}-args`;
739
- const attrsAttr = `${attr}-attr`;
740
- const richAttr = `${attr}-rich`;
741
- const linksAttr = `${attr}-links`;
742
- const richTags = new Set(options.richTags ?? RICH_TAGS);
743
- const globalLinks = options.richLinks;
744
- const sourceLocale = options.sourceLocale ?? "en";
745
- const messages = {};
746
- for (const [locale, catalog] of Object.entries(options.catalogs)) {
747
- const clean = {};
748
- for (const [key, msg] of Object.entries(catalog)) if (msg) clean[key] = msg;
749
- messages[locale] = clean;
750
- }
751
- const v = createVerbaly({ locale: options.locale, fallback: sourceLocale, messages });
752
- const t = v.t;
753
- const ms = new MagicString(html);
754
- const missing = /* @__PURE__ */ new Set();
755
- const skip = protectedRanges(html);
756
- const inSkip = (index) => skip.some(([from, to]) => index >= from && index < to);
757
- START_TAG.lastIndex = 0;
758
- let m;
759
- while ((m = START_TAG.exec(html)) !== null) {
760
- if (inSkip(m.index)) continue;
761
- const [full, rawName, attrChunk] = m;
762
- const tagName = rawName.toLowerCase();
763
- const openEnd = m.index + full.length;
764
- const chunkStart = m.index + 1 + rawName.length;
765
- if (tagName === "html" && options.setLang !== false) {
766
- setAttribute(ms, html, chunkStart, openEnd, attrChunk, "lang", options.locale);
767
- continue;
768
- }
769
- const attrs = parseAttrs(attrChunk);
770
- const key = attrs.get(attr);
771
- const attrMapRaw = attrs.get(attrsAttr);
772
- if (key === void 0 && attrMapRaw === void 0) continue;
773
- const args = parseArgs(attrs.get(argsAttr));
774
- if (key) {
775
- if (!v.has(key)) {
776
- missing.add(key);
777
- } else if (!VOID_TAGS.has(tagName) && !attrChunk.trimEnd().endsWith("/")) {
778
- const close = findClose(html, tagName, openEnd, inSkip);
779
- if (close) {
780
- const text = t(key, args);
781
- const own = parseArgs(attrs.get(linksAttr));
782
- const links = own ? globalLinks ? { ...globalLinks, ...own } : own : globalLinks;
783
- const content = attrs.has(richAttr) ? richToHtml(parseTags(text), richTags, links) : escapeHtml(text);
784
- if (html.slice(openEnd, close.contentEnd) !== content) {
785
- if (openEnd === close.contentEnd) ms.appendLeft(openEnd, content);
786
- else ms.overwrite(openEnd, close.contentEnd, content);
787
- }
788
- }
789
- }
790
- }
791
- if (attrMapRaw !== void 0) {
792
- const map = parseArgs(attrMapRaw);
793
- if (map) {
794
- for (const [name, attrKey] of Object.entries(map)) {
795
- if (typeof attrKey !== "string" || name.toLowerCase().startsWith("on")) continue;
796
- if (!v.has(attrKey)) {
797
- missing.add(attrKey);
798
- continue;
799
- }
800
- setAttribute(ms, html, chunkStart, openEnd, attrChunk, name, t(attrKey, args));
801
- }
802
- }
803
- }
804
- }
805
- return { html: ms.toString(), missing: [...missing] };
901
+ const attr = options.attribute ?? "data-verbaly";
902
+ const argsAttr = `${attr}-args`;
903
+ const attrsAttr = `${attr}-attr`;
904
+ const richAttr = `${attr}-rich`;
905
+ const linksAttr = `${attr}-links`;
906
+ const richTags = new Set(options.richTags ?? RICH_TAGS);
907
+ const globalLinks = options.richLinks;
908
+ const sourceLocale = options.sourceLocale ?? "en";
909
+ const messages = {};
910
+ for (const [locale, catalog] of Object.entries(options.catalogs)) {
911
+ const clean = {};
912
+ for (const [key, msg] of Object.entries(catalog)) if (msg) clean[key] = msg;
913
+ messages[locale] = clean;
914
+ }
915
+ const v = createVerbaly({
916
+ locale: options.locale,
917
+ fallback: sourceLocale,
918
+ messages
919
+ });
920
+ const t = v.t;
921
+ const ms = new MagicString(html);
922
+ const missing = /* @__PURE__ */ new Set();
923
+ const skip = protectedRanges(html);
924
+ const inSkip = (index) => skip.some(([from, to]) => index >= from && index < to);
925
+ START_TAG.lastIndex = 0;
926
+ let m;
927
+ while ((m = START_TAG.exec(html)) !== null) {
928
+ if (inSkip(m.index)) continue;
929
+ const [full, rawName, attrChunk] = m;
930
+ const tagName = rawName.toLowerCase();
931
+ const openEnd = m.index + full.length;
932
+ const chunkStart = m.index + 1 + rawName.length;
933
+ if (tagName === "html" && options.setLang !== false) {
934
+ setAttribute(ms, html, chunkStart, openEnd, attrChunk, "lang", options.locale);
935
+ continue;
936
+ }
937
+ const attrs = parseAttrs(attrChunk);
938
+ const key = attrs.get(attr);
939
+ const attrMapRaw = attrs.get(attrsAttr);
940
+ if (key === void 0 && attrMapRaw === void 0) continue;
941
+ const args = parseArgs$1(attrs.get(argsAttr));
942
+ if (key) {
943
+ if (!v.has(key)) missing.add(key);
944
+ else if (!VOID_TAGS.has(tagName) && !attrChunk.trimEnd().endsWith("/")) {
945
+ const close = findClose(html, tagName, openEnd, inSkip);
946
+ if (close) {
947
+ const text = t(key, args);
948
+ const own = parseArgs$1(attrs.get(linksAttr));
949
+ const links = own ? globalLinks ? {
950
+ ...globalLinks,
951
+ ...own
952
+ } : own : globalLinks;
953
+ const content = attrs.has(richAttr) ? richToHtml(parseTags(text), richTags, links) : escapeHtml(text);
954
+ if (html.slice(openEnd, close.contentEnd) !== content) if (openEnd === close.contentEnd) ms.appendLeft(openEnd, content);
955
+ else ms.overwrite(openEnd, close.contentEnd, content);
956
+ }
957
+ }
958
+ }
959
+ if (attrMapRaw !== void 0) {
960
+ const map = parseArgs$1(attrMapRaw);
961
+ if (map) for (const [name, attrKey] of Object.entries(map)) {
962
+ if (typeof attrKey !== "string" || name.toLowerCase().startsWith("on")) continue;
963
+ if (!v.has(attrKey)) {
964
+ missing.add(attrKey);
965
+ continue;
966
+ }
967
+ setAttribute(ms, html, chunkStart, openEnd, attrChunk, name, t(attrKey, args));
968
+ }
969
+ }
970
+ }
971
+ return {
972
+ html: ms.toString(),
973
+ missing: [...missing]
974
+ };
806
975
  }
807
976
  async function renderSite(cfg, options = {}) {
808
- const site = join4(cfg.root, options.site ?? "dist");
809
- const locales = options.locales ?? cfg.locales;
810
- const catalogs = loadCatalogs(cfg);
811
- const files = await glob2("**/*.html", {
812
- cwd: site,
813
- absolute: true,
814
- ignore: locales.map((locale) => `${locale}/**`)
815
- });
816
- const missing = {};
817
- for (const file of files) {
818
- const html = readFileSync4(file, "utf8");
819
- const rel = relative(site, file);
820
- for (const locale of locales) {
821
- const result = renderHtml(html, {
822
- locale,
823
- catalogs,
824
- sourceLocale: cfg.sourceLocale,
825
- attribute: options.attribute,
826
- richTags: options.richTags,
827
- richLinks: options.richLinks ?? cfg.render.links
828
- });
829
- for (const key of result.missing) {
830
- const list = missing[locale] ??= [];
831
- if (!list.includes(key)) list.push(key);
832
- }
833
- const out = locale === cfg.sourceLocale ? file : join4(site, locale, rel);
834
- mkdirSync2(dirname(out), { recursive: true });
835
- writeFileSync3(out, result.html);
836
- }
837
- }
838
- return { files: files.length, locales: [...locales], missing };
977
+ const site = join(cfg.root, options.site ?? "dist");
978
+ const locales = options.locales ?? cfg.locales;
979
+ const catalogs = loadCatalogs(cfg);
980
+ const files = await glob("**/*.html", {
981
+ cwd: site,
982
+ absolute: true,
983
+ ignore: locales.map((locale) => `${locale}/**`)
984
+ });
985
+ const missing = {};
986
+ for (const file of files) {
987
+ const html = readFileSync(file, "utf8");
988
+ const rel = relative(site, file);
989
+ for (const locale of locales) {
990
+ const result = renderHtml(html, {
991
+ locale,
992
+ catalogs,
993
+ sourceLocale: cfg.sourceLocale,
994
+ attribute: options.attribute,
995
+ richTags: options.richTags,
996
+ richLinks: options.richLinks ?? cfg.render.links
997
+ });
998
+ for (const key of result.missing) {
999
+ const list = missing[locale] ??= [];
1000
+ if (!list.includes(key)) list.push(key);
1001
+ }
1002
+ const out = locale === cfg.sourceLocale ? file : join(site, locale, rel);
1003
+ mkdirSync(dirname(out), { recursive: true });
1004
+ writeFileSync(out, result.html);
1005
+ }
1006
+ }
1007
+ return {
1008
+ files: files.length,
1009
+ locales: [...locales],
1010
+ missing
1011
+ };
839
1012
  }
840
1013
  function parseAttrs(chunk) {
841
- const attrs = /* @__PURE__ */ new Map();
842
- ATTR.lastIndex = 0;
843
- let m;
844
- while ((m = ATTR.exec(chunk)) !== null) {
845
- attrs.set(m[1].toLowerCase(), m[2] ?? m[3] ?? m[4] ?? "");
846
- }
847
- return attrs;
848
- }
849
- function parseArgs(raw) {
850
- if (!raw) return void 0;
851
- try {
852
- return JSON.parse(decodeEntities(raw));
853
- } catch {
854
- console.warn(`[verbaly] invalid args JSON: ${raw}`);
855
- return void 0;
856
- }
1014
+ const attrs = /* @__PURE__ */ new Map();
1015
+ ATTR.lastIndex = 0;
1016
+ let m;
1017
+ while ((m = ATTR.exec(chunk)) !== null) attrs.set(m[1].toLowerCase(), m[2] ?? m[3] ?? m[4] ?? "");
1018
+ return attrs;
1019
+ }
1020
+ function parseArgs$1(raw) {
1021
+ if (!raw) return void 0;
1022
+ try {
1023
+ return JSON.parse(decodeEntities(raw));
1024
+ } catch {
1025
+ console.warn(`[verbaly] invalid args JSON: ${raw}`);
1026
+ return;
1027
+ }
857
1028
  }
858
1029
  function protectedRanges(html) {
859
- const ranges = [];
860
- const re = /<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>/gi;
861
- let m;
862
- while ((m = re.exec(html)) !== null) {
863
- const openEnd = m[0].startsWith("<!--") ? m.index : html.indexOf(">", m.index) + 1;
864
- ranges.push([openEnd, m.index + m[0].length]);
865
- }
866
- return ranges;
1030
+ const ranges = [];
1031
+ const re = /<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>/gi;
1032
+ let m;
1033
+ while ((m = re.exec(html)) !== null) {
1034
+ const openEnd = m[0].startsWith("<!--") ? m.index : html.indexOf(">", m.index) + 1;
1035
+ ranges.push([openEnd, m.index + m[0].length]);
1036
+ }
1037
+ return ranges;
867
1038
  }
868
1039
  function findClose(html, tagName, from, inSkip) {
869
- const re = new RegExp(
870
- `<${tagName}(?=[\\s/>])(?:"[^"]*"|'[^']*'|[^"'>])*>|</${tagName}\\s*>`,
871
- "gi"
872
- );
873
- re.lastIndex = from;
874
- let depth = 1;
875
- let m;
876
- while ((m = re.exec(html)) !== null) {
877
- if (inSkip(m.index)) continue;
878
- if (m[0][1] === "/") {
879
- depth -= 1;
880
- if (depth === 0) return { contentEnd: m.index };
881
- } else if (!m[0].endsWith("/>")) {
882
- depth += 1;
883
- }
884
- }
885
- return null;
1040
+ const re = new RegExp(`<${tagName}(?=[\\s/>])(?:"[^"]*"|'[^']*'|[^"'>])*>|</${tagName}\\s*>`, "gi");
1041
+ re.lastIndex = from;
1042
+ let depth = 1;
1043
+ let m;
1044
+ while ((m = re.exec(html)) !== null) {
1045
+ if (inSkip(m.index)) continue;
1046
+ if (m[0][1] === "/") {
1047
+ depth -= 1;
1048
+ if (depth === 0) return { contentEnd: m.index };
1049
+ } else if (!m[0].endsWith("/>")) depth += 1;
1050
+ }
1051
+ return null;
886
1052
  }
887
1053
  function setAttribute(ms, html, chunkStart, openEnd, attrChunk, name, value) {
888
- const escaped = escapeAttr(value);
889
- const existing = new RegExp(`(\\s${name}\\s*=\\s*)("[^"]*"|'[^']*'|[^\\s>]+)`, "i").exec(
890
- attrChunk
891
- );
892
- if (existing) {
893
- const valueStart = chunkStart + existing.index + existing[1].length;
894
- const valueEnd = valueStart + existing[2].length;
895
- if (html.slice(valueStart, valueEnd) !== `"${escaped}"`) {
896
- ms.overwrite(valueStart, valueEnd, `"${escaped}"`);
897
- }
898
- return;
899
- }
900
- const selfClosing = html.slice(openEnd - 2, openEnd) === "/>";
901
- const insertAt = openEnd - (selfClosing ? 2 : 1);
902
- ms.appendLeft(insertAt, ` ${name}="${escaped}"`);
1054
+ const escaped = escapeAttr(value);
1055
+ const existing = new RegExp(`(\\s${name}\\s*=\\s*)("[^"]*"|'[^']*'|[^\\s>]+)`, "i").exec(attrChunk);
1056
+ if (existing) {
1057
+ const valueStart = chunkStart + existing.index + existing[1].length;
1058
+ const valueEnd = valueStart + existing[2].length;
1059
+ if (html.slice(valueStart, valueEnd) !== `"${escaped}"`) ms.overwrite(valueStart, valueEnd, `"${escaped}"`);
1060
+ return;
1061
+ }
1062
+ const insertAt = openEnd - (html.slice(openEnd - 2, openEnd) === "/>" ? 2 : 1);
1063
+ ms.appendLeft(insertAt, ` ${name}="${escaped}"`);
903
1064
  }
904
1065
  function richToHtml(nodes, allowed, links) {
905
- let out = "";
906
- for (const node of nodes) {
907
- if (typeof node === "string") {
908
- out += escapeHtml(node);
909
- } else if (links?.[node.name] !== void 0) {
910
- const link = links[node.name];
911
- const { href, target, rel } = typeof link === "string" ? { href: link } : link;
912
- const safe = safeHref(href);
913
- let attrs = safe !== void 0 ? ` href="${escapeAttr(safe)}"` : "";
914
- if (target) attrs += ` target="${escapeAttr(target)}"`;
915
- if (rel) attrs += ` rel="${escapeAttr(rel)}"`;
916
- out += `<a${attrs}>${richToHtml(node.children, allowed, links)}</a>`;
917
- } else if (allowed.has(node.name)) {
918
- out += `<${node.name}>${richToHtml(node.children, allowed, links)}</${node.name}>`;
919
- } else {
920
- out += richToHtml(node.children, allowed, links);
921
- }
922
- }
923
- return out;
1066
+ let out = "";
1067
+ for (const node of nodes) if (typeof node === "string") out += escapeHtml(node);
1068
+ else if (links?.[node.name] !== void 0) {
1069
+ const link = links[node.name];
1070
+ const { href, target, rel } = typeof link === "string" ? { href: link } : link;
1071
+ const safe = safeHref(href);
1072
+ let attrs = safe !== void 0 ? ` href="${escapeAttr(safe)}"` : "";
1073
+ if (target) attrs += ` target="${escapeAttr(target)}"`;
1074
+ if (rel) attrs += ` rel="${escapeAttr(rel)}"`;
1075
+ out += `<a${attrs}>${richToHtml(node.children, allowed, links)}</a>`;
1076
+ } else if (allowed.has(node.name)) out += `<${node.name}>${richToHtml(node.children, allowed, links)}</${node.name}>`;
1077
+ else out += richToHtml(node.children, allowed, links);
1078
+ return out;
924
1079
  }
925
1080
  function escapeHtml(text) {
926
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1081
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
927
1082
  }
928
1083
  function escapeAttr(text) {
929
- return escapeHtml(text).replace(/"/g, "&quot;");
1084
+ return escapeHtml(text).replace(/"/g, "&quot;");
930
1085
  }
931
1086
  function decodeEntities(text) {
932
- return text.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
1087
+ return text.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
933
1088
  }
934
-
935
- // src/translate.ts
1089
+ //#endregion
1090
+ //#region src/translate.ts
936
1091
  async function translateCatalogs(cfg, catalogs, provider, options = {}) {
937
- const batchSize = options.batchSize ?? 20;
938
- const targets = (options.locales ?? cfg.locales).filter(
939
- (locale) => locale !== cfg.sourceLocale && cfg.locales.includes(locale)
940
- );
941
- const source = catalogs[cfg.sourceLocale] ?? {};
942
- const result = { translated: {}, invalid: {}, pending: {} };
943
- for (const locale of targets) {
944
- const catalog = catalogs[locale] ??= {};
945
- const missing = Object.keys(source).filter((key) => source[key] && !catalog[key]);
946
- if (missing.length === 0) continue;
947
- if (options.dryRun) {
948
- result.pending[locale] = missing;
949
- continue;
950
- }
951
- for (let i = 0; i < missing.length; i += batchSize) {
952
- const keys = missing.slice(i, i + batchSize);
953
- const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
954
- const out = await provider({
955
- sourceLocale: cfg.sourceLocale,
956
- targetLocale: locale,
957
- messages
958
- });
959
- for (const key of keys) {
960
- const text = out[key];
961
- if (typeof text === "string" && text.trim() && structureMatches(source[key], text)) {
962
- catalog[key] = text;
963
- (result.translated[locale] ??= []).push(key);
964
- } else {
965
- (result.invalid[locale] ??= []).push(key);
966
- }
967
- }
968
- }
969
- }
970
- return result;
1092
+ const batchSize = options.batchSize ?? 20;
1093
+ const targets = (options.locales ?? cfg.locales).filter((locale) => locale !== cfg.sourceLocale && cfg.locales.includes(locale));
1094
+ const source = catalogs[cfg.sourceLocale] ?? {};
1095
+ const result = {
1096
+ translated: {},
1097
+ invalid: {},
1098
+ pending: {}
1099
+ };
1100
+ for (const locale of targets) {
1101
+ const catalog = catalogs[locale] ??= {};
1102
+ const missing = Object.keys(source).filter((key) => source[key] && !catalog[key]);
1103
+ if (missing.length === 0) continue;
1104
+ if (options.dryRun) {
1105
+ result.pending[locale] = missing;
1106
+ continue;
1107
+ }
1108
+ for (let i = 0; i < missing.length; i += batchSize) {
1109
+ const keys = missing.slice(i, i + batchSize);
1110
+ const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
1111
+ const out = await provider({
1112
+ sourceLocale: cfg.sourceLocale,
1113
+ targetLocale: locale,
1114
+ messages
1115
+ });
1116
+ for (const key of keys) {
1117
+ const text = out[key];
1118
+ if (typeof text === "string" && text.trim() && structureMatches(source[key], text)) {
1119
+ catalog[key] = text;
1120
+ (result.translated[locale] ??= []).push(key);
1121
+ } else (result.invalid[locale] ??= []).push(key);
1122
+ }
1123
+ }
1124
+ }
1125
+ return result;
971
1126
  }
972
1127
  function structureMatches(source, translated) {
973
- return sameMembers(paramNames(source), paramNames(translated)) && sameMembers(tagTokens(source), tagTokens(translated));
1128
+ return sameMembers(paramNames(source), paramNames(translated)) && sameMembers(tagTokens(source), tagTokens(translated));
974
1129
  }
975
1130
  function paramNames(message) {
976
- try {
977
- return [...collectParams(message).keys()].sort();
978
- } catch {
979
- return ["\0invalid"];
980
- }
1131
+ try {
1132
+ return [...collectParams(message).keys()].sort();
1133
+ } catch {
1134
+ return ["\0invalid"];
1135
+ }
981
1136
  }
982
- var TAG = /<(\/?)([a-zA-Z][\w-]*)(\/?)>/g;
1137
+ const TAG = /<(\/?)([a-zA-Z][\w-]*)(\/?)>/g;
983
1138
  function tagTokens(message) {
984
- const out = [];
985
- for (const match of message.matchAll(TAG)) {
986
- out.push(`${match[1]}${match[2]}${match[3]}`);
987
- }
988
- return out.sort();
1139
+ const out = [];
1140
+ for (const match of message.matchAll(TAG)) out.push(`${match[1]}${match[2]}${match[3]}`);
1141
+ return out.sort();
989
1142
  }
990
1143
  function sameMembers(a, b) {
991
- return a.length === b.length && a.every((value, i) => value === b[i]);
1144
+ return a.length === b.length && a.every((value, i) => value === b[i]);
992
1145
  }
993
-
994
- // src/cli.ts
995
- var HELP = `verbaly \u2014 i18n compiler
1146
+ //#endregion
1147
+ //#region src/cli.ts
1148
+ const HELP = `verbaly i18n compiler
996
1149
 
997
1150
  Usage:
1151
+ verbaly init scaffold config + locale catalogs (detects your bundler)
1152
+ verbaly doctor diagnose the setup (config, catalogs, plugin, types, keys)
998
1153
  verbaly extract scan sources, update catalogs and types
999
1154
  verbaly check verify translations are complete (CI)
1000
1155
  verbaly translate fill missing translations via a provider (default: claude)
@@ -1016,131 +1171,151 @@ Config file: verbaly.config.{js,mjs,ts,mts,json} at root (flags win).
1016
1171
  The claude provider needs @anthropic-ai/sdk installed and ANTHROPIC_API_KEY set.
1017
1172
  `;
1018
1173
  async function main() {
1019
- const { positionals, values } = parseArgs2({
1020
- allowPositionals: true,
1021
- options: {
1022
- root: { type: "string" },
1023
- dir: { type: "string" },
1024
- source: { type: "string" },
1025
- locales: { type: "string" },
1026
- prune: { type: "boolean" },
1027
- model: { type: "string" },
1028
- locale: { type: "string" },
1029
- site: { type: "string" },
1030
- "dry-run": { type: "boolean" },
1031
- help: { type: "boolean", short: "h" }
1032
- }
1033
- });
1034
- const command = positionals[0];
1035
- if (values.help || !command) {
1036
- console.log(HELP);
1037
- process.exitCode = command ? 0 : 1;
1038
- return;
1039
- }
1040
- const cfg = await loadConfig(values.root ?? process.cwd(), {
1041
- dir: values.dir,
1042
- sourceLocale: values.source,
1043
- locales: values.locales?.split(",")
1044
- });
1045
- if (command === "extract") {
1046
- const registry = await extractProject(cfg);
1047
- const catalogs = loadCatalogs(cfg);
1048
- if (values.prune) {
1049
- const removed = pruneCatalogs(cfg, catalogs, registry);
1050
- for (const [locale, keys] of Object.entries(removed)) {
1051
- console.log(` ${locale}: -${keys.length} pruned`);
1052
- }
1053
- }
1054
- const { added } = syncCatalogs(cfg, catalogs, registry);
1055
- for (const locale of cfg.locales) {
1056
- writeCatalog(cfg, locale, catalogs[locale] ?? {});
1057
- }
1058
- writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));
1059
- const total = registry.messages().size;
1060
- console.log(`[verbaly] ${total} messages \xB7 locales: ${cfg.locales.join(", ")}`);
1061
- for (const [locale, keys] of Object.entries(added)) {
1062
- console.log(` ${locale}: +${keys.length}`);
1063
- }
1064
- return;
1065
- }
1066
- if (command === "check") {
1067
- const registry = await extractProject(cfg);
1068
- const result = check(cfg, loadCatalogs(cfg), registry);
1069
- if (result.ok) {
1070
- console.log("[verbaly] all translations complete \u2713");
1071
- return;
1072
- }
1073
- console.error(`[verbaly] check failed
1074
- ${formatCheckResult(result)}`);
1075
- process.exitCode = 1;
1076
- return;
1077
- }
1078
- if (command === "translate") {
1079
- const catalogs = loadCatalogs(cfg);
1080
- const provider = await resolveProvider(cfg, values.model);
1081
- const result = await translateCatalogs(cfg, catalogs, provider, {
1082
- locales: values.locales?.split(","),
1083
- batchSize: cfg.translate.batchSize,
1084
- dryRun: values["dry-run"]
1085
- });
1086
- if (values["dry-run"]) {
1087
- const entries = Object.entries(result.pending);
1088
- if (entries.length === 0) {
1089
- console.log("[verbaly] nothing to translate \u2713");
1090
- return;
1091
- }
1092
- for (const [locale, keys] of entries) {
1093
- console.log(` ${locale}: ${keys.length} missing \u2014 ${keys.join(", ")}`);
1094
- }
1095
- return;
1096
- }
1097
- for (const locale of Object.keys(result.translated)) {
1098
- writeCatalog(cfg, locale, catalogs[locale] ?? {});
1099
- console.log(` ${locale}: +${result.translated[locale].length} translated`);
1100
- }
1101
- for (const [locale, keys] of Object.entries(result.invalid)) {
1102
- console.warn(
1103
- ` ${locale}: ${keys.length} rejected (params/tags not preserved): ${keys.join(", ")}`
1104
- );
1105
- }
1106
- if (Object.keys(result.translated).length === 0 && Object.keys(result.invalid).length === 0) {
1107
- console.log("[verbaly] nothing to translate \u2713");
1108
- }
1109
- return;
1110
- }
1111
- if (command === "render") {
1112
- const result = await renderSite(cfg, {
1113
- site: values.site,
1114
- locales: values.locales?.split(",")
1115
- });
1116
- console.log(
1117
- `[verbaly] ${result.files} pages \xD7 ${result.locales.length} locales (${result.locales.join(", ")})`
1118
- );
1119
- for (const [locale, keys] of Object.entries(result.missing)) {
1120
- console.warn(` ${locale}: ${keys.length} keys not pre-filled \u2014 ${keys.join(", ")}`);
1121
- }
1122
- return;
1123
- }
1124
- if (command === "pseudo") {
1125
- const catalogs = loadCatalogs(cfg);
1126
- const locale = values.locale ?? PSEUDO_LOCALE;
1127
- const keys = pseudoCatalogs(cfg, catalogs, locale);
1128
- writeCatalog(cfg, locale, catalogs[locale] ?? {});
1129
- console.log(`[verbaly] ${keys.length} messages pseudo-localized \u2192 ${locale}`);
1130
- return;
1131
- }
1132
- console.error(`[verbaly] unknown command "${command}"
1133
- ${HELP}`);
1134
- process.exitCode = 1;
1174
+ const { positionals, values } = parseArgs({
1175
+ allowPositionals: true,
1176
+ options: {
1177
+ root: { type: "string" },
1178
+ dir: { type: "string" },
1179
+ source: { type: "string" },
1180
+ locales: { type: "string" },
1181
+ prune: { type: "boolean" },
1182
+ model: { type: "string" },
1183
+ locale: { type: "string" },
1184
+ site: { type: "string" },
1185
+ "dry-run": { type: "boolean" },
1186
+ help: {
1187
+ type: "boolean",
1188
+ short: "h"
1189
+ }
1190
+ }
1191
+ });
1192
+ const command = positionals[0];
1193
+ if (values.help || !command) {
1194
+ console.log(HELP);
1195
+ process.exitCode = command ? 0 : 1;
1196
+ return;
1197
+ }
1198
+ if (command === "init") {
1199
+ const result = init({
1200
+ root: values.root,
1201
+ dir: values.dir,
1202
+ sourceLocale: values.source,
1203
+ locales: values.locales?.split(",")
1204
+ });
1205
+ if (result.created.length) console.log(`[verbaly] created: ${result.created.join(", ")}`);
1206
+ if (result.skipped.length) console.log(` kept (already there): ${result.skipped.join(", ")}`);
1207
+ if (result.bundler) console.log(` detected bundler: ${result.bundler}`);
1208
+ console.log([" next steps:", ...result.next.map((step, i) => ` ${i + 1}. ${step}`)].join("\n"));
1209
+ return;
1210
+ }
1211
+ const cfg = await loadConfig(values.root ?? process.cwd(), {
1212
+ dir: values.dir,
1213
+ sourceLocale: values.source,
1214
+ locales: values.locales?.split(",")
1215
+ });
1216
+ if (command === "doctor") {
1217
+ const result = await doctor(cfg);
1218
+ const icon = {
1219
+ ok: "✓",
1220
+ warn: "⚠",
1221
+ error: ""
1222
+ };
1223
+ console.log(`[verbaly] doctor ${result.entries.length} checks`);
1224
+ for (const entry of result.entries) {
1225
+ const line = ` ${icon[entry.level]} ${entry.check}: ${entry.message}`;
1226
+ if (entry.level === "error") console.error(line);
1227
+ else if (entry.level === "warn") console.warn(line);
1228
+ else console.log(line);
1229
+ if (entry.fix) console.log(` fix: ${entry.fix}`);
1230
+ }
1231
+ if (result.ok) console.log("[verbaly] setup looks healthy ✓");
1232
+ else {
1233
+ console.error("[verbaly] doctor found problems");
1234
+ process.exitCode = 1;
1235
+ }
1236
+ return;
1237
+ }
1238
+ if (command === "extract") {
1239
+ const registry = await extractProject(cfg);
1240
+ const catalogs = loadCatalogs(cfg);
1241
+ if (values.prune) {
1242
+ const removed = pruneCatalogs(cfg, catalogs, registry);
1243
+ for (const [locale, keys] of Object.entries(removed)) console.log(` ${locale}: -${keys.length} pruned`);
1244
+ }
1245
+ const { added } = syncCatalogs(cfg, catalogs, registry);
1246
+ for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});
1247
+ writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));
1248
+ const total = registry.messages().size;
1249
+ console.log(`[verbaly] ${total} messages · locales: ${cfg.locales.join(", ")}`);
1250
+ for (const [locale, keys] of Object.entries(added)) console.log(` ${locale}: +${keys.length}`);
1251
+ return;
1252
+ }
1253
+ if (command === "check") {
1254
+ const registry = await extractProject(cfg);
1255
+ const result = check(cfg, loadCatalogs(cfg), registry);
1256
+ if (result.ok) {
1257
+ console.log("[verbaly] all translations complete ✓");
1258
+ return;
1259
+ }
1260
+ console.error(`[verbaly] check failed\n${formatCheckResult(result)}`);
1261
+ process.exitCode = 1;
1262
+ return;
1263
+ }
1264
+ if (command === "translate") {
1265
+ const catalogs = loadCatalogs(cfg);
1266
+ const result = await translateCatalogs(cfg, catalogs, await resolveProvider(cfg, values.model), {
1267
+ locales: values.locales?.split(","),
1268
+ batchSize: cfg.translate.batchSize,
1269
+ dryRun: values["dry-run"]
1270
+ });
1271
+ if (values["dry-run"]) {
1272
+ const entries = Object.entries(result.pending);
1273
+ if (entries.length === 0) {
1274
+ console.log("[verbaly] nothing to translate ✓");
1275
+ return;
1276
+ }
1277
+ for (const [locale, keys] of entries) console.log(` ${locale}: ${keys.length} missing — ${keys.join(", ")}`);
1278
+ return;
1279
+ }
1280
+ for (const locale of Object.keys(result.translated)) {
1281
+ writeCatalog(cfg, locale, catalogs[locale] ?? {});
1282
+ console.log(` ${locale}: +${result.translated[locale].length} translated`);
1283
+ }
1284
+ for (const [locale, keys] of Object.entries(result.invalid)) console.warn(` ${locale}: ${keys.length} rejected (params/tags not preserved): ${keys.join(", ")}`);
1285
+ if (Object.keys(result.translated).length === 0 && Object.keys(result.invalid).length === 0) console.log("[verbaly] nothing to translate ✓");
1286
+ return;
1287
+ }
1288
+ if (command === "render") {
1289
+ const result = await renderSite(cfg, {
1290
+ site: values.site,
1291
+ locales: values.locales?.split(",")
1292
+ });
1293
+ console.log(`[verbaly] ${result.files} pages × ${result.locales.length} locales (${result.locales.join(", ")})`);
1294
+ for (const [locale, keys] of Object.entries(result.missing)) console.warn(` ${locale}: ${keys.length} keys not pre-filled — ${keys.join(", ")}`);
1295
+ return;
1296
+ }
1297
+ if (command === "pseudo") {
1298
+ const catalogs = loadCatalogs(cfg);
1299
+ const locale = values.locale ?? "en-XA";
1300
+ const keys = pseudoCatalogs(cfg, catalogs, locale);
1301
+ writeCatalog(cfg, locale, catalogs[locale] ?? {});
1302
+ console.log(`[verbaly] ${keys.length} messages pseudo-localized → ${locale}`);
1303
+ return;
1304
+ }
1305
+ console.error(`[verbaly] unknown command "${command}"\n${HELP}`);
1306
+ process.exitCode = 1;
1135
1307
  }
1136
1308
  async function resolveProvider(cfg, model) {
1137
- const configured = cfg.translate.provider;
1138
- if (typeof configured === "function") return configured;
1139
- const { claudeProvider } = await import("./claude-XSRCRBAQ.js");
1140
- return claudeProvider({ model: model ?? cfg.translate.model });
1309
+ const configured = cfg.translate.provider;
1310
+ if (typeof configured === "function") return configured;
1311
+ const { claudeProvider } = await import("./claude-Bi_Ex2hm.js");
1312
+ return claudeProvider({ model: model ?? cfg.translate.model });
1141
1313
  }
1142
1314
  main().catch((error) => {
1143
- console.error("[verbaly]", error instanceof Error ? error.message : error);
1144
- process.exitCode = 1;
1315
+ console.error("[verbaly]", error instanceof Error ? error.message : error);
1316
+ process.exitCode = 1;
1145
1317
  });
1318
+ //#endregion
1319
+ export {};
1320
+
1146
1321
  //# sourceMappingURL=cli.js.map