@nxgt/mail-i18n 0.1.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 ADDED
@@ -0,0 +1,675 @@
1
+ // src/manifest.ts
2
+ import { readFileSync as readFileSync2 } from "node:fs";
3
+ import { isAbsolute, join as join2, relative as relative2, sep as sep2 } from "node:path";
4
+
5
+ // src/translator.ts
6
+ import { IntlMessageFormat } from "intl-messageformat";
7
+ function lookup(catalogue, key) {
8
+ let node = catalogue;
9
+ for (const segment of key.split(".")) {
10
+ if (typeof node !== "object" || !Object.hasOwn(node, segment))
11
+ return null;
12
+ node = node[segment];
13
+ }
14
+ return typeof node === "string" ? node : null;
15
+ }
16
+ var resolveLanguage = (language) => typeof language === "function" ? language() : language;
17
+ function createFormatter(prefix) {
18
+ const compiled = new Map;
19
+ return (locale, key, text, args) => {
20
+ const id = `${locale}\x00${key}`;
21
+ try {
22
+ let format = compiled.get(id);
23
+ if (format === undefined) {
24
+ format = new IntlMessageFormat(text, locale, undefined, {
25
+ ignoreTag: true
26
+ });
27
+ compiled.set(id, format);
28
+ }
29
+ return String(format.format(args));
30
+ } catch (cause) {
31
+ throw new Error(`${prefix}: ${locale}: ${key} could not be formatted`, {
32
+ cause
33
+ });
34
+ }
35
+ };
36
+ }
37
+ function createTranslator(catalogues, getLanguage) {
38
+ if (typeof catalogues !== "object" || catalogues === null) {
39
+ throw new TypeError("createTranslator: catalogues must be an object of catalogues by locale, as { en, fr }");
40
+ }
41
+ if (typeof getLanguage !== "string" && typeof getLanguage !== "function") {
42
+ throw new TypeError("createTranslator: getLanguage must be a locale or a function that answers one");
43
+ }
44
+ const format = createFormatter("t");
45
+ return (key, args, language = getLanguage) => {
46
+ const locale = resolveLanguage(language);
47
+ if (typeof locale !== "string" || !Object.hasOwn(catalogues, locale)) {
48
+ throw new Error("t: the language is not a locale of the catalogues — pick one with pickLocale");
49
+ }
50
+ const text = lookup(catalogues[locale], key);
51
+ if (text === null)
52
+ throw new Error(`t: ${locale}: ${key} is not a key`);
53
+ return format(locale, key, text, args);
54
+ };
55
+ }
56
+
57
+ // src/wrappers.ts
58
+ import {
59
+ existsSync,
60
+ mkdirSync,
61
+ readdirSync,
62
+ readFileSync,
63
+ rmSync,
64
+ writeFileSync
65
+ } from "node:fs";
66
+ import { dirname, join, relative, sep } from "node:path";
67
+ var NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
68
+ var posix = (path) => path.split(sep).join("/");
69
+ function listEmails(dir, label) {
70
+ if (!existsSync(dir))
71
+ return [];
72
+ return readdirSync(dir, { recursive: true, encoding: "utf8" }).filter((file) => file.endsWith(".vue")).map((file) => {
73
+ const email = posix(file).slice(0, -".vue".length);
74
+ for (const segment of email.split("/")) {
75
+ if (!NAME.test(segment)) {
76
+ throw new Error(`i18n: ${label}/${email}.vue is not a kebab-case name — name a template as verify-email.vue`);
77
+ }
78
+ }
79
+ return email;
80
+ }).sort();
81
+ }
82
+ function entryPath(entry, layout) {
83
+ return layout === "nested" ? `${entry.locale}/${entry.email}` : `${entry.email}.${entry.locale}`;
84
+ }
85
+ function parseEntry(path, layout, locales) {
86
+ if (layout === "nested") {
87
+ const [locale, ...rest] = path.split("/");
88
+ if (locale === undefined || rest.length === 0)
89
+ return null;
90
+ return locales.includes(locale) ? { email: rest.join("/"), locale } : null;
91
+ }
92
+ const dot = path.lastIndexOf(".");
93
+ const locale = path.slice(dot + 1);
94
+ return dot > 0 && locales.includes(locale) ? { email: path.slice(0, dot), locale } : null;
95
+ }
96
+ function wrapperSource(wrapper, template) {
97
+ const from = posix(relative(dirname(wrapper), template));
98
+ return [
99
+ "<!-- Generated by @nxgt/mail-i18n: one per template and locale. Never edited, never committed. -->",
100
+ "<script setup>",
101
+ `import Email from '${from.startsWith(".") ? from : `./${from}`}';`,
102
+ "</script>",
103
+ "<template><Email /></template>",
104
+ ""
105
+ ].join(`
106
+ `);
107
+ }
108
+ function listFiles(dir) {
109
+ if (!existsSync(dir))
110
+ return [];
111
+ return readdirSync(dir, { recursive: true, encoding: "utf8" }).map((file) => join(dir, file)).filter((file) => file.endsWith(".vue"));
112
+ }
113
+ function folderEmails({
114
+ dir,
115
+ label,
116
+ only,
117
+ packaged
118
+ }) {
119
+ const emails = listEmails(dir, label);
120
+ if (packaged && emails.length === 0) {
121
+ throw new Error(`i18n: ${label} holds no template — is ${dir} the folder of a package's e-mails?`);
122
+ }
123
+ for (const email of only ?? []) {
124
+ if (!emails.includes(email)) {
125
+ throw new Error(`i18n: ${label} has no template ${email}.vue — name one of its e-mails`);
126
+ }
127
+ }
128
+ return [...only ?? emails];
129
+ }
130
+ function collectTemplates(folders) {
131
+ const templates = new Map;
132
+ const owners = new Map;
133
+ for (const folder of folders) {
134
+ for (const email of folderEmails(folder)) {
135
+ const owner = owners.get(email);
136
+ if (owner === undefined) {
137
+ owners.set(email, folder);
138
+ templates.set(email, join(folder.dir, `${email}.vue`));
139
+ } else if (owner.packaged) {
140
+ throw new Error(`i18n: ${owner.label} and ${folder.label} both have ${email}.vue — keep one with emails: [...], or write the project's own in its folder`);
141
+ }
142
+ }
143
+ }
144
+ return templates;
145
+ }
146
+ function writeWrappers(options) {
147
+ const { wrappersDir, locales, layout } = options;
148
+ const templates = collectTemplates(options.folders);
149
+ const wanted = new Set;
150
+ for (const [email, template] of templates) {
151
+ for (const locale of locales) {
152
+ const wrapper = join(wrappersDir, `${entryPath({ email, locale }, layout)}.vue`);
153
+ wanted.add(wrapper);
154
+ const source = wrapperSource(wrapper, template);
155
+ if (existsSync(wrapper) && readFileSync(wrapper, "utf8") === source) {
156
+ continue;
157
+ }
158
+ mkdirSync(dirname(wrapper), { recursive: true });
159
+ writeFileSync(wrapper, source);
160
+ }
161
+ }
162
+ for (const file of listFiles(wrappersDir)) {
163
+ if (!wanted.has(file))
164
+ rmSync(file);
165
+ }
166
+ return [...templates.keys()].sort();
167
+ }
168
+ function watchTemplates(emailsDir, regenerate) {
169
+ return {
170
+ name: "nxgt-mail-i18n:wrappers",
171
+ configureServer(server) {
172
+ server.watcher.add(emailsDir);
173
+ const onTemplate = (file) => {
174
+ if (!file.startsWith(emailsDir + sep) || !file.endsWith(".vue"))
175
+ return;
176
+ try {
177
+ regenerate();
178
+ } catch (error) {
179
+ console.error(error instanceof Error ? error.message : error);
180
+ }
181
+ };
182
+ server.watcher.on("add", onTemplate);
183
+ server.watcher.on("unlink", onTemplate);
184
+ }
185
+ };
186
+ }
187
+
188
+ // src/manifest.ts
189
+ var PLACEHOLDER = /\{\{\s*([a-z][a-zA-Z0-9]*)\s*\}\}/g;
190
+ var URL_ATTRIBUTE = /\s(?:href|src|background|poster|action)\s*=\s*(?:"\s*(\{\{[^"]*)"|'\s*(\{\{[^']*)')/gi;
191
+ var placeholderMark = (name) => `{{ ${name} }}`;
192
+ var placeholdersIn = (text) => [...text.matchAll(PLACEHOLDER)].map((match) => match[1]);
193
+ var emailKey = (email) => email.split("/").map((segment) => segment.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase())).join(".");
194
+ function subjectOf(email, locale, messages, format) {
195
+ const key = `${emailKey(email)}.subject`;
196
+ const message = messages.get(key);
197
+ if (message === undefined) {
198
+ throw new Error(`i18n: ${email} has no subject — add ${key} to the catalogues`);
199
+ }
200
+ const args = {};
201
+ for (const [name, kind] of message.args) {
202
+ if (message.selects.has(name)) {
203
+ throw new Error(`i18n: ${locale}: ${key} chooses on {${name}} with a select — a subject's arguments are placeholders, which always choose other`);
204
+ }
205
+ if (kind !== "string") {
206
+ throw new Error(`i18n: ${locale}: ${key} uses {${name}} as a ${kind} — a subject's arguments are placeholders, filled at send time as strings`);
207
+ }
208
+ args[name] = placeholderMark(name);
209
+ }
210
+ return format(locale, key, message.text, args);
211
+ }
212
+ var sorted = (values) => [...new Set(values)].sort();
213
+ function buildManifest(options) {
214
+ const { layout, locales } = options;
215
+ const format = createFormatter("i18n");
216
+ const found = new Map;
217
+ for (const file of options.files) {
218
+ const path = relative2(options.outputDir, file).split(sep2).join("/");
219
+ if (path.startsWith("../") || isAbsolute(path)) {
220
+ throw new Error(`i18n: ${path} was written outside the output folder — the i18n plugin lays out every e-mail; set no plaintext.destination and no output path in a template`);
221
+ }
222
+ const dot = path.lastIndexOf(".");
223
+ const base = path.slice(0, Math.max(dot, 0));
224
+ const entry = dot > 0 ? parseEntry(base, layout, locales) : null;
225
+ if (entry === null) {
226
+ throw new Error(`i18n: ${path} is not where the i18n plugin puts an e-mail — set no output path in a template`);
227
+ }
228
+ const seen = found.get(base) ?? { entry, html: null, text: null };
229
+ if (path.slice(dot + 1) === options.htmlExtension)
230
+ seen.html = path;
231
+ else
232
+ seen.text = path;
233
+ found.set(base, seen);
234
+ }
235
+ const emails = {};
236
+ for (const email of sorted([...found.values()].map((seen) => seen.entry.email))) {
237
+ const variables = [];
238
+ const urlVariables = [];
239
+ const subject = {};
240
+ const files = {};
241
+ for (const locale of locales) {
242
+ const seen = found.get(entryPath({ email, locale }, layout));
243
+ if (seen?.html == null) {
244
+ throw new Error(`i18n: ${email} was not built in ${locale}`);
245
+ }
246
+ const html = readFileSync2(join2(options.outputDir, seen.html), "utf8");
247
+ if (html.replace(/<!doctype[^>]*>/i, "").trim() === "") {
248
+ throw new Error(`i18n: ${seen.html} is empty — a tag of its template resolved to no component; list the plugin that brings it, as ui()`);
249
+ }
250
+ variables.push(...placeholdersIn(html));
251
+ for (const match of html.matchAll(URL_ATTRIBUTE)) {
252
+ urlVariables.push(...placeholdersIn(match[1] ?? match[2] ?? "").slice(0, 1));
253
+ }
254
+ if (seen.text !== null) {
255
+ const text = readFileSync2(join2(options.outputDir, seen.text), "utf8");
256
+ variables.push(...placeholdersIn(text));
257
+ }
258
+ subject[locale] = subjectOf(email, locale, options.messages.get(locale), format);
259
+ variables.push(...placeholdersIn(subject[locale]));
260
+ files[locale] = { html: seen.html, text: seen.text };
261
+ }
262
+ emails[email] = {
263
+ variables: sorted(variables),
264
+ urlVariables: sorted(urlVariables),
265
+ subject,
266
+ files
267
+ };
268
+ }
269
+ return {
270
+ locales: [...locales],
271
+ fallbackLocale: options.fallbackLocale,
272
+ emails
273
+ };
274
+ }
275
+ // src/plugin.ts
276
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
277
+ import { join as join3, relative as relative3, resolve, sep as sep3 } from "node:path";
278
+ import { isMainThread } from "node:worker_threads";
279
+ import { defineMailPlugin } from "@nxgt/mail-config";
280
+
281
+ // src/catalogues.ts
282
+ import {
283
+ parse,
284
+ TYPE
285
+ } from "@formatjs/icu-messageformat-parser";
286
+ var SEGMENT = /^[a-z][a-zA-Z0-9]*$/;
287
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
288
+ function mergeCatalogue(under, over) {
289
+ const out = { ...under };
290
+ for (const [key, value] of Object.entries(over)) {
291
+ const below = Object.hasOwn(out, key) ? out[key] : undefined;
292
+ Object.defineProperty(out, key, {
293
+ value: isObject(below) && isObject(value) ? mergeCatalogue(below, value) : value,
294
+ enumerable: true,
295
+ writable: true,
296
+ configurable: true
297
+ });
298
+ }
299
+ return out;
300
+ }
301
+ function layerCatalogues(sources, project) {
302
+ const out = {};
303
+ for (const [locale, catalogue] of Object.entries(project)) {
304
+ out[locale] = [...sources.map((source) => source[locale]), catalogue].filter((layer) => layer !== undefined).reduce(mergeCatalogue, {});
305
+ }
306
+ return out;
307
+ }
308
+ function flatten(catalogue, locale, prefix, into) {
309
+ if (!isObject(catalogue)) {
310
+ throw new Error(`i18n: ${locale}: ${prefix || "the catalogue"} must be an object of messages`);
311
+ }
312
+ for (const [segment, value] of Object.entries(catalogue)) {
313
+ const key = prefix === "" ? segment : `${prefix}.${segment}`;
314
+ if (!SEGMENT.test(segment)) {
315
+ throw new Error(`i18n: ${locale}: ${key} is not camelCase — every segment of a key is camelCase, and nested rather than dotted, as verifyEmail.title`);
316
+ }
317
+ if (typeof value === "string")
318
+ into.set(key, value);
319
+ else if (isObject(value))
320
+ flatten(value, locale, key, into);
321
+ else {
322
+ throw new Error(`i18n: ${locale}: ${key} must be a message (a string) or an object of messages`);
323
+ }
324
+ }
325
+ }
326
+ function collect(elements, uses) {
327
+ const use = (name, kind) => uses.set(name, (uses.get(name) ?? new Set).add(kind));
328
+ for (const element of elements) {
329
+ if (element.type === TYPE.argument)
330
+ use(element.value, "plain");
331
+ else if (element.type === TYPE.number)
332
+ use(element.value, "number");
333
+ else if (element.type === TYPE.date || element.type === TYPE.time) {
334
+ use(element.value, "date");
335
+ } else if (element.type === TYPE.select || element.type === TYPE.plural) {
336
+ use(element.value, element.type === TYPE.plural ? "number" : "select");
337
+ for (const option of Object.values(element.options)) {
338
+ collect(option.value, uses);
339
+ }
340
+ } else if (element.type === TYPE.tag)
341
+ collect(element.children, uses);
342
+ }
343
+ }
344
+ function analyse(text, locale, key) {
345
+ let ast;
346
+ try {
347
+ ast = parse(text, { ignoreTag: true, shouldParseSkeletons: true });
348
+ } catch (cause) {
349
+ const reason = cause instanceof Error ? cause.message : "refused";
350
+ throw new Error(`i18n: ${locale}: ${key} is not a valid ICU message (${reason})`);
351
+ }
352
+ const uses = new Map;
353
+ collect(ast, uses);
354
+ const args = new Map;
355
+ const selects = new Set;
356
+ for (const [name, kinds] of uses) {
357
+ if (!SEGMENT.test(name)) {
358
+ throw new Error(`i18n: ${locale}: ${key} uses {${name}}, which is not camelCase — an argument is a camelCase name, as {firstName}`);
359
+ }
360
+ if (kinds.has("select"))
361
+ selects.add(name);
362
+ const typed = [
363
+ ...new Set([...kinds].filter((kind) => kind !== "plain").map((kind) => kind === "select" ? "string" : kind))
364
+ ];
365
+ if (typed.length > 1) {
366
+ throw new Error(`i18n: ${locale}: ${key} uses {${name}} as ${typed.join(" and as ")}`);
367
+ }
368
+ args.set(name, typed[0] ?? "string");
369
+ }
370
+ return { text, args, selects };
371
+ }
372
+ function compare(locale, messages, fallbackLocale, reference) {
373
+ for (const key of [...reference.keys()].sort()) {
374
+ if (!messages.has(key)) {
375
+ throw new Error(`i18n: ${locale}: ${key} is missing — ${fallbackLocale}, the fallback locale, has it`);
376
+ }
377
+ }
378
+ for (const key of [...messages.keys()].sort()) {
379
+ const declared = reference.get(key);
380
+ if (declared === undefined) {
381
+ throw new Error(`i18n: ${locale}: ${key} is not a key of ${fallbackLocale}, the fallback locale`);
382
+ }
383
+ for (const [name, kind] of messages.get(key).args) {
384
+ const expected = declared.args.get(name);
385
+ if (expected === undefined) {
386
+ throw new Error(`i18n: ${locale}: ${key} uses {${name}}, which ${fallbackLocale} does not declare`);
387
+ }
388
+ if (expected !== kind) {
389
+ throw new Error(`i18n: ${locale}: ${key} uses {${name}} as ${kind}, and ${fallbackLocale} declares it as ${expected}`);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ function checkCatalogues(catalogues, locales, fallbackLocale) {
395
+ const out = new Map;
396
+ for (const locale of locales) {
397
+ const flat = new Map;
398
+ flatten(catalogues[locale], locale, "", flat);
399
+ out.set(locale, new Map([...flat].map(([key, text]) => [key, analyse(text, locale, key)])));
400
+ }
401
+ const reference = out.get(fallbackLocale);
402
+ for (const locale of locales) {
403
+ if (locale !== fallbackLocale) {
404
+ compare(locale, out.get(locale), fallbackLocale, reference);
405
+ }
406
+ }
407
+ return out;
408
+ }
409
+
410
+ // src/sources.ts
411
+ import { isAbsolute as isAbsolute2 } from "node:path";
412
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
413
+ var isSource = (source) => isObject2(source) && typeof source.dir === "string" && isAbsolute2(source.dir) && (source.emails === undefined || Array.isArray(source.emails) && source.emails.length > 0 && source.emails.every((email) => typeof email === "string") && new Set(source.emails).size === source.emails.length);
414
+ function checkTemplates(templates) {
415
+ if (templates !== undefined && (!Array.isArray(templates) || !templates.every(isSource))) {
416
+ throw new TypeError("i18n: templates must be a list of template folders, as [{ dir: '/abs/path/emails' }] — emails, when given, names at least one, each once");
417
+ }
418
+ }
419
+ function templateFolders(emails, templates) {
420
+ return [
421
+ { dir: emails.dir, label: emails.name },
422
+ ...templates.map((source, index) => ({
423
+ dir: source.dir,
424
+ label: `templates[${index}]`,
425
+ packaged: true,
426
+ ...source.emails && { only: source.emails }
427
+ }))
428
+ ];
429
+ }
430
+
431
+ // src/template.ts
432
+ var NAME2 = /^[a-z][a-zA-Z0-9]*$/;
433
+ var KINDS = {
434
+ string: (value) => typeof value === "string" || typeof value === "number",
435
+ number: (value) => typeof value === "number",
436
+ date: (value) => value instanceof Date || typeof value === "number"
437
+ };
438
+ var isPlaceholder = (value) => typeof value === "string" && /^\{\{ [a-z][a-zA-Z0-9]* \}\}$/.test(value);
439
+ var kindOf = (value) => value === null ? "null" : value instanceof Date ? "date" : typeof value;
440
+ function templateProperties(options) {
441
+ const { email, locale, messages, reference, format } = options;
442
+ const where = `i18n: ${locale}: ${email}`;
443
+ return {
444
+ locale,
445
+ t(key, args = {}) {
446
+ const message = typeof key === "string" ? messages.get(key) : undefined;
447
+ const declared = typeof key === "string" ? reference.get(key) : undefined;
448
+ if (message === undefined || declared === undefined) {
449
+ throw new Error(`${where} calls t('${String(key)}'), which is not a key of the catalogues`);
450
+ }
451
+ if (typeof args !== "object" || args === null) {
452
+ throw new Error(`${where} calls t('${key}') with arguments that are not an object, as { name: placeholder('name') }`);
453
+ }
454
+ for (const [name, kind] of declared.args) {
455
+ if (!Object.hasOwn(args, name)) {
456
+ throw new Error(`${where} calls t('${key}') without {${name}}`);
457
+ }
458
+ if (declared.selects.has(name) && isPlaceholder(args[name])) {
459
+ throw new Error(`${where} passes a placeholder to {${name}}, which ${key} chooses on with a select — a placeholder always chooses other`);
460
+ }
461
+ if (!KINDS[kind](args[name])) {
462
+ throw new Error(`${where} passes {${name}} to ${key} as a ${kindOf(args[name])} — the message uses it as a ${kind}`);
463
+ }
464
+ }
465
+ for (const name of Object.keys(args)) {
466
+ if (!declared.args.has(name)) {
467
+ throw new Error(`${where} passes {${name}} to ${key}, which does not use it`);
468
+ }
469
+ }
470
+ return format(locale, key, message.text, args);
471
+ },
472
+ placeholder(name) {
473
+ if (typeof name !== "string" || !NAME2.test(name)) {
474
+ throw new Error(`${where} calls placeholder() with a name that is not camelCase — as placeholder('firstName')`);
475
+ }
476
+ return placeholderMark(name);
477
+ }
478
+ };
479
+ }
480
+
481
+ // src/types.ts
482
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
483
+ import { dirname as dirname2 } from "node:path";
484
+ var TYPES_FILE = ".maizzle/nxgt-mail-i18n.d.ts";
485
+ var TYPE_OF = {
486
+ string: "string | number",
487
+ number: "number",
488
+ date: "Date | number"
489
+ };
490
+ function templateTypes(reference, fallbackLocale) {
491
+ const entries = [...reference.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, message]) => {
492
+ const args = [...message.args.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, kind]) => `${name}: ${TYPE_OF[kind]}`);
493
+ return ` '${key}': { ${args.join("; ")}${args.length > 0 ? " " : ""}};`;
494
+ });
495
+ return [
496
+ `// Generated by @nxgt/mail-i18n from the ${fallbackLocale} catalogue, each time the config loads.`,
497
+ "// Never edited, never committed: it types t() in the templates.",
498
+ "import type {} from '@nxgt/mail-i18n';",
499
+ "",
500
+ "declare module '@nxgt/mail-i18n' {",
501
+ "\tinterface TemplateMessages {",
502
+ ...entries,
503
+ "\t}",
504
+ "}",
505
+ ""
506
+ ].join(`
507
+ `);
508
+ }
509
+ var RENDERER_TYPES_HEADER = "// Generated by @nxgt/mail-i18n from the build";
510
+ var RENDERER_TYPES_FILE = "generated/mail.ts";
511
+ function rendererTypes(manifest) {
512
+ const entries = Object.entries(manifest.emails).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([email, { variables, urlVariables }]) => {
513
+ const fields = variables.map((name) => `readonly ${name}: ${urlVariables.includes(name) ? "string" : "string | number"}`);
514
+ const type = fields.length > 0 ? `{ ${fields.join("; ")} }` : "Readonly<Record<string, never>>";
515
+ return ` ${JSON.stringify(email)}: ${type};`;
516
+ });
517
+ return [
518
+ `${RENDERER_TYPES_HEADER}, after each maizzle build.`,
519
+ "// Never edited; committed, so the code that sends type-checks without a build.",
520
+ "",
521
+ "/** The e-mails of the build, each with the variables it takes when it is sent. */",
522
+ "export interface MailEmails {",
523
+ ...entries,
524
+ "}",
525
+ ""
526
+ ].join(`
527
+ `);
528
+ }
529
+ function writeRendererTypes(file, path, source) {
530
+ if (existsSync2(file) && !readFileSync3(file, "utf8").startsWith(RENDERER_TYPES_HEADER)) {
531
+ throw new Error(`i18n: ${path} was not written by i18n() — point rendererTypes at a file of its own`);
532
+ }
533
+ writeIfChanged(file, source);
534
+ }
535
+ function writeIfChanged(file, source) {
536
+ if (existsSync2(file) && readFileSync3(file, "utf8") === source)
537
+ return;
538
+ mkdirSync2(dirname2(file), { recursive: true });
539
+ writeFileSync2(file, source);
540
+ }
541
+
542
+ // src/plugin.ts
543
+ var WRAPPERS_DIR = ".maizzle/i18n";
544
+ var MANIFEST_FILE = "mail-manifest.json";
545
+ var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
546
+ var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
547
+ function checkOptions(options) {
548
+ if (typeof options !== "object" || options === null) {
549
+ throw new TypeError("i18n: options must be an object, as { locales: ['en', 'fr'] }");
550
+ }
551
+ const { locales } = options;
552
+ if (!Array.isArray(locales) || locales.length === 0) {
553
+ throw new TypeError("i18n: locales must hold at least one locale, as ['en', 'fr']");
554
+ }
555
+ for (const locale of locales) {
556
+ if (typeof locale !== "string" || !LOCALE.test(locale)) {
557
+ throw new TypeError("i18n: locales holds something that is not a locale — write each as a BCP 47 tag, as en or pt-BR");
558
+ }
559
+ }
560
+ if (new Set(locales).size !== locales.length) {
561
+ throw new TypeError("i18n: locales holds the same locale twice");
562
+ }
563
+ if (options.fallbackLocale !== undefined && !locales.includes(options.fallbackLocale)) {
564
+ throw new TypeError("i18n: fallbackLocale must be one of locales");
565
+ }
566
+ for (const key of ["dir", "emails"]) {
567
+ const value = options[key];
568
+ if (value !== undefined && (typeof value !== "string" || value === "")) {
569
+ throw new TypeError(`i18n: ${key} must be a folder of the project`);
570
+ }
571
+ }
572
+ if (options.layout !== undefined && options.layout !== "nested" && options.layout !== "flat") {
573
+ throw new TypeError("i18n: layout must be 'nested' or 'flat'");
574
+ }
575
+ const { catalogues } = options;
576
+ if (catalogues !== undefined && (!Array.isArray(catalogues) || !catalogues.every((source) => isObject3(source) && Object.values(source).every(isObject3)))) {
577
+ throw new TypeError("i18n: catalogues must be a list of catalogues by locale, as [{ en: {...}, fr: {...} }]");
578
+ }
579
+ const typesPath = options.rendererTypes;
580
+ if (typesPath !== undefined && typesPath !== false && (typeof typesPath !== "string" || !typesPath.endsWith(".ts"))) {
581
+ throw new TypeError("i18n: rendererTypes must be the path of a .ts file, as generated/mail.ts, or false");
582
+ }
583
+ checkTemplates(options.templates);
584
+ }
585
+ function readCatalogues(dir, dirName, locales) {
586
+ const out = {};
587
+ for (const locale of locales) {
588
+ const file = join3(dir, `${locale}.json`);
589
+ const name = `${dirName}/${locale}.json`;
590
+ if (!existsSync3(file)) {
591
+ throw new Error(`i18n: ${name} is missing — every locale has a catalogue`);
592
+ }
593
+ try {
594
+ out[locale] = JSON.parse(readFileSync4(file, "utf8"));
595
+ } catch {
596
+ throw new Error(`i18n: ${name} is not valid JSON`);
597
+ }
598
+ }
599
+ return out;
600
+ }
601
+ function i18n(options) {
602
+ checkOptions(options);
603
+ const { locales } = options;
604
+ const fallbackLocale = options.fallbackLocale ?? locales[0];
605
+ const layout = options.layout ?? "nested";
606
+ const dirName = options.dir ?? "locales";
607
+ const emailsName = options.emails ?? "emails";
608
+ const cwd = process.cwd();
609
+ const emailsDir = resolve(cwd, emailsName);
610
+ const wrappersDir = resolve(cwd, WRAPPERS_DIR);
611
+ const messages = checkCatalogues(layerCatalogues(options.catalogues ?? [], readCatalogues(resolve(cwd, dirName), dirName, locales)), locales, fallbackLocale);
612
+ const reference = messages.get(fallbackLocale);
613
+ const format = createFormatter("i18n");
614
+ const regenerate = () => writeWrappers({
615
+ folders: templateFolders({ dir: emailsDir, name: emailsName }, options.templates ?? []),
616
+ wrappersDir,
617
+ locales,
618
+ layout
619
+ });
620
+ if (isMainThread) {
621
+ regenerate();
622
+ writeIfChanged(resolve(cwd, TYPES_FILE), templateTypes(reference, fallbackLocale));
623
+ }
624
+ return defineMailPlugin({
625
+ name: "i18n",
626
+ content: [`${wrappersDir}/**/*.vue`],
627
+ ...dirName === "locales" ? {} : { server: { watch: [`${dirName}/**`] } },
628
+ vite: { plugins: [watchTemplates(emailsDir, regenerate)] },
629
+ beforeRender({ config, template }) {
630
+ const path = relative3(wrappersDir, join3(template.path.dir, template.path.name)).split(sep3).join("/");
631
+ const entry = parseEntry(path, layout, locales);
632
+ if (entry === null) {
633
+ throw new Error(`i18n: ${relative3(cwd, join3(template.path.dir, template.path.base))} is not built through the i18n plugin — leave content to it, and put templates in ${emailsName}/`);
634
+ }
635
+ config.vue ??= {};
636
+ config.vue.globalProperties = {
637
+ ...config.vue.globalProperties,
638
+ ...templateProperties({
639
+ ...entry,
640
+ messages: messages.get(entry.locale),
641
+ reference,
642
+ format
643
+ })
644
+ };
645
+ },
646
+ afterBuild({ files, config }) {
647
+ const outputDir = resolve(cwd, config.output?.path ?? "dist");
648
+ const manifest = buildManifest({
649
+ files,
650
+ outputDir,
651
+ htmlExtension: config.output?.extension ?? "html",
652
+ layout,
653
+ locales,
654
+ fallbackLocale,
655
+ messages
656
+ });
657
+ writeFileSync3(join3(outputDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
658
+ `);
659
+ const typesFile = options.rendererTypes ?? RENDERER_TYPES_FILE;
660
+ if (typesFile !== false) {
661
+ writeRendererTypes(resolve(cwd, typesFile), typesFile, rendererTypes(manifest));
662
+ }
663
+ }
664
+ });
665
+ }
666
+ export {
667
+ MANIFEST_FILE,
668
+ WRAPPERS_DIR,
669
+ createTranslator,
670
+ emailKey,
671
+ i18n
672
+ };
673
+
674
+ //# debugId=874EDA850D792EF364756E2164756E21
675
+ //# sourceMappingURL=index.js.map