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