@nxgt/mail-i18n 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/index.js +3 -3
- package/dist/index.js.map +3 -3
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/docs/guide/manifest.md +22 -7
- package/docs/guide/templates.md +9 -9
- package/docs/roadmap.md +8 -4
- package/docs/troubleshooting.md +40 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Peers:
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
The plugin writes one generated file per template and locale under
|
|
54
|
-
`.maizzle/
|
|
54
|
+
`.maizzle/emails/`. They are rewritten on every run, never edited, and never
|
|
55
55
|
committed.
|
|
56
56
|
|
|
57
57
|
```css
|
|
@@ -88,7 +88,7 @@ file is how the editor learns them. See
|
|
|
88
88
|
| `createTranslator(catalogues, getLanguage)` | `t(key, args?, language?)` outside templates, shaped like `@nxgt/i18n`, but it throws on a missing key |
|
|
89
89
|
| `emailKey(email)` | Where an e-mail's messages live: `auth/reset-password` → `auth.resetPassword` |
|
|
90
90
|
| `MANIFEST_FILE` | `'mail-manifest.json'`, the manifest's name in the output folder |
|
|
91
|
-
| `WRAPPERS_DIR` | `'.maizzle/
|
|
91
|
+
| `WRAPPERS_DIR` | `'.maizzle/emails'`, where the generated files go |
|
|
92
92
|
| `I18nOptions`, `Layout`, `TemplateSource` | The plugin's options, `'nested' \| 'flat'`, and a package's folder of templates for `templates` |
|
|
93
93
|
| `Catalogue`, `Catalogues`, `ArgumentKind` | A catalogue as written, catalogues by locale, and `'string' \| 'number' \| 'date'` |
|
|
94
94
|
| `Translate`, `LanguageProvider`, `MessageArgs` | What `createTranslator` answers and takes |
|
|
@@ -289,8 +289,10 @@ image URL starts with, its subject in each locale, and its files:
|
|
|
289
289
|
answers the subject, HTML and text, every value escaped.
|
|
290
290
|
|
|
291
291
|
After the manifest, each build writes `generated/mail.ts`, in the project — the manifest's e-mails
|
|
292
|
-
and variables as a type, rewritten only when they change. **
|
|
293
|
-
|
|
292
|
+
and variables as a type, rewritten only when they change. **Git-ignore it**
|
|
293
|
+
and build before type-checking — `"typecheck": "maizzle build && tsc
|
|
294
|
+
--noEmit"` — as you do for `dist/`: it is output of the build, like the
|
|
295
|
+
e-mails it describes.
|
|
294
296
|
|
|
295
297
|
```ts
|
|
296
298
|
// generated/mail.ts — never edited
|
|
@@ -378,7 +380,7 @@ not affected. See [Editor and type checking](docs/guide/editor.md).
|
|
|
378
380
|
`content` in your config replaces that, and each template then fails with
|
|
379
381
|
`… is not built through the i18n plugin`.
|
|
380
382
|
|
|
381
|
-
**Run `maizzle` from the project root.** `dir`, `emails` and `.maizzle/
|
|
383
|
+
**Run `maizzle` from the project root.** `dir`, `emails` and `.maizzle/emails`
|
|
382
384
|
are resolved against the directory `maizzle` runs in.
|
|
383
385
|
|
|
384
386
|
**Nest keys; never dot them.** `{ "verifyEmail": { "title": "…" } }`, not
|
package/dist/index.js
CHANGED
|
@@ -516,7 +516,7 @@ function rendererTypes(manifest) {
|
|
|
516
516
|
});
|
|
517
517
|
return [
|
|
518
518
|
`${RENDERER_TYPES_HEADER}, after each maizzle build.`,
|
|
519
|
-
"// Never edited
|
|
519
|
+
"// Never edited, never committed: git-ignore it, and build before type-checking.",
|
|
520
520
|
"",
|
|
521
521
|
"/** The e-mails of the build, each with the variables it takes when it is sent. */",
|
|
522
522
|
"export interface MailEmails {",
|
|
@@ -540,7 +540,7 @@ function writeIfChanged(file, source) {
|
|
|
540
540
|
}
|
|
541
541
|
|
|
542
542
|
// src/plugin.ts
|
|
543
|
-
var WRAPPERS_DIR = ".maizzle/
|
|
543
|
+
var WRAPPERS_DIR = ".maizzle/emails";
|
|
544
544
|
var MANIFEST_FILE = "mail-manifest.json";
|
|
545
545
|
var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
546
546
|
var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -671,5 +671,5 @@ export {
|
|
|
671
671
|
i18n
|
|
672
672
|
};
|
|
673
673
|
|
|
674
|
-
//# debugId=
|
|
674
|
+
//# debugId=8EAF60F48839FE4D64756E2164756E21
|
|
675
675
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
"import { readFileSync } from 'node:fs';\nimport { isAbsolute, join, relative, sep } from 'node:path';\nimport type { Messages } from './catalogues';\nimport { createFormatter } from './translator';\nimport { type Entry, entryPath, type Layout, parseEntry } from './wrappers';\n\n/** One e-mail, as the renderer reads it. */\nexport interface ManifestEmail {\n\t/** Every placeholder of the e-mail, in any locale, sorted. */\n\treadonly variables: readonly string[];\n\t/**\n\t * The placeholders a URL attribute starts with — `href`, `src`,\n\t * `background`, `poster`, `action`: they decide the scheme, so a URL fills\n\t * them. One later in the value, as `?token={{ token }}`, is not one.\n\t */\n\treadonly urlVariables: readonly string[];\n\t/** The subject per locale, its placeholders kept as `{{ name }}`. */\n\treadonly subject: Readonly<Record<string, string>>;\n\t/** The built files per locale, relative to the output folder. */\n\treadonly files: Readonly<\n\t\tRecord<string, { readonly html: string; readonly text: string | null }>\n\t>;\n}\n\n/** `dist/mail-manifest.json`: what the build wrote, for the renderer. */\nexport interface Manifest {\n\treadonly locales: readonly string[];\n\treadonly fallbackLocale: string;\n\treadonly emails: Readonly<Record<string, ManifestEmail>>;\n}\n\n// Copied in packages/mail/src/renderer.ts, which reads this manifest: change both.\nconst PLACEHOLDER = /\\{\\{\\s*([a-z][a-zA-Z0-9]*)\\s*\\}\\}/g;\n/** An attribute that holds a URL, its value starting with a placeholder: that placeholder is the whole scheme. */\nconst URL_ATTRIBUTE =\n\t/\\s(?:href|src|background|poster|action)\\s*=\\s*(?:\"\\s*(\\{\\{[^\"]*)\"|'\\s*(\\{\\{[^']*)')/gi;\n\n/** `{{ name }}` — what `placeholder('name')` writes. */\nexport const placeholderMark = (name: string) => `{{ ${name} }}`;\n\nconst placeholdersIn = (text: string) =>\n\t[...text.matchAll(PLACEHOLDER)].map((match) => match[1] as string);\n\n/** `auth/reset-password` → `auth.resetPassword`: where its messages live. */\nexport const emailKey = (email: string) =>\n\temail\n\t\t.split('/')\n\t\t.map((segment) =>\n\t\t\tsegment.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()),\n\t\t)\n\t\t.join('.');\n\n/**\n * The subject of `email` in `locale`: the message `<emailKey>.subject`, each\n * argument a placeholder. A missing subject, or one with an argument that is\n * not a string, **throws**.\n */\nfunction subjectOf(\n\temail: string,\n\tlocale: string,\n\tmessages: Messages,\n\tformat: ReturnType<typeof createFormatter>,\n): string {\n\tconst key = `${emailKey(email)}.subject`;\n\tconst message = messages.get(key);\n\tif (message === undefined) {\n\t\tthrow new Error(\n\t\t\t`i18n: ${email} has no subject — add ${key} to the catalogues`,\n\t\t);\n\t}\n\tconst args: Record<string, string> = {};\n\tfor (const [name, kind] of message.args) {\n\t\tif (message.selects.has(name)) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} chooses on {${name}} with a select — a subject's arguments are placeholders, which always choose other`,\n\t\t\t);\n\t\t}\n\t\tif (kind !== 'string') {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} uses {${name}} as a ${kind} — a subject's arguments are placeholders, filled at send time as strings`,\n\t\t\t);\n\t\t}\n\t\targs[name] = placeholderMark(name);\n\t}\n\treturn format(locale, key, message.text, args);\n}\n\n/** One e-mail in one locale, as found in the build's files. */\ninterface Built {\n\treadonly entry: Entry;\n\thtml: string | null;\n\ttext: string | null;\n}\n\nconst sorted = (values: Iterable<string>) => [...new Set(values)].sort();\n\n/**\n * Reads what the build wrote — `files`, absolute, under `outputDir` — and\n * answers the manifest: for each e-mail its placeholders, those in a URL, its\n * subject per locale and its files.\n */\nexport function buildManifest(options: {\n\treadonly files: readonly string[];\n\treadonly outputDir: string;\n\treadonly htmlExtension: string;\n\treadonly layout: Layout;\n\treadonly locales: readonly string[];\n\treadonly fallbackLocale: string;\n\treadonly messages: ReadonlyMap<string, Messages>;\n}): Manifest {\n\tconst { layout, locales } = options;\n\tconst format = createFormatter('i18n');\n\tconst found = new Map<string, Built>();\n\tfor (const file of options.files) {\n\t\tconst path = relative(options.outputDir, file).split(sep).join('/');\n\t\tif (path.startsWith('../') || isAbsolute(path)) {\n\t\t\tthrow new Error(\n\t\t\t\t`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`,\n\t\t\t);\n\t\t}\n\t\tconst dot = path.lastIndexOf('.');\n\t\tconst base = path.slice(0, Math.max(dot, 0));\n\t\tconst entry = dot > 0 ? parseEntry(base, layout, locales) : null;\n\t\tif (entry === null) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${path} is not where the i18n plugin puts an e-mail — set no output path in a template`,\n\t\t\t);\n\t\t}\n\t\tconst seen = found.get(base) ?? { entry, html: null, text: null };\n\t\tif (path.slice(dot + 1) === options.htmlExtension) seen.html = path;\n\t\telse seen.text = path;\n\t\tfound.set(base, seen);\n\t}\n\n\tconst emails: Record<string, ManifestEmail> = {};\n\tfor (const email of sorted(\n\t\t[...found.values()].map((seen) => seen.entry.email),\n\t)) {\n\t\tconst variables: string[] = [];\n\t\tconst urlVariables: string[] = [];\n\t\tconst subject: Record<string, string> = {};\n\t\tconst files: Record<string, { html: string; text: string | null }> = {};\n\t\tfor (const locale of locales) {\n\t\t\tconst seen = found.get(entryPath({ email, locale }, layout));\n\t\t\tif (seen?.html == null) {\n\t\t\t\tthrow new Error(`i18n: ${email} was not built in ${locale}`);\n\t\t\t}\n\t\t\tconst html = readFileSync(join(options.outputDir, seen.html), 'utf8');\n\t\t\t// Vue renders an unresolved component as nothing, and Maizzle still\n\t\t\t// writes the doctype: the build would pass with an empty e-mail.\n\t\t\tif (html.replace(/<!doctype[^>]*>/i, '').trim() === '') {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`i18n: ${seen.html} is empty — a tag of its template resolved to no component; list the plugin that brings it, as ui()`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tvariables.push(...placeholdersIn(html));\n\t\t\tfor (const match of html.matchAll(URL_ATTRIBUTE)) {\n\t\t\t\turlVariables.push(\n\t\t\t\t\t...placeholdersIn(match[1] ?? match[2] ?? '').slice(0, 1),\n\t\t\t\t);\n\t\t\t}\n\t\t\t// A <Plaintext> block is in the text part only.\n\t\t\tif (seen.text !== null) {\n\t\t\t\tconst text = readFileSync(join(options.outputDir, seen.text), 'utf8');\n\t\t\t\tvariables.push(...placeholdersIn(text));\n\t\t\t}\n\t\t\tsubject[locale] = subjectOf(\n\t\t\t\temail,\n\t\t\t\tlocale,\n\t\t\t\toptions.messages.get(locale) as Messages,\n\t\t\t\tformat,\n\t\t\t);\n\t\t\tvariables.push(...placeholdersIn(subject[locale]));\n\t\t\tfiles[locale] = { html: seen.html, text: seen.text };\n\t\t}\n\t\temails[email] = {\n\t\t\tvariables: sorted(variables),\n\t\t\turlVariables: sorted(urlVariables),\n\t\t\tsubject,\n\t\t\tfiles,\n\t\t};\n\t}\n\treturn {\n\t\tlocales: [...locales],\n\t\tfallbackLocale: options.fallbackLocale,\n\t\temails,\n\t};\n}\n",
|
|
6
6
|
"import { IntlMessageFormat } from 'intl-messageformat';\nimport type { Catalogue, Catalogues } from './catalogues';\n\n/** The values a message's arguments take: `{ name: 'Ada', count: 3 }`. */\nexport type MessageArgs = Readonly<Record<string, string | number | Date>>;\n\n/** A locale, or a function that answers it at each call — as in `@nxgt/i18n`. */\nexport type LanguageProvider = string | (() => string);\n\n/** `t(key, args?, language?)`: the message `key`, formatted in the language. */\nexport type Translate = (\n\tkey: string,\n\targs?: MessageArgs,\n\tlanguage?: LanguageProvider,\n) => string;\n\nfunction lookup(catalogue: Catalogue, key: string): string | null {\n\tlet node: string | Catalogue | undefined = catalogue;\n\tfor (const segment of key.split('.')) {\n\t\tif (typeof node !== 'object' || !Object.hasOwn(node, segment)) return null;\n\t\tnode = node[segment];\n\t}\n\treturn typeof node === 'string' ? node : null;\n}\n\nconst resolveLanguage = (language: LanguageProvider): unknown =>\n\ttypeof language === 'function' ? language() : language;\n\n/**\n * Formats with a cache of compiled messages, one per locale and key. A\n * message that does not format **throws**, the formatter's error as the\n * cause.\n */\nexport function createFormatter(prefix: string) {\n\tconst compiled = new Map<string, IntlMessageFormat>();\n\treturn (locale: string, key: string, text: string, args?: MessageArgs) => {\n\t\tconst id = `${locale}\\u0000${key}`;\n\t\ttry {\n\t\t\tlet format = compiled.get(id);\n\t\t\tif (format === undefined) {\n\t\t\t\tformat = new IntlMessageFormat(text, locale, undefined, {\n\t\t\t\t\tignoreTag: true,\n\t\t\t\t});\n\t\t\t\tcompiled.set(id, format);\n\t\t\t}\n\t\t\treturn String(format.format(args));\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`${prefix}: ${locale}: ${key} could not be formatted`, {\n\t\t\t\tcause,\n\t\t\t});\n\t\t}\n\t};\n}\n\n/**\n * The translator of `@nxgt/i18n`, for mail: `createTranslator(catalogues,\n * getLanguage)` answers `t(key, args?, language?)`.\n *\n * Where `@nxgt/i18n` answers the key, this **throws**: a key the language's\n * catalogue does not have, a language with no catalogue, and a message that\n * does not format. An e-mail is not sent with a key in it.\n *\n * ```ts\n * import en from './locales/en.json';\n * import fr from './locales/fr.json';\n *\n * const t = createTranslator({ en, fr }, () => pickLocale(user.locale, ['en', 'fr'], 'en'));\n * t('verifyEmail.subject');\n * ```\n */\nexport function createTranslator(\n\tcatalogues: Catalogues,\n\tgetLanguage: LanguageProvider,\n): Translate {\n\tif (typeof catalogues !== 'object' || catalogues === null) {\n\t\tthrow new TypeError(\n\t\t\t'createTranslator: catalogues must be an object of catalogues by locale, as { en, fr }',\n\t\t);\n\t}\n\tif (typeof getLanguage !== 'string' && typeof getLanguage !== 'function') {\n\t\tthrow new TypeError(\n\t\t\t'createTranslator: getLanguage must be a locale or a function that answers one',\n\t\t);\n\t}\n\tconst format = createFormatter('t');\n\treturn (key, args, language = getLanguage) => {\n\t\tconst locale = resolveLanguage(language);\n\t\tif (typeof locale !== 'string' || !Object.hasOwn(catalogues, locale)) {\n\t\t\tthrow new Error(\n\t\t\t\t't: the language is not a locale of the catalogues — pick one with pickLocale',\n\t\t\t);\n\t\t}\n\t\tconst text = lookup(catalogues[locale] as Catalogue, key);\n\t\tif (text === null) throw new Error(`t: ${locale}: ${key} is not a key`);\n\t\treturn format(locale, key, text, args);\n\t};\n}\n",
|
|
7
7
|
"import {\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trmSync,\n\twriteFileSync,\n} from 'node:fs';\nimport { dirname, join, relative, sep } from 'node:path';\n\n/**\n * Where each locale's output goes: `nested` writes `en/verify-email.html`,\n * `flat` writes `verify-email.en.html`.\n */\nexport type Layout = 'nested' | 'flat';\n\n/** One e-mail in one locale: `{ email: 'auth/reset-password', locale: 'fr' }`. */\nexport interface Entry {\n\treadonly email: string;\n\treadonly locale: string;\n}\n\nconst NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;\n\nconst posix = (path: string) => path.split(sep).join('/');\n\n/** Every `.vue` file under `dir`, as a slash-separated path without `.vue`. */\nfunction listEmails(dir: string, label: string): string[] {\n\tif (!existsSync(dir)) return [];\n\treturn readdirSync(dir, { recursive: true, encoding: 'utf8' })\n\t\t.filter((file) => file.endsWith('.vue'))\n\t\t.map((file) => {\n\t\t\tconst email = posix(file).slice(0, -'.vue'.length);\n\t\t\tfor (const segment of email.split('/')) {\n\t\t\t\tif (!NAME.test(segment)) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`i18n: ${label}/${email}.vue is not a kebab-case name — name a template as verify-email.vue`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn email;\n\t\t})\n\t\t.sort();\n}\n\n/** The path of `entry`'s file under a folder laid out as `layout`, without an extension. */\nexport function entryPath(entry: Entry, layout: Layout): string {\n\treturn layout === 'nested'\n\t\t? `${entry.locale}/${entry.email}`\n\t\t: `${entry.email}.${entry.locale}`;\n}\n\n/**\n * Reads back what {@link entryPath} wrote — `path` relative to its folder,\n * slash-separated, without an extension — or `null` for a path that is not\n * one e-mail in one of `locales`.\n */\nexport function parseEntry(\n\tpath: string,\n\tlayout: Layout,\n\tlocales: readonly string[],\n): Entry | null {\n\tif (layout === 'nested') {\n\t\tconst [locale, ...rest] = path.split('/');\n\t\tif (locale === undefined || rest.length === 0) return null;\n\t\treturn locales.includes(locale) ? { email: rest.join('/'), locale } : null;\n\t}\n\tconst dot = path.lastIndexOf('.');\n\tconst locale = path.slice(dot + 1);\n\treturn dot > 0 && locales.includes(locale)\n\t\t? { email: path.slice(0, dot), locale }\n\t\t: null;\n}\n\nfunction wrapperSource(wrapper: string, template: string): string {\n\tconst from = posix(relative(dirname(wrapper), template));\n\treturn [\n\t\t'<!-- Generated by @nxgt/mail-i18n: one per template and locale. Never edited, never committed. -->',\n\t\t'<script setup>',\n\t\t`import Email from '${from.startsWith('.') ? from : `./${from}`}';`,\n\t\t'</script>',\n\t\t'<template><Email /></template>',\n\t\t'',\n\t].join('\\n');\n}\n\n/** Every file under `dir`, absolute. */\nfunction listFiles(dir: string): string[] {\n\tif (!existsSync(dir)) return [];\n\treturn readdirSync(dir, { recursive: true, encoding: 'utf8' })\n\t\t.map((file) => join(dir, file))\n\t\t.filter((file) => file.endsWith('.vue'));\n}\n\n/**\n * A folder of templates. `label` names it in an error: `emails`, or\n * `templates[0]` for a package's. `only`, when given, keeps those e-mails of\n * the folder and no other. A `packaged` folder — a package's — must hold\n * templates, and shares no name with another package's.\n */\nexport interface TemplateFolder {\n\treadonly dir: string;\n\treadonly label: string;\n\treadonly only?: readonly string[];\n\treadonly packaged?: boolean;\n}\n\n/** The e-mails of `folder`, the ones it keeps, checked. */\nfunction folderEmails({\n\tdir,\n\tlabel,\n\tonly,\n\tpackaged,\n}: TemplateFolder): string[] {\n\tconst emails = listEmails(dir, label);\n\tif (packaged && emails.length === 0) {\n\t\tthrow new Error(\n\t\t\t`i18n: ${label} holds no template — is ${dir} the folder of a package's e-mails?`,\n\t\t);\n\t}\n\tfor (const email of only ?? []) {\n\t\tif (!emails.includes(email)) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${label} has no template ${email}.vue — name one of its e-mails`,\n\t\t\t);\n\t\t}\n\t}\n\treturn [...(only ?? emails)];\n}\n\n/**\n * Each e-mail of `folders` and its file. The project's folder, first, wins\n * over a package's; two packages with the same e-mail **throw**, since\n * neither would be the obvious one.\n */\nfunction collectTemplates(\n\tfolders: readonly TemplateFolder[],\n): Map<string, string> {\n\tconst templates = new Map<string, string>();\n\tconst owners = new Map<string, TemplateFolder>();\n\tfor (const folder of folders) {\n\t\tfor (const email of folderEmails(folder)) {\n\t\t\tconst owner = owners.get(email);\n\t\t\tif (owner === undefined) {\n\t\t\t\towners.set(email, folder);\n\t\t\t\ttemplates.set(email, join(folder.dir, `${email}.vue`));\n\t\t\t} else if (owner.packaged) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`i18n: ${owner.label} and ${folder.label} both have ${email}.vue — keep one with emails: [...], or write the project's own in its folder`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\treturn templates;\n}\n\n/**\n * Writes one wrapper per template of `folders` and locale under\n * `wrappersDir` — the project's folder first, so its template replaces a\n * package's of the same name — each only when its text changed, and removes\n * the wrappers of templates that are gone, so a running dev server sees a\n * change only where there is one. Answers the e-mails, sorted.\n */\nexport function writeWrappers(options: {\n\treadonly folders: readonly TemplateFolder[];\n\treadonly wrappersDir: string;\n\treadonly locales: readonly string[];\n\treadonly layout: Layout;\n}): string[] {\n\tconst { wrappersDir, locales, layout } = options;\n\tconst templates = collectTemplates(options.folders);\n\tconst wanted = new Set<string>();\n\tfor (const [email, template] of templates) {\n\t\tfor (const locale of locales) {\n\t\t\tconst wrapper = join(\n\t\t\t\twrappersDir,\n\t\t\t\t`${entryPath({ email, locale }, layout)}.vue`,\n\t\t\t);\n\t\t\twanted.add(wrapper);\n\t\t\tconst source = wrapperSource(wrapper, template);\n\t\t\tif (existsSync(wrapper) && readFileSync(wrapper, 'utf8') === source) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tmkdirSync(dirname(wrapper), { recursive: true });\n\t\t\twriteFileSync(wrapper, source);\n\t\t}\n\t}\n\tfor (const file of listFiles(wrappersDir)) {\n\t\tif (!wanted.has(file)) rmSync(file);\n\t}\n\treturn [...templates.keys()].sort();\n}\n\n/** The part of Vite's dev server the watcher uses. */\ninterface WatchedServer {\n\treadonly watcher: {\n\t\tadd(path: string): void;\n\t\ton(event: 'add' | 'unlink', listener: (file: string) => void): void;\n\t};\n}\n\n/**\n * A Vite plugin for Maizzle's renderer: under `maizzle serve`, a template\n * added to or removed from `emailsDir` gets its wrappers at once, so the\n * preview lists it without a restart. A template the build would refuse is\n * reported, never thrown: an error in a watcher would stop the server.\n */\nexport function watchTemplates(emailsDir: string, regenerate: () => void) {\n\treturn {\n\t\tname: 'nxgt-mail-i18n:wrappers',\n\t\tconfigureServer(server: WatchedServer) {\n\t\t\tserver.watcher.add(emailsDir);\n\t\t\tconst onTemplate = (file: string) => {\n\t\t\t\tif (!file.startsWith(emailsDir + sep) || !file.endsWith('.vue')) return;\n\t\t\t\ttry {\n\t\t\t\t\tregenerate();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(error instanceof Error ? error.message : error);\n\t\t\t\t}\n\t\t\t};\n\t\t\tserver.watcher.on('add', onTemplate);\n\t\t\tserver.watcher.on('unlink', onTemplate);\n\t\t},\n\t};\n}\n",
|
|
8
|
-
"import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, relative, resolve, sep } from 'node:path';\nimport { isMainThread } from 'node:worker_threads';\nimport { defineMailPlugin, type MailPlugin } from '@nxgt/mail-config';\nimport {\n\ttype Catalogue,\n\ttype Catalogues,\n\tcheckCatalogues,\n\tlayerCatalogues,\n\ttype Messages,\n} from './catalogues';\nimport { buildManifest } from './manifest';\nimport {\n\tcheckTemplates,\n\ttype TemplateSource,\n\ttemplateFolders,\n} from './sources';\nimport { templateProperties } from './template';\nimport { createFormatter } from './translator';\nimport {\n\tRENDERER_TYPES_FILE,\n\trendererTypes,\n\tTYPES_FILE,\n\ttemplateTypes,\n\twriteIfChanged,\n\twriteRendererTypes,\n} from './types';\nimport {\n\ttype Layout,\n\tparseEntry,\n\twatchTemplates,\n\twriteWrappers,\n} from './wrappers';\n\nexport interface I18nOptions {\n\t/** Every locale the project writes, as BCP 47 tags: `['en', 'fr']`. */\n\treadonly locales: readonly string[];\n\t/** The reference every other locale is checked against. Default the first locale. */\n\treadonly fallbackLocale?: string;\n\t/** The folder of `<locale>.json` catalogues. Default `locales`. */\n\treadonly dir?: string;\n\t/** The folder of templates. Default `emails`. */\n\treadonly emails?: string;\n\t/** `nested` writes `dist/en/verify-email.html`; `flat` writes `dist/verify-email.en.html`. Default `nested`. */\n\treadonly layout?: Layout;\n\t/**\n\t * Catalogues under the project's own, as a package ships them —\n\t * `[uiCatalogues]` from `@nxgt/mail-ui`. Each is merged key by key under\n\t * the next, and the project's `<locale>.json` over all of them.\n\t */\n\treadonly catalogues?: readonly Catalogues[];\n\t/**\n\t * Folders of templates under the project's own, as a package ships them —\n\t * `presets().templates` from `@nxgt/mail-presets`. A template in the\n\t * project's `emails/` replaces a package's of the same name.\n\t */\n\treadonly templates?: readonly TemplateSource[];\n\t/**\n\t * The module typing the renderer, written after each build: `MailEmails`,\n\t * for `createMailRenderer<MailEmails>(…)` in the code that sends. A path\n\t * from where `maizzle` runs — outside the project too, as\n\t * `../api/src/generated/mail.ts` — or `false` to write none. Default\n\t * `generated/mail.ts`.\n\t */\n\treadonly rendererTypes?: string | false;\n}\n\n/** Where the wrappers go, under the project. */\nexport const WRAPPERS_DIR = '.maizzle/
|
|
8
|
+
"import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, relative, resolve, sep } from 'node:path';\nimport { isMainThread } from 'node:worker_threads';\nimport { defineMailPlugin, type MailPlugin } from '@nxgt/mail-config';\nimport {\n\ttype Catalogue,\n\ttype Catalogues,\n\tcheckCatalogues,\n\tlayerCatalogues,\n\ttype Messages,\n} from './catalogues';\nimport { buildManifest } from './manifest';\nimport {\n\tcheckTemplates,\n\ttype TemplateSource,\n\ttemplateFolders,\n} from './sources';\nimport { templateProperties } from './template';\nimport { createFormatter } from './translator';\nimport {\n\tRENDERER_TYPES_FILE,\n\trendererTypes,\n\tTYPES_FILE,\n\ttemplateTypes,\n\twriteIfChanged,\n\twriteRendererTypes,\n} from './types';\nimport {\n\ttype Layout,\n\tparseEntry,\n\twatchTemplates,\n\twriteWrappers,\n} from './wrappers';\n\nexport interface I18nOptions {\n\t/** Every locale the project writes, as BCP 47 tags: `['en', 'fr']`. */\n\treadonly locales: readonly string[];\n\t/** The reference every other locale is checked against. Default the first locale. */\n\treadonly fallbackLocale?: string;\n\t/** The folder of `<locale>.json` catalogues. Default `locales`. */\n\treadonly dir?: string;\n\t/** The folder of templates. Default `emails`. */\n\treadonly emails?: string;\n\t/** `nested` writes `dist/en/verify-email.html`; `flat` writes `dist/verify-email.en.html`. Default `nested`. */\n\treadonly layout?: Layout;\n\t/**\n\t * Catalogues under the project's own, as a package ships them —\n\t * `[uiCatalogues]` from `@nxgt/mail-ui`. Each is merged key by key under\n\t * the next, and the project's `<locale>.json` over all of them.\n\t */\n\treadonly catalogues?: readonly Catalogues[];\n\t/**\n\t * Folders of templates under the project's own, as a package ships them —\n\t * `presets().templates` from `@nxgt/mail-presets`. A template in the\n\t * project's `emails/` replaces a package's of the same name.\n\t */\n\treadonly templates?: readonly TemplateSource[];\n\t/**\n\t * The module typing the renderer, written after each build: `MailEmails`,\n\t * for `createMailRenderer<MailEmails>(…)` in the code that sends. A path\n\t * from where `maizzle` runs — outside the project too, as\n\t * `../api/src/generated/mail.ts` — or `false` to write none. Default\n\t * `generated/mail.ts`.\n\t */\n\treadonly rendererTypes?: string | false;\n}\n\n/** Where the wrappers go, under the project. */\nexport const WRAPPERS_DIR = '.maizzle/emails';\n\n/** The manifest's name, in the output folder. */\nexport const MANIFEST_FILE = 'mail-manifest.json';\n\nconst LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\nfunction checkOptions(options: I18nOptions): void {\n\tif (typeof options !== 'object' || options === null) {\n\t\tthrow new TypeError(\n\t\t\t\"i18n: options must be an object, as { locales: ['en', 'fr'] }\",\n\t\t);\n\t}\n\tconst { locales } = options;\n\tif (!Array.isArray(locales) || locales.length === 0) {\n\t\tthrow new TypeError(\n\t\t\t\"i18n: locales must hold at least one locale, as ['en', 'fr']\",\n\t\t);\n\t}\n\tfor (const locale of locales) {\n\t\tif (typeof locale !== 'string' || !LOCALE.test(locale)) {\n\t\t\tthrow new TypeError(\n\t\t\t\t'i18n: locales holds something that is not a locale — write each as a BCP 47 tag, as en or pt-BR',\n\t\t\t);\n\t\t}\n\t}\n\tif (new Set(locales).size !== locales.length) {\n\t\tthrow new TypeError('i18n: locales holds the same locale twice');\n\t}\n\tif (\n\t\toptions.fallbackLocale !== undefined &&\n\t\t!locales.includes(options.fallbackLocale)\n\t) {\n\t\tthrow new TypeError('i18n: fallbackLocale must be one of locales');\n\t}\n\tfor (const key of ['dir', 'emails'] as const) {\n\t\tconst value = options[key];\n\t\tif (value !== undefined && (typeof value !== 'string' || value === '')) {\n\t\t\tthrow new TypeError(`i18n: ${key} must be a folder of the project`);\n\t\t}\n\t}\n\tif (\n\t\toptions.layout !== undefined &&\n\t\toptions.layout !== 'nested' &&\n\t\toptions.layout !== 'flat'\n\t) {\n\t\tthrow new TypeError(\"i18n: layout must be 'nested' or 'flat'\");\n\t}\n\tconst { catalogues } = options;\n\tif (\n\t\tcatalogues !== undefined &&\n\t\t(!Array.isArray(catalogues) ||\n\t\t\t!catalogues.every(\n\t\t\t\t(source) => isObject(source) && Object.values(source).every(isObject),\n\t\t\t))\n\t) {\n\t\tthrow new TypeError(\n\t\t\t'i18n: catalogues must be a list of catalogues by locale, as [{ en: {...}, fr: {...} }]',\n\t\t);\n\t}\n\tconst typesPath = options.rendererTypes;\n\tif (\n\t\ttypesPath !== undefined &&\n\t\ttypesPath !== false &&\n\t\t(typeof typesPath !== 'string' || !typesPath.endsWith('.ts'))\n\t) {\n\t\tthrow new TypeError(\n\t\t\t'i18n: rendererTypes must be the path of a .ts file, as generated/mail.ts, or false',\n\t\t);\n\t}\n\tcheckTemplates(options.templates);\n}\n\n/** Reads `<dir>/<locale>.json` for each locale. A missing or broken file **throws**. */\nfunction readCatalogues(\n\tdir: string,\n\tdirName: string,\n\tlocales: readonly string[],\n): Record<string, Catalogue> {\n\tconst out: Record<string, Catalogue> = {};\n\tfor (const locale of locales) {\n\t\tconst file = join(dir, `${locale}.json`);\n\t\tconst name = `${dirName}/${locale}.json`;\n\t\tif (!existsSync(file)) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${name} is missing — every locale has a catalogue`,\n\t\t\t);\n\t\t}\n\t\ttry {\n\t\t\tout[locale] = JSON.parse(readFileSync(file, 'utf8'));\n\t\t} catch {\n\t\t\tthrow new Error(`i18n: ${name} is not valid JSON`);\n\t\t}\n\t}\n\treturn out;\n}\n\n/**\n * The i18n plugin, for `defineMailConfig`:\n *\n * ```ts\n * defineMailConfig({ plugins: [i18n({ locales: ['en', 'fr'] })] });\n * ```\n *\n * It checks `locales/<locale>.json`, writes one wrapper per template and\n * locale under `.maizzle/emails/` so one build writes every locale, gives each\n * template `t`, `locale` and `placeholder`, and writes\n * `dist/mail-manifest.json`. A catalogue or a template that cannot be right\n * **fails the build**, naming the locale and the key.\n */\nexport function i18n(options: I18nOptions): MailPlugin {\n\tcheckOptions(options);\n\tconst { locales } = options;\n\tconst fallbackLocale = options.fallbackLocale ?? (locales[0] as string);\n\tconst layout = options.layout ?? 'nested';\n\tconst dirName = options.dir ?? 'locales';\n\tconst emailsName = options.emails ?? 'emails';\n\tconst cwd = process.cwd();\n\tconst emailsDir = resolve(cwd, emailsName);\n\tconst wrappersDir = resolve(cwd, WRAPPERS_DIR);\n\n\tconst messages = checkCatalogues(\n\t\tlayerCatalogues(\n\t\t\toptions.catalogues ?? [],\n\t\t\treadCatalogues(resolve(cwd, dirName), dirName, locales),\n\t\t),\n\t\tlocales,\n\t\tfallbackLocale,\n\t);\n\tconst reference = messages.get(fallbackLocale) as Messages;\n\tconst format = createFormatter('i18n');\n\tconst regenerate = () =>\n\t\twriteWrappers({\n\t\t\tfolders: templateFolders(\n\t\t\t\t{ dir: emailsDir, name: emailsName },\n\t\t\t\toptions.templates ?? [],\n\t\t\t),\n\t\t\twrappersDir,\n\t\t\tlocales,\n\t\t\tlayout,\n\t\t});\n\t// A parallel build loads the config again in each worker: only the main\n\t// thread writes, so two workers never write the same file.\n\tif (isMainThread) {\n\t\tregenerate();\n\t\twriteIfChanged(\n\t\t\tresolve(cwd, TYPES_FILE),\n\t\t\ttemplateTypes(reference, fallbackLocale),\n\t\t);\n\t}\n\n\treturn defineMailPlugin({\n\t\tname: 'i18n',\n\t\tcontent: [`${wrappersDir}/**/*.vue`],\n\t\t// `maizzle serve` watches locales/ already; another folder is added.\n\t\t...(dirName === 'locales' ? {} : { server: { watch: [`${dirName}/**`] } }),\n\t\tvite: { plugins: [watchTemplates(emailsDir, regenerate)] },\n\t\tbeforeRender({ config, template }) {\n\t\t\tconst path = relative(\n\t\t\t\twrappersDir,\n\t\t\t\tjoin(template.path.dir, template.path.name),\n\t\t\t)\n\t\t\t\t.split(sep)\n\t\t\t\t.join('/');\n\t\t\tconst entry = parseEntry(path, layout, locales);\n\t\t\tif (entry === null) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`i18n: ${relative(cwd, join(template.path.dir, template.path.base))} is not built through the i18n plugin — leave content to it, and put templates in ${emailsName}/`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconfig.vue ??= {};\n\t\t\tconfig.vue.globalProperties = {\n\t\t\t\t...config.vue.globalProperties,\n\t\t\t\t...templateProperties({\n\t\t\t\t\t...entry,\n\t\t\t\t\tmessages: messages.get(entry.locale) as Messages,\n\t\t\t\t\treference,\n\t\t\t\t\tformat,\n\t\t\t\t}),\n\t\t\t};\n\t\t},\n\t\tafterBuild({ files, config }) {\n\t\t\tconst outputDir = resolve(cwd, config.output?.path ?? 'dist');\n\t\t\tconst manifest = buildManifest({\n\t\t\t\tfiles,\n\t\t\t\toutputDir,\n\t\t\t\thtmlExtension: config.output?.extension ?? 'html',\n\t\t\t\tlayout,\n\t\t\t\tlocales,\n\t\t\t\tfallbackLocale,\n\t\t\t\tmessages,\n\t\t\t});\n\t\t\twriteFileSync(\n\t\t\t\tjoin(outputDir, MANIFEST_FILE),\n\t\t\t\t`${JSON.stringify(manifest, null, '\\t')}\\n`,\n\t\t\t);\n\t\t\tconst typesFile = options.rendererTypes ?? RENDERER_TYPES_FILE;\n\t\t\tif (typesFile !== false) {\n\t\t\t\twriteRendererTypes(\n\t\t\t\t\tresolve(cwd, typesFile),\n\t\t\t\t\ttypesFile,\n\t\t\t\t\trendererTypes(manifest),\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t});\n}\n",
|
|
9
9
|
"import {\n\ttype MessageFormatElement,\n\tparse,\n\tTYPE,\n} from '@formatjs/icu-messageformat-parser';\n\n/**\n * A catalogue as written: nested objects whose leaves are ICU messages, the\n * conventions of `@nxgt/i18n`.\n *\n * ```json\n * { \"verifyEmail\": { \"subject\": \"Confirm your e-mail address\" } }\n * ```\n */\nexport interface Catalogue {\n\treadonly [key: string]: string | Catalogue;\n}\n\n/** A catalogue per locale: `{ en: {...}, fr: {...} }`. */\nexport type Catalogues = Readonly<Record<string, Catalogue>>;\n\n/**\n * What an argument is, from the way a message uses it: `{n, number}` and\n * `{n, plural, …}` a number, `{at, date}` a date, anything else a string.\n */\nexport type ArgumentKind = 'string' | 'number' | 'date';\n\n/** One message, checked, with the kind of each argument it uses. */\nexport interface Message {\n\treadonly text: string;\n\treadonly args: ReadonlyMap<string, ArgumentKind>;\n\t/** The arguments a `{x, select, …}` chooses on: a placeholder would always choose `other`. */\n\treadonly selects: ReadonlySet<string>;\n}\n\n/** Every message of one locale, by dotted key: `verifyEmail.subject`. */\nexport type Messages = ReadonlyMap<string, Message>;\n\nconst SEGMENT = /^[a-z][a-zA-Z0-9]*$/;\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** `over` merged into `under` key by key: an object is merged, anything else replaces. */\nfunction mergeCatalogue(under: Catalogue, over: Catalogue): Catalogue {\n\tconst out: Record<string, string | Catalogue> = { ...under };\n\tfor (const [key, value] of Object.entries(over)) {\n\t\tconst below = Object.hasOwn(out, key) ? out[key] : undefined;\n\t\t// Defined, not assigned: `out.__proto__ = …` would set the prototype and\n\t\t// hide the key from the check that refuses it.\n\t\tObject.defineProperty(out, key, {\n\t\t\tvalue:\n\t\t\t\tisObject(below) && isObject(value)\n\t\t\t\t\t? mergeCatalogue(below, value)\n\t\t\t\t\t: value,\n\t\t\tenumerable: true,\n\t\t\twritable: true,\n\t\t\tconfigurable: true,\n\t\t});\n\t}\n\treturn out;\n}\n\n/**\n * Each of `project`'s locales, with `sources` merged under it in order: a\n * source's locale the project does not have is left out.\n */\nexport function layerCatalogues(\n\tsources: readonly Catalogues[],\n\tproject: Record<string, Catalogue>,\n): Record<string, Catalogue> {\n\tconst out: Record<string, Catalogue> = {};\n\tfor (const [locale, catalogue] of Object.entries(project)) {\n\t\tout[locale] = [...sources.map((source) => source[locale]), catalogue]\n\t\t\t.filter((layer): layer is Catalogue => layer !== undefined)\n\t\t\t.reduce(mergeCatalogue, {});\n\t}\n\treturn out;\n}\n\nfunction flatten(\n\tcatalogue: unknown,\n\tlocale: string,\n\tprefix: string,\n\tinto: Map<string, string>,\n): void {\n\tif (!isObject(catalogue)) {\n\t\tthrow new Error(\n\t\t\t`i18n: ${locale}: ${prefix || 'the catalogue'} must be an object of messages`,\n\t\t);\n\t}\n\tfor (const [segment, value] of Object.entries(catalogue)) {\n\t\tconst key = prefix === '' ? segment : `${prefix}.${segment}`;\n\t\tif (!SEGMENT.test(segment)) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} is not camelCase — every segment of a key is camelCase, and nested rather than dotted, as verifyEmail.title`,\n\t\t\t);\n\t\t}\n\t\tif (typeof value === 'string') into.set(key, value);\n\t\telse if (isObject(value)) flatten(value, locale, key, into);\n\t\telse {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} must be a message (a string) or an object of messages`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction collect(\n\telements: readonly MessageFormatElement[],\n\tuses: Map<string, Set<ArgumentKind | 'plain' | 'select'>>,\n): void {\n\tconst use = (name: string, kind: ArgumentKind | 'plain' | 'select') =>\n\t\tuses.set(name, (uses.get(name) ?? new Set()).add(kind));\n\tfor (const element of elements) {\n\t\tif (element.type === TYPE.argument) use(element.value, 'plain');\n\t\telse if (element.type === TYPE.number) use(element.value, 'number');\n\t\telse if (element.type === TYPE.date || element.type === TYPE.time) {\n\t\t\tuse(element.value, 'date');\n\t\t} else if (element.type === TYPE.select || element.type === TYPE.plural) {\n\t\t\tuse(element.value, element.type === TYPE.plural ? 'number' : 'select');\n\t\t\tfor (const option of Object.values(element.options)) {\n\t\t\t\tcollect(option.value, uses);\n\t\t\t}\n\t\t} else if (element.type === TYPE.tag) collect(element.children, uses);\n\t}\n}\n\n/**\n * Parses a message and types its arguments. The parser's own error is not\n * kept as the cause: it carries the text of the message, and a build failure\n * names a key, never a text.\n */\nfunction analyse(text: string, locale: string, key: string): Message {\n\tlet ast: MessageFormatElement[];\n\ttry {\n\t\tast = parse(text, { ignoreTag: true, shouldParseSkeletons: true });\n\t} catch (cause) {\n\t\tconst reason = cause instanceof Error ? cause.message : 'refused';\n\t\tthrow new Error(\n\t\t\t`i18n: ${locale}: ${key} is not a valid ICU message (${reason})`,\n\t\t);\n\t}\n\tconst uses = new Map<string, Set<ArgumentKind | 'plain' | 'select'>>();\n\tcollect(ast, uses);\n\tconst args = new Map<string, ArgumentKind>();\n\tconst selects = new Set<string>();\n\tfor (const [name, kinds] of uses) {\n\t\tif (!SEGMENT.test(name)) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} uses {${name}}, which is not camelCase — an argument is a camelCase name, as {firstName}`,\n\t\t\t);\n\t\t}\n\t\tif (kinds.has('select')) selects.add(name);\n\t\tconst typed = [\n\t\t\t...new Set(\n\t\t\t\t[...kinds]\n\t\t\t\t\t.filter((kind) => kind !== 'plain')\n\t\t\t\t\t.map((kind) => (kind === 'select' ? 'string' : kind)),\n\t\t\t),\n\t\t];\n\t\tif (typed.length > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} uses {${name}} as ${typed.join(' and as ')}`,\n\t\t\t);\n\t\t}\n\t\targs.set(name, (typed[0] as ArgumentKind | undefined) ?? 'string');\n\t}\n\treturn { text, args, selects };\n}\n\n/**\n * Checks `locale` against the fallback locale: the same keys, and no\n * argument the fallback does not declare, or declares as another kind. A\n * translation may leave an argument out.\n */\nfunction compare(\n\tlocale: string,\n\tmessages: Messages,\n\tfallbackLocale: string,\n\treference: Messages,\n): void {\n\tfor (const key of [...reference.keys()].sort()) {\n\t\tif (!messages.has(key)) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} is missing — ${fallbackLocale}, the fallback locale, has it`,\n\t\t\t);\n\t\t}\n\t}\n\tfor (const key of [...messages.keys()].sort()) {\n\t\tconst declared = reference.get(key);\n\t\tif (declared === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`i18n: ${locale}: ${key} is not a key of ${fallbackLocale}, the fallback locale`,\n\t\t\t);\n\t\t}\n\t\tfor (const [name, kind] of (messages.get(key) as Message).args) {\n\t\t\tconst expected = declared.args.get(name);\n\t\t\tif (expected === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`i18n: ${locale}: ${key} uses {${name}}, which ${fallbackLocale} does not declare`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (expected !== kind) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`i18n: ${locale}: ${key} uses {${name}} as ${kind}, and ${fallbackLocale} declares it as ${expected}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Checks the catalogues of every locale and answers their messages. A\n * catalogue that is not objects of camelCase keys, a message that does not\n * parse, and a locale that differs from the fallback locale in its keys or\n * its arguments **throw**, naming the locale and the key.\n */\nexport function checkCatalogues(\n\tcatalogues: Catalogues,\n\tlocales: readonly string[],\n\tfallbackLocale: string,\n): ReadonlyMap<string, Messages> {\n\tconst out = new Map<string, Messages>();\n\tfor (const locale of locales) {\n\t\tconst flat = new Map<string, string>();\n\t\tflatten(catalogues[locale], locale, '', flat);\n\t\tout.set(\n\t\t\tlocale,\n\t\t\tnew Map(\n\t\t\t\t[...flat].map(([key, text]) => [key, analyse(text, locale, key)]),\n\t\t\t),\n\t\t);\n\t}\n\tconst reference = out.get(fallbackLocale) as Messages;\n\tfor (const locale of locales) {\n\t\tif (locale !== fallbackLocale) {\n\t\t\tcompare(locale, out.get(locale) as Messages, fallbackLocale, reference);\n\t\t}\n\t}\n\treturn out;\n}\n",
|
|
10
10
|
"import { isAbsolute } from 'node:path';\nimport type { TemplateFolder } from './wrappers';\n\n/** A package's folder of templates, for `i18n({ templates })`. */\nexport interface TemplateSource {\n\t/** The folder, absolute. */\n\treadonly dir: string;\n\t/** The e-mails of the folder to build, as `['verify-email']`. Default every one. */\n\treadonly emails?: readonly [string, ...string[]];\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isSource = (source: unknown): source is TemplateSource =>\n\tisObject(source) &&\n\ttypeof source.dir === 'string' &&\n\tisAbsolute(source.dir) &&\n\t(source.emails === undefined ||\n\t\t(Array.isArray(source.emails) &&\n\t\t\tsource.emails.length > 0 &&\n\t\t\tsource.emails.every((email) => typeof email === 'string') &&\n\t\t\tnew Set(source.emails).size === source.emails.length));\n\n/** Refuses a `templates` option that is not a list of {@link TemplateSource}. */\nexport function checkTemplates(\n\ttemplates: unknown,\n): asserts templates is readonly TemplateSource[] | undefined {\n\tif (\n\t\ttemplates !== undefined &&\n\t\t(!Array.isArray(templates) || !templates.every(isSource))\n\t) {\n\t\tthrow new TypeError(\n\t\t\t\"i18n: templates must be a list of template folders, as [{ dir: '/abs/path/emails' }] — emails, when given, names at least one, each once\",\n\t\t);\n\t}\n}\n\n/**\n * The folders the wrappers are written from: the project's `emails/` first,\n * so its template replaces a package's of the same name, then each package's.\n */\nexport function templateFolders(\n\temails: { readonly dir: string; readonly name: string },\n\ttemplates: readonly TemplateSource[],\n): TemplateFolder[] {\n\treturn [\n\t\t{ dir: emails.dir, label: emails.name },\n\t\t...templates.map((source, index) => ({\n\t\t\tdir: source.dir,\n\t\t\tlabel: `templates[${index}]`,\n\t\t\tpackaged: true,\n\t\t\t...(source.emails && { only: source.emails }),\n\t\t})),\n\t];\n}\n",
|
|
11
11
|
"import type { ArgumentKind, Messages } from './catalogues';\nimport { placeholderMark } from './manifest';\nimport type { createFormatter, MessageArgs } from './translator';\n\nconst NAME = /^[a-z][a-zA-Z0-9]*$/;\n\nconst KINDS: Record<ArgumentKind, (value: unknown) => boolean> = {\n\tstring: (value) => typeof value === 'string' || typeof value === 'number',\n\tnumber: (value) => typeof value === 'number',\n\tdate: (value) => value instanceof Date || typeof value === 'number',\n};\n\nconst isPlaceholder = (value: unknown) =>\n\ttypeof value === 'string' && /^\\{\\{ [a-z][a-zA-Z0-9]* \\}\\}$/.test(value);\n\nconst kindOf = (value: unknown) =>\n\tvalue === null ? 'null' : value instanceof Date ? 'date' : typeof value;\n\n/**\n * What one template gets in one locale: `t`, `locale` and `placeholder`.\n *\n * `t` is checked against the fallback locale's message, which declares every\n * argument: an unknown key, an argument left out, one the message does not\n * use, or one of the wrong kind **fails the build**, naming the locale, the\n * template and the key.\n */\nexport function templateProperties(options: {\n\treadonly email: string;\n\treadonly locale: string;\n\treadonly messages: Messages;\n\treadonly reference: Messages;\n\treadonly format: ReturnType<typeof createFormatter>;\n}) {\n\tconst { email, locale, messages, reference, format } = options;\n\tconst where = `i18n: ${locale}: ${email}`;\n\treturn {\n\t\tlocale,\n\t\tt(key: string, args: MessageArgs = {}): string {\n\t\t\tconst message = typeof key === 'string' ? messages.get(key) : undefined;\n\t\t\tconst declared = typeof key === 'string' ? reference.get(key) : undefined;\n\t\t\tif (message === undefined || declared === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`${where} calls t('${String(key)}'), which is not a key of the catalogues`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (typeof args !== 'object' || args === null) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`${where} calls t('${key}') with arguments that are not an object, as { name: placeholder('name') }`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tfor (const [name, kind] of declared.args) {\n\t\t\t\tif (!Object.hasOwn(args, name)) {\n\t\t\t\t\tthrow new Error(`${where} calls t('${key}') without {${name}}`);\n\t\t\t\t}\n\t\t\t\tif (declared.selects.has(name) && isPlaceholder(args[name])) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`${where} passes a placeholder to {${name}}, which ${key} chooses on with a select — a placeholder always chooses other`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!KINDS[kind](args[name])) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`${where} passes {${name}} to ${key} as a ${kindOf(args[name])} — the message uses it as a ${kind}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const name of Object.keys(args)) {\n\t\t\t\tif (!declared.args.has(name)) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`${where} passes {${name}} to ${key}, which does not use it`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn format(locale, key, message.text, args);\n\t\t},\n\t\tplaceholder(name: string): string {\n\t\t\tif (typeof name !== 'string' || !NAME.test(name)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`${where} calls placeholder() with a name that is not camelCase — as placeholder('firstName')`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn placeholderMark(name);\n\t\t},\n\t};\n}\n",
|
|
12
|
-
"import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { ArgumentKind, Messages } from './catalogues';\nimport type { Manifest } from './manifest';\n\n/** Where the template types go, under the project: `.maizzle/*.d.ts` is in the starter's `tsconfig.json`. */\nexport const TYPES_FILE = '.maizzle/nxgt-mail-i18n.d.ts';\n\n/** What `t` accepts for each kind — as the build checks it. */\nconst TYPE_OF: Readonly<Record<ArgumentKind, string>> = {\n\tstring: 'string | number',\n\tnumber: 'number',\n\tdate: 'Date | number',\n};\n\n/**\n * The declaration file that types `t` in the templates: each key of the\n * `reference` catalogue, with its arguments, so an editor completes a key and\n * flags an unknown one or a missing argument. Keys and names are checked\n * before this runs, so they are safe to write quoted.\n */\nexport function templateTypes(\n\treference: Messages,\n\tfallbackLocale: string,\n): string {\n\tconst entries = [...reference.entries()]\n\t\t.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\t\t.map(([key, message]) => {\n\t\t\tconst args = [...message.args.entries()]\n\t\t\t\t.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\t\t\t\t.map(([name, kind]) => `${name}: ${TYPE_OF[kind]}`);\n\t\t\treturn `\\t\\t'${key}': { ${args.join('; ')}${args.length > 0 ? ' ' : ''}};`;\n\t\t});\n\treturn [\n\t\t`// Generated by @nxgt/mail-i18n from the ${fallbackLocale} catalogue, each time the config loads.`,\n\t\t'// Never edited, never committed: it types t() in the templates.',\n\t\t\"import type {} from '@nxgt/mail-i18n';\",\n\t\t'',\n\t\t\"declare module '@nxgt/mail-i18n' {\",\n\t\t'\\tinterface TemplateMessages {',\n\t\t...entries,\n\t\t'\\t}',\n\t\t'}',\n\t\t'',\n\t].join('\\n');\n}\n\n/** The first line of the renderer's types: a file without it is not the plugin's to replace. */\nconst RENDERER_TYPES_HEADER = '// Generated by @nxgt/mail-i18n from the build';\n\n/** Where the renderer's types go, under the project, by default. */\nexport const RENDERER_TYPES_FILE = 'generated/mail.ts';\n\n/**\n * The module that types the renderer: `MailEmails`, each e-mail of the build\n * with the variables it takes at send time, for\n * `createMailRenderer<MailEmails>(…)`. A URL variable takes a string; any\n * other a string or a number, as `render` writes it. `@nxgt/mail`'s\n * `RenderArguments` reads `Readonly<Record<string, never>>` as an e-mail\n * without variables, and its type specs copy this output: change them\n * together.\n */\nexport function rendererTypes(manifest: Manifest): string {\n\tconst entries = Object.entries(manifest.emails)\n\t\t.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\t\t.map(([email, { variables, urlVariables }]) => {\n\t\t\tconst fields = variables.map(\n\t\t\t\t(name) =>\n\t\t\t\t\t`readonly ${name}: ${urlVariables.includes(name) ? 'string' : 'string | number'}`,\n\t\t\t);\n\t\t\tconst type =\n\t\t\t\tfields.length > 0\n\t\t\t\t\t? `{ ${fields.join('; ')} }`\n\t\t\t\t\t: 'Readonly<Record<string, never>>';\n\t\t\t// A name is a file's path, quoted as JSON: valid TypeScript whatever it holds.\n\t\t\treturn `\\t${JSON.stringify(email)}: ${type};`;\n\t\t});\n\treturn [\n\t\t`${RENDERER_TYPES_HEADER}, after each maizzle build.`,\n\t\t'// Never edited
|
|
12
|
+
"import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { ArgumentKind, Messages } from './catalogues';\nimport type { Manifest } from './manifest';\n\n/** Where the template types go, under the project: `.maizzle/*.d.ts` is in the starter's `tsconfig.json`. */\nexport const TYPES_FILE = '.maizzle/nxgt-mail-i18n.d.ts';\n\n/** What `t` accepts for each kind — as the build checks it. */\nconst TYPE_OF: Readonly<Record<ArgumentKind, string>> = {\n\tstring: 'string | number',\n\tnumber: 'number',\n\tdate: 'Date | number',\n};\n\n/**\n * The declaration file that types `t` in the templates: each key of the\n * `reference` catalogue, with its arguments, so an editor completes a key and\n * flags an unknown one or a missing argument. Keys and names are checked\n * before this runs, so they are safe to write quoted.\n */\nexport function templateTypes(\n\treference: Messages,\n\tfallbackLocale: string,\n): string {\n\tconst entries = [...reference.entries()]\n\t\t.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\t\t.map(([key, message]) => {\n\t\t\tconst args = [...message.args.entries()]\n\t\t\t\t.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\t\t\t\t.map(([name, kind]) => `${name}: ${TYPE_OF[kind]}`);\n\t\t\treturn `\\t\\t'${key}': { ${args.join('; ')}${args.length > 0 ? ' ' : ''}};`;\n\t\t});\n\treturn [\n\t\t`// Generated by @nxgt/mail-i18n from the ${fallbackLocale} catalogue, each time the config loads.`,\n\t\t'// Never edited, never committed: it types t() in the templates.',\n\t\t\"import type {} from '@nxgt/mail-i18n';\",\n\t\t'',\n\t\t\"declare module '@nxgt/mail-i18n' {\",\n\t\t'\\tinterface TemplateMessages {',\n\t\t...entries,\n\t\t'\\t}',\n\t\t'}',\n\t\t'',\n\t].join('\\n');\n}\n\n/** The first line of the renderer's types: a file without it is not the plugin's to replace. */\nconst RENDERER_TYPES_HEADER = '// Generated by @nxgt/mail-i18n from the build';\n\n/** Where the renderer's types go, under the project, by default. */\nexport const RENDERER_TYPES_FILE = 'generated/mail.ts';\n\n/**\n * The module that types the renderer: `MailEmails`, each e-mail of the build\n * with the variables it takes at send time, for\n * `createMailRenderer<MailEmails>(…)`. A URL variable takes a string; any\n * other a string or a number, as `render` writes it. `@nxgt/mail`'s\n * `RenderArguments` reads `Readonly<Record<string, never>>` as an e-mail\n * without variables, and its type specs copy this output: change them\n * together.\n */\nexport function rendererTypes(manifest: Manifest): string {\n\tconst entries = Object.entries(manifest.emails)\n\t\t.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\t\t.map(([email, { variables, urlVariables }]) => {\n\t\t\tconst fields = variables.map(\n\t\t\t\t(name) =>\n\t\t\t\t\t`readonly ${name}: ${urlVariables.includes(name) ? 'string' : 'string | number'}`,\n\t\t\t);\n\t\t\tconst type =\n\t\t\t\tfields.length > 0\n\t\t\t\t\t? `{ ${fields.join('; ')} }`\n\t\t\t\t\t: 'Readonly<Record<string, never>>';\n\t\t\t// A name is a file's path, quoted as JSON: valid TypeScript whatever it holds.\n\t\t\treturn `\\t${JSON.stringify(email)}: ${type};`;\n\t\t});\n\treturn [\n\t\t`${RENDERER_TYPES_HEADER}, after each maizzle build.`,\n\t\t'// Never edited, never committed: git-ignore it, and build before type-checking.',\n\t\t'',\n\t\t'/** The e-mails of the build, each with the variables it takes when it is sent. */',\n\t\t'export interface MailEmails {',\n\t\t...entries,\n\t\t'}',\n\t\t'',\n\t].join('\\n');\n}\n\n/**\n * Writes the renderer's types to `file`, shown as `path` — unless `file` holds\n * something the plugin did not write: that **throws**, and the file is kept.\n */\nexport function writeRendererTypes(\n\tfile: string,\n\tpath: string,\n\tsource: string,\n): void {\n\tif (\n\t\texistsSync(file) &&\n\t\t!readFileSync(file, 'utf8').startsWith(RENDERER_TYPES_HEADER)\n\t) {\n\t\tthrow new Error(\n\t\t\t`i18n: ${path} was not written by i18n() — point rendererTypes at a file of its own`,\n\t\t);\n\t}\n\twriteIfChanged(file, source);\n}\n\n/** Writes `source` to `file` only when it changed, so an editor or a watcher sees a change only where there is one. */\nexport function writeIfChanged(file: string, source: string): void {\n\tif (existsSync(file) && readFileSync(file, 'utf8') === source) return;\n\tmkdirSync(dirname(file), { recursive: true });\n\twriteFileSync(file, source);\n}\n"
|
|
13
13
|
],
|
|
14
14
|
"mappings": "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",
|
|
15
|
-
"debugId": "
|
|
15
|
+
"debugId": "8EAF60F48839FE4D64756E2164756E21",
|
|
16
16
|
"names": []
|
|
17
17
|
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface I18nOptions {
|
|
|
35
35
|
readonly rendererTypes?: string | false;
|
|
36
36
|
}
|
|
37
37
|
/** Where the wrappers go, under the project. */
|
|
38
|
-
export declare const WRAPPERS_DIR = ".maizzle/
|
|
38
|
+
export declare const WRAPPERS_DIR = ".maizzle/emails";
|
|
39
39
|
/** The manifest's name, in the output folder. */
|
|
40
40
|
export declare const MANIFEST_FILE = "mail-manifest.json";
|
|
41
41
|
/**
|
|
@@ -46,7 +46,7 @@ export declare const MANIFEST_FILE = "mail-manifest.json";
|
|
|
46
46
|
* ```
|
|
47
47
|
*
|
|
48
48
|
* It checks `locales/<locale>.json`, writes one wrapper per template and
|
|
49
|
-
* locale under `.maizzle/
|
|
49
|
+
* locale under `.maizzle/emails/` so one build writes every locale, gives each
|
|
50
50
|
* template `t`, `locale` and `placeholder`, and writes
|
|
51
51
|
* `dist/mail-manifest.json`. A catalogue or a template that cannot be right
|
|
52
52
|
* **fails the build**, naming the locale and the key.
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAEN,KAAK,UAAU,EAIf,MAAM,cAAc,CAAC;AAEtB,OAAO,EAEN,KAAK,cAAc,EAEnB,MAAM,WAAW,CAAC;AAWnB,OAAO,EACN,KAAK,MAAM,EAIX,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,WAAW;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,qFAAqF;IACrF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,gHAAgH;IAChH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACxC;AAED,gDAAgD;AAChD,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAEN,KAAK,UAAU,EAIf,MAAM,cAAc,CAAC;AAEtB,OAAO,EAEN,KAAK,cAAc,EAEnB,MAAM,WAAW,CAAC;AAWnB,OAAO,EACN,KAAK,MAAM,EAIX,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,WAAW;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,qFAAqF;IACrF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,gHAAgH;IAChH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACxC;AAED,gDAAgD;AAChD,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAE9C,iDAAiD;AACjD,eAAO,MAAM,aAAa,uBAAuB,CAAC;AAiGlD;;;;;;;;;;;;GAYG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAgGrD"}
|
package/docs/guide/manifest.md
CHANGED
|
@@ -222,7 +222,7 @@ For the manifest above, the file holds exactly this:
|
|
|
222
222
|
|
|
223
223
|
```ts
|
|
224
224
|
// Generated by @nxgt/mail-i18n from the build, after each maizzle build.
|
|
225
|
-
// Never edited
|
|
225
|
+
// Never edited, never committed: git-ignore it, and build before type-checking.
|
|
226
226
|
|
|
227
227
|
/** The e-mails of the build, each with the variables it takes when it is sent. */
|
|
228
228
|
export interface MailEmails {
|
|
@@ -260,13 +260,28 @@ What each refusal looks like, and the untyped default, are in `@nxgt/mail`'s
|
|
|
260
260
|
to check.
|
|
261
261
|
- Its folder is created when missing.
|
|
262
262
|
|
|
263
|
-
###
|
|
263
|
+
### Git-ignore it, and build before type-checking
|
|
264
264
|
|
|
265
|
-
The file is
|
|
266
|
-
committed
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
265
|
+
The file is output of the build, as `dist/` is, so it is git-ignored, never
|
|
266
|
+
committed: a copy in git goes stale the moment a template changes, and each
|
|
267
|
+
build rewrites it anyway. Build before type-checking — locally and in CI:
|
|
268
|
+
|
|
269
|
+
```gitignore
|
|
270
|
+
dist
|
|
271
|
+
generated
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"scripts": {
|
|
277
|
+
"typecheck": "maizzle build && tsc --noEmit"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
A fresh clone that type-checks before its first build reports
|
|
283
|
+
`Cannot find module './generated/mail'` — see
|
|
284
|
+
[troubleshooting](../troubleshooting.md#cannot-find-module-generatedmail-or-its-corresponding-type-declarations).
|
|
270
285
|
|
|
271
286
|
The `generated/` folder holds generated code, never edited by hand: leave it
|
|
272
287
|
out of your formatter and linter, as Biome's `"includes": ["**",
|
package/docs/guide/templates.md
CHANGED
|
@@ -247,7 +247,7 @@ i18n({
|
|
|
247
247
|
The wrapper of a package's template imports it from where it is:
|
|
248
248
|
|
|
249
249
|
```vue
|
|
250
|
-
<!-- .maizzle/
|
|
250
|
+
<!-- .maizzle/emails/fr/verify-email.vue, generated; the path follows your install -->
|
|
251
251
|
<script setup>
|
|
252
252
|
import Email from '../../../node_modules/@nxgt/mail-presets/emails/verify-email.vue';
|
|
253
253
|
</script>
|
|
@@ -325,13 +325,13 @@ export default defineMailConfig({
|
|
|
325
325
|
## What the build writes
|
|
326
326
|
|
|
327
327
|
For each template and locale, the plugin generates a small file, a
|
|
328
|
-
**wrapper**, under `.maizzle/
|
|
328
|
+
**wrapper**, under `.maizzle/emails/`. The wrapper imports the template and
|
|
329
329
|
nothing else, and the plugin points Maizzle's `content` at these files. One
|
|
330
330
|
`maizzle build` then renders every template once per locale, and `t` knows
|
|
331
331
|
which locale each render is for.
|
|
332
332
|
|
|
333
333
|
```vue
|
|
334
|
-
<!-- .maizzle/
|
|
334
|
+
<!-- .maizzle/emails/fr/verify-email.vue, generated -->
|
|
335
335
|
<!-- Generated by @nxgt/mail-i18n: one per template and locale. Never edited, never committed. -->
|
|
336
336
|
<script setup>
|
|
337
337
|
import Email from '../../../emails/verify-email.vue';
|
|
@@ -351,8 +351,8 @@ default):
|
|
|
351
351
|
|
|
352
352
|
| `layout` | Wrapper | Built files |
|
|
353
353
|
| --- | --- | --- |
|
|
354
|
-
| `'nested'` (default) | `.maizzle/
|
|
355
|
-
| `'flat'` | `.maizzle/
|
|
354
|
+
| `'nested'` (default) | `.maizzle/emails/fr/auth/reset-password.vue` | `dist/fr/auth/reset-password.html`, `.txt` |
|
|
355
|
+
| `'flat'` | `.maizzle/emails/auth/reset-password.fr.vue` | `dist/auth/reset-password.fr.html`, `.txt` |
|
|
356
356
|
|
|
357
357
|
```ts
|
|
358
358
|
// maizzle.config.ts — the fixture's flat build, in its own folder
|
|
@@ -373,10 +373,10 @@ Then the plugin writes `mail-manifest.json` next to them. See
|
|
|
373
373
|
The dev UI lists each template once per locale, by its wrapper:
|
|
374
374
|
|
|
375
375
|
```text
|
|
376
|
-
.maizzle/
|
|
377
|
-
.maizzle/
|
|
378
|
-
.maizzle/
|
|
379
|
-
.maizzle/
|
|
376
|
+
.maizzle/emails/en/auth/reset-password.vue
|
|
377
|
+
.maizzle/emails/en/verify-email.vue
|
|
378
|
+
.maizzle/emails/fr/auth/reset-password.vue
|
|
379
|
+
.maizzle/emails/fr/verify-email.vue
|
|
380
380
|
```
|
|
381
381
|
|
|
382
382
|
Placeholders show as they are, `Bonjour {{ name }},`: the preview is the file
|
package/docs/roadmap.md
CHANGED
|
@@ -5,7 +5,11 @@ no dates here, and the version something shipped in is the only number.
|
|
|
5
5
|
|
|
6
6
|
## Now
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
- **The wrappers under `.maizzle/emails/`** — the files generated per
|
|
9
|
+
template and locale move from `.maizzle/i18n/` to `.maizzle/emails/`, so
|
|
10
|
+
`maizzle serve` lists the e-mails under `.maizzle/emails/en` rather than a
|
|
11
|
+
folder named after the plugin. The build output does not move. Built, not
|
|
12
|
+
yet published.
|
|
9
13
|
|
|
10
14
|
## Next
|
|
11
15
|
|
|
@@ -79,10 +83,10 @@ before is in the [CHANGELOG](../CHANGELOG.md).
|
|
|
79
83
|
CI.
|
|
80
84
|
- **The renderer typed by the build, v0.1.0** — after each `maizzle build`, the
|
|
81
85
|
plugin writes `generated/mail.ts`: `MailEmails`, each e-mail with the
|
|
82
|
-
variables it takes, a URL variable as a `string`.
|
|
83
|
-
`createMailRenderer<MailEmails>` from `@nxgt/mail/renderer`
|
|
86
|
+
variables it takes, a URL variable as a `string`. With it,
|
|
87
|
+
`createMailRenderer<MailEmails>` from `@nxgt/mail/renderer` refuses an
|
|
84
88
|
unknown e-mail, a missing or unknown variable, or a number for a URL at
|
|
85
|
-
compile time
|
|
89
|
+
compile time. Rewritten only when it changes;
|
|
86
90
|
`rendererTypes` moves it, or `false` turns it off.
|
|
87
91
|
- **A starter project, with v0.1.0** — the official Maizzle starter with
|
|
88
92
|
`defineMailConfig`, the i18n and the UI plugins wired in as the READMEs
|
package/docs/troubleshooting.md
CHANGED
|
@@ -32,6 +32,7 @@ template `emails/verify-email.vue`, and keys such as `verifyEmail.title`.
|
|
|
32
32
|
|
|
33
33
|
**Install and types**
|
|
34
34
|
- [Which `moduleResolution` is supported](#install-and-types)
|
|
35
|
+
- [`Cannot find module './generated/mail' or its corresponding type declarations`](#cannot-find-module-generatedmail-or-its-corresponding-type-declarations)
|
|
35
36
|
|
|
36
37
|
**Wiring**
|
|
37
38
|
- [`i18n: options must be an object, as { locales: ['en', 'fr'] }`](#i18n-options-must-be-an-object-as--locales-en-fr-)
|
|
@@ -96,6 +97,7 @@ template `emails/verify-email.vue`, and keys such as `verifyEmail.title`.
|
|
|
96
97
|
- [`[Vue warn]: Property "name" was accessed during render but is not defined on instance.`](#vue-warn-property-name-was-accessed-during-render-but-is-not-defined-on-instance)
|
|
97
98
|
- [A link's placeholder is prefixed with a domain](#a-links-placeholder-is-prefixed-with-a-domain)
|
|
98
99
|
- [`.maizzle/` shows up in `git status`](#maizzle-shows-up-in-git-status)
|
|
100
|
+
- [A `.maizzle/i18n/` folder is left after upgrading](#a-maizzlei18n-folder-is-left-after-upgrading)
|
|
99
101
|
- [`No templates found`, or old templates, when Maizzle is built from a worker thread](#no-templates-found-or-old-templates-when-maizzle-is-built-from-a-worker-thread)
|
|
100
102
|
- [The editor says `Property 't' does not exist` in a template, or completes no key](#the-editor-says-property-t-does-not-exist-in-a-template-or-completes-no-key)
|
|
101
103
|
- [The editor flags a key you just added to a catalogue](#the-editor-flags-a-key-you-just-added-to-a-catalogue)
|
|
@@ -110,6 +112,26 @@ Resolve as a bundler does (`"moduleResolution": "bundler"`, as Maizzle's jiti
|
|
|
110
112
|
loader does): that is the supported contract. `nodenext` and `node16` are out
|
|
111
113
|
of contract — they may work today, and are not tested.
|
|
112
114
|
|
|
115
|
+
### `Cannot find module './generated/mail' or its corresponding type declarations`
|
|
116
|
+
|
|
117
|
+
**When:** `tsc` or `vue-tsc` (TS2307), or the editor, on the code that sends
|
|
118
|
+
— `import type { MailEmails } from './generated/mail'` — in a fresh clone,
|
|
119
|
+
or in CI, before any `maizzle build`.
|
|
120
|
+
**Why:** `generated/mail.ts` is written by each build and git-ignored, as
|
|
121
|
+
`dist/` is: until the project has been built once, it does not exist.
|
|
122
|
+
**Fix:** build before type-checking, in the script CI runs too:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"scripts": {
|
|
127
|
+
"typecheck": "maizzle build && tsc --noEmit"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
In an editor, run `maizzle build` once after cloning, and again when an
|
|
133
|
+
e-mail gains or loses a variable.
|
|
134
|
+
|
|
113
135
|
---
|
|
114
136
|
|
|
115
137
|
## Wiring
|
|
@@ -614,7 +636,7 @@ emails/auth/reset-password.vue → messages under auth.resetPassword
|
|
|
614
636
|
**When:** `maizzle build`, on the first template, when the project sets
|
|
615
637
|
`content` itself.
|
|
616
638
|
**Why:** the plugin builds each template through one generated wrapper per
|
|
617
|
-
locale, under `.maizzle/
|
|
639
|
+
locale, under `.maizzle/emails/`, and lists those in `content`. A `content`
|
|
618
640
|
set in the project **replaces** the plugin's — arrays are not joined — so
|
|
619
641
|
Maizzle builds the templates directly, with no locale.
|
|
620
642
|
**Fix:** leave `content` out, and put every template in the `emails` folder
|
|
@@ -985,7 +1007,7 @@ export default defineMailConfig({
|
|
|
985
1007
|
### `.maizzle/` shows up in `git status`
|
|
986
1008
|
|
|
987
1009
|
**When:** after the first `maizzle build` or `maizzle serve`, `git status`
|
|
988
|
-
lists `.maizzle/
|
|
1010
|
+
lists `.maizzle/emails/en/verify-email.vue` and one file per template and
|
|
989
1011
|
locale.
|
|
990
1012
|
**Why:** those are the wrappers the plugin generates, one per template and
|
|
991
1013
|
locale, so that one build writes every locale. They are rewritten on every
|
|
@@ -999,13 +1021,28 @@ build, and removed when their template is.
|
|
|
999
1021
|
|
|
1000
1022
|
Never edit a wrapper: the change is lost on the next build.
|
|
1001
1023
|
|
|
1024
|
+
### A `.maizzle/i18n/` folder is left after upgrading
|
|
1025
|
+
|
|
1026
|
+
**When:** after upgrading `@nxgt/mail-i18n` from 0.1 to 0.2 or later,
|
|
1027
|
+
`.maizzle/` holds both `i18n/` and `emails/`.
|
|
1028
|
+
**Why:** 0.1 wrote the wrappers under `.maizzle/i18n/`; 0.2 writes them under
|
|
1029
|
+
`.maizzle/emails/`, so that `maizzle serve` lists the e-mails under a folder
|
|
1030
|
+
that says what they are. Nothing reads the old folder any more — not the
|
|
1031
|
+
build, not `maizzle serve`, not the type-check — and the build output does
|
|
1032
|
+
not move.
|
|
1033
|
+
**Fix:** delete it, once:
|
|
1034
|
+
|
|
1035
|
+
```sh
|
|
1036
|
+
rm -rf .maizzle/i18n
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1002
1039
|
### `No templates found`, or old templates, when Maizzle is built from a worker thread
|
|
1003
1040
|
|
|
1004
1041
|
**When:** Maizzle's `build()` is called from code that runs in a worker
|
|
1005
1042
|
thread, such as a job runner or a Vitest `threads` pool. The build prints
|
|
1006
1043
|
`No templates found`, or builds templates that were since renamed or
|
|
1007
1044
|
removed, without the ones added.
|
|
1008
|
-
**Why:** the plugin writes the wrappers under `.maizzle/
|
|
1045
|
+
**Why:** the plugin writes the wrappers under `.maizzle/emails/` only on the
|
|
1009
1046
|
main thread, so that the workers of a parallel build never write the same
|
|
1010
1047
|
file twice. From a worker thread it writes none, and the build finds whatever
|
|
1011
1048
|
wrappers are already there, or none at all.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/mail-i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An i18n plugin for a Maizzle project: ICU catalogues checked at build time, t() in templates, one output per locale, and the manifest the renderer reads. createTranslator for use outside templates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|