@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/LICENSE +21 -0
- package/README.md +470 -0
- package/dist/catalogues.d.ts +40 -0
- package/dist/catalogues.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +17 -0
- package/dist/manifest.d.ts +45 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/plugin.d.ts +55 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/sources.d.ts +19 -0
- package/dist/sources.d.ts.map +1 -0
- package/dist/template.d.ts +22 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/translator.d.ts +31 -0
- package/dist/translator.d.ts.map +1 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vue.d.ts +44 -0
- package/dist/vue.d.ts.map +1 -0
- package/dist/wrappers.d.ts +62 -0
- package/dist/wrappers.d.ts.map +1 -0
- package/docs/README.md +17 -0
- package/docs/guide/catalogues.md +354 -0
- package/docs/guide/editor.md +215 -0
- package/docs/guide/manifest.md +343 -0
- package/docs/guide/templates.md +421 -0
- package/docs/guide/translator.md +170 -0
- package/docs/roadmap.md +91 -0
- package/docs/troubleshooting.md +1096 -0
- package/package.json +71 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# The manifest
|
|
2
|
+
|
|
3
|
+
This page is for reading `dist/mail-manifest.json`, the file `maizzle build`
|
|
4
|
+
writes for the code that sends: which e-mails were built, in which locales,
|
|
5
|
+
with which placeholders, under which subject — and `generated/mail.ts`, the
|
|
6
|
+
same e-mails as a type for the renderer.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { MANIFEST_FILE, type Manifest } from '@nxgt/mail-i18n';
|
|
11
|
+
|
|
12
|
+
const manifest: Manifest = JSON.parse(readFileSync(`dist/${MANIFEST_FILE}`, 'utf8'));
|
|
13
|
+
|
|
14
|
+
manifest.emails['verify-email']?.variables; // ['link', 'name']
|
|
15
|
+
manifest.emails['verify-email']?.subject.fr; // 'Confirmez votre adresse e-mail, {{ name }}'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`createMailRenderer` from `@nxgt/mail/renderer` reads this file, and every file it
|
|
19
|
+
lists, so that sending takes one call:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createMailRenderer } from '@nxgt/mail/renderer';
|
|
23
|
+
|
|
24
|
+
const mails = createMailRenderer({ dir: 'dist' }); // reads dist/mail-manifest.json
|
|
25
|
+
mails.render('verify-email', { name: 'Ada', link: 'https://app.example.com/verify?token=abc' });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It requires every variable in `variables`, refuses a value of a URL variable
|
|
29
|
+
that is not an `http:`, `https:` or `mailto:` URL, and refuses a build whose
|
|
30
|
+
`text` is `null`. See [Rendering](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/rendering.md) in `@nxgt/mail`. For checks of your
|
|
31
|
+
own, the file is plain JSON, typed by `Manifest`.
|
|
32
|
+
|
|
33
|
+
## An example
|
|
34
|
+
|
|
35
|
+
The project of [Templates](templates.md): `emails/verify-email.vue` and
|
|
36
|
+
`emails/auth/reset-password.vue`, built in `en` and `fr`, with the default
|
|
37
|
+
`nested` layout. This is the golden file the package's own build is tested
|
|
38
|
+
against:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"locales": ["en", "fr"],
|
|
43
|
+
"fallbackLocale": "en",
|
|
44
|
+
"emails": {
|
|
45
|
+
"auth/reset-password": {
|
|
46
|
+
"variables": ["email", "resetLink"],
|
|
47
|
+
"urlVariables": ["resetLink"],
|
|
48
|
+
"subject": {
|
|
49
|
+
"en": "Reset your password",
|
|
50
|
+
"fr": "Réinitialisez votre mot de passe"
|
|
51
|
+
},
|
|
52
|
+
"files": {
|
|
53
|
+
"en": {
|
|
54
|
+
"html": "en/auth/reset-password.html",
|
|
55
|
+
"text": "en/auth/reset-password.txt"
|
|
56
|
+
},
|
|
57
|
+
"fr": {
|
|
58
|
+
"html": "fr/auth/reset-password.html",
|
|
59
|
+
"text": "fr/auth/reset-password.txt"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"verify-email": {
|
|
64
|
+
"variables": ["link", "name"],
|
|
65
|
+
"urlVariables": ["link"],
|
|
66
|
+
"subject": {
|
|
67
|
+
"en": "Confirm your e-mail address, {{ name }}",
|
|
68
|
+
"fr": "Confirmez votre adresse e-mail, {{ name }}"
|
|
69
|
+
},
|
|
70
|
+
"files": {
|
|
71
|
+
"en": {
|
|
72
|
+
"html": "en/verify-email.html",
|
|
73
|
+
"text": "en/verify-email.txt"
|
|
74
|
+
},
|
|
75
|
+
"fr": {
|
|
76
|
+
"html": "fr/verify-email.html",
|
|
77
|
+
"text": "fr/verify-email.txt"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## The shape
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
interface Manifest {
|
|
89
|
+
readonly locales: readonly string[];
|
|
90
|
+
readonly fallbackLocale: string;
|
|
91
|
+
readonly emails: Readonly<Record<string, ManifestEmail>>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ManifestEmail {
|
|
95
|
+
readonly variables: readonly string[];
|
|
96
|
+
readonly urlVariables: readonly string[];
|
|
97
|
+
readonly subject: Readonly<Record<string, string>>;
|
|
98
|
+
readonly files: Readonly<Record<string, { readonly html: string; readonly text: string | null }>>;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Field | Holds |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `locales` | The plugin's `locales`, in the order given |
|
|
105
|
+
| `fallbackLocale` | The plugin's `fallbackLocale`, or the first locale |
|
|
106
|
+
| `emails` | One entry per e-mail, keyed by its template's path under `emails/` without `.vue` (`auth/reset-password`), sorted |
|
|
107
|
+
|
|
108
|
+
Each e-mail:
|
|
109
|
+
|
|
110
|
+
| Field | Holds |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `variables` | Every placeholder of the e-mail, in the HTML or the text part of any locale, or in any subject, sorted, each once. The values the sending side must pass |
|
|
113
|
+
| `urlVariables` | The placeholders a URL attribute (`href`, `src`, `background`, `poster`, `action`) starts with, sorted. The values that must be URLs |
|
|
114
|
+
| `subject` | The subject per locale, formatted from `<emailKey>.subject`, each argument kept as `{{ name }}` |
|
|
115
|
+
| `files` | The built files per locale, relative to the output folder: `html`, and `text` or `null` when there is no plain-text part |
|
|
116
|
+
|
|
117
|
+
### `variables`
|
|
118
|
+
|
|
119
|
+
Collected from the built files, not from the template's source: every
|
|
120
|
+
`{{ name }}` the build left in the HTML of each locale, in its text part, and
|
|
121
|
+
in each subject. A placeholder only in the text part, inside a Maizzle
|
|
122
|
+
`<Plaintext>` block, is listed. So is a placeholder that only one locale
|
|
123
|
+
writes. A value the
|
|
124
|
+
template formatted at build time (`t('verifyEmail.expires', { minutes: 15 })`)
|
|
125
|
+
is not a variable.
|
|
126
|
+
|
|
127
|
+
### `urlVariables`
|
|
128
|
+
|
|
129
|
+
A placeholder that **starts** the value of a URL attribute: `href`, `src`,
|
|
130
|
+
`background`, `poster` or `action`, in either kind of quotes, leading spaces
|
|
131
|
+
ignored. `:href="placeholder('link')"` writes one. Such a placeholder decides
|
|
132
|
+
the scheme of the URL, so the sending side must fill it with a URL and refuse
|
|
133
|
+
anything else: a `javascript:` value in a link is an injection.
|
|
134
|
+
|
|
135
|
+
A placeholder later in the value is a variable, but not a URL variable. The
|
|
136
|
+
URL's scheme is already written, and the value only fills a part of it:
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<!-- in the built file -->
|
|
140
|
+
<a href="{{ link }}">…</a> <!-- link: a URL variable -->
|
|
141
|
+
<img src='{{ logo }}'> <!-- logo: a URL variable -->
|
|
142
|
+
<a href="https://app.example.com/verify?token={{ token }}">…</a> <!-- token: a variable only -->
|
|
143
|
+
<p title="{{ label }}">…</p> <!-- label: a variable only -->
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A placeholder in the text, or in another attribute (`title`, `alt`), is
|
|
147
|
+
listed in `variables` only.
|
|
148
|
+
|
|
149
|
+
### `subject`
|
|
150
|
+
|
|
151
|
+
The message `<emailKey>.subject` of each locale, formatted, each argument
|
|
152
|
+
turned into a placeholder. `verifyEmail.subject` is
|
|
153
|
+
`"Confirm your e-mail address, {name}"`, so the manifest holds
|
|
154
|
+
`"Confirm your e-mail address, {{ name }}"`, and `name` is in `variables`.
|
|
155
|
+
See [the subject](catalogues.md#the-subject).
|
|
156
|
+
|
|
157
|
+
### `files`
|
|
158
|
+
|
|
159
|
+
Paths relative to the output folder, with `/` as separator on every system.
|
|
160
|
+
They follow `layout` and Maizzle's `output.extension`:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"files": {
|
|
165
|
+
"fr": { "html": "auth/reset-password.fr.html", "text": "auth/reset-password.fr.txt" }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
That is the `flat` layout. `text` is `null` when the project turned
|
|
171
|
+
Maizzle's `plaintext` off. Every e-mail has an entry for each locale of
|
|
172
|
+
`locales`, in that order: an e-mail missing in one locale fails the build.
|
|
173
|
+
|
|
174
|
+
## Where it is written
|
|
175
|
+
|
|
176
|
+
`<output.path>/mail-manifest.json`: `dist/mail-manifest.json` by default, next
|
|
177
|
+
to the built files. `MANIFEST_FILE` is the name, and `output.path` is
|
|
178
|
+
Maizzle's.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { MANIFEST_FILE } from '@nxgt/mail-i18n'; // 'mail-manifest.json'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
It is written at the end of `maizzle build`, after every template. It is
|
|
185
|
+
rewritten whole on each build, with tabs and a final newline, so a diff of
|
|
186
|
+
two builds is readable. `maizzle serve` does not write it.
|
|
187
|
+
|
|
188
|
+
A parallel build writes the same manifest: when Maizzle builds in workers,
|
|
189
|
+
each worker loads the config again, and only the main thread writes the
|
|
190
|
+
generated files and the manifest. The package's own specs build the fixture
|
|
191
|
+
in parallel and compare its manifest with the golden file above.
|
|
192
|
+
|
|
193
|
+
These fail here, since they need the built files. Like every build failure,
|
|
194
|
+
each is a plain `Error`, a mistake to fix in the templates or the
|
|
195
|
+
catalogues, not a condition to catch:
|
|
196
|
+
|
|
197
|
+
| Build failure | Cause |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `i18n: welcome has no subject — add welcome.subject to the catalogues` | An e-mail without a subject message |
|
|
200
|
+
| `i18n: en: welcome.subject uses {count} as a number — a subject's arguments are placeholders, filled at send time as strings` | A subject with a `number`, `plural` or `date` argument |
|
|
201
|
+
| `i18n: en: welcome.subject chooses on {kind} with a select — a subject's arguments are placeholders, which always choose other` | A subject with a `select` argument |
|
|
202
|
+
| `i18n: welcome was not built in fr` | No HTML file (with `output.extension`) was written for that e-mail in that locale |
|
|
203
|
+
| `i18n: fr/welcome.html is empty — a tag of its template resolved to no component; list the plugin that brings it, as ui()` | The file holds nothing but its doctype: a tag of the template matched no component, which Vue renders as nothing. See [Templates from a package](templates.md#templates-from-a-package) |
|
|
204
|
+
| `i18n: ../text/welcome.en.txt 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` | A `plaintext.destination`, or an output path set in a template, put a file outside `output.path` |
|
|
205
|
+
| `i18n: custom/welcome.html is not where the i18n plugin puts an e-mail — set no output path in a template` | A file in the output folder that is not `<locale>/<email>` (or `<email>.<locale>` with the flat layout), usually from an output path set in a template |
|
|
206
|
+
|
|
207
|
+
## The renderer's types — `generated/mail.ts`
|
|
208
|
+
|
|
209
|
+
After writing the manifest, the build writes the same e-mails and variables
|
|
210
|
+
as a TypeScript module, `MailEmails`, for the code that sends:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { createMailRenderer } from '@nxgt/mail/renderer';
|
|
214
|
+
import type { MailEmails } from './generated/mail';
|
|
215
|
+
|
|
216
|
+
const mails = createMailRenderer<MailEmails>({ dir: 'dist' });
|
|
217
|
+
|
|
218
|
+
mails.render('verify-email', { name: 'Ada', link: 'https://app.example.com/verify?token=abc' });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
For the manifest above, the file holds exactly this:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// Generated by @nxgt/mail-i18n from the build, after each maizzle build.
|
|
225
|
+
// Never edited; committed, so the code that sends type-checks without a build.
|
|
226
|
+
|
|
227
|
+
/** The e-mails of the build, each with the variables it takes when it is sent. */
|
|
228
|
+
export interface MailEmails {
|
|
229
|
+
"auth/reset-password": { readonly email: string | number; readonly resetLink: string };
|
|
230
|
+
"verify-email": { readonly link: string; readonly name: string | number };
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| From the manifest | In `MailEmails` |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| each key of `emails` | a property, quoted, sorted |
|
|
237
|
+
| each name of `variables` | a `readonly` property of that e-mail, in the manifest's order |
|
|
238
|
+
| a name also in `urlVariables` | `string`: a URL decides a link's scheme, and `render` checks it as a string |
|
|
239
|
+
| any other variable | `string \| number`, as `render` writes it |
|
|
240
|
+
| an e-mail with no variable | `Readonly<Record<string, never>>`: `render('welcome')`, with no variables argument |
|
|
241
|
+
|
|
242
|
+
With it, `render` refuses at compile time an e-mail the build does not have,
|
|
243
|
+
a variable the e-mail does not take, a missing one, and a number for a URL —
|
|
244
|
+
for a name written as a literal and variables written at the call.
|
|
245
|
+
What each refusal looks like, and the untyped default, are in `@nxgt/mail`'s
|
|
246
|
+
[Rendering — typing the renderer](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/rendering.md#typing-the-renderer).
|
|
247
|
+
|
|
248
|
+
### When it is written
|
|
249
|
+
|
|
250
|
+
- At the end of `maizzle build`, right after `mail-manifest.json`, once per
|
|
251
|
+
build, a parallel one included. `maizzle serve` and `maizzle prepare` do
|
|
252
|
+
not write it.
|
|
253
|
+
- **Only over a file it wrote**: one that does not start with its header line,
|
|
254
|
+
`// Generated by @nxgt/mail-i18n from the build`, fails the build and is
|
|
255
|
+
kept — see
|
|
256
|
+
[`i18n: src/index.ts was not written by i18n()`](../troubleshooting.md#i18n-srcindexts-was-not-written-by-i18n--point-renderertypes-at-a-file-of-its-own).
|
|
257
|
+
- **Only when its content changed**: a build that changes no e-mail name and
|
|
258
|
+
no variable leaves the file untouched, so an editor or a watcher sees no
|
|
259
|
+
change, and `git status` shows it only when the sending code has something
|
|
260
|
+
to check.
|
|
261
|
+
- Its folder is created when missing.
|
|
262
|
+
|
|
263
|
+
### Commit it
|
|
264
|
+
|
|
265
|
+
The file is the contract between the build and the code that sends, so it is
|
|
266
|
+
committed, not ignored: a fresh clone type-checks, and CI checks the sending
|
|
267
|
+
code, without running `maizzle build` first. When a template changes, the
|
|
268
|
+
next build rewrites it, and the diff shows which e-mail gained or lost a
|
|
269
|
+
variable.
|
|
270
|
+
|
|
271
|
+
The `generated/` folder holds generated code, never edited by hand: leave it
|
|
272
|
+
out of your formatter and linter, as Biome's `"includes": ["**",
|
|
273
|
+
"!**/generated"]` does.
|
|
274
|
+
|
|
275
|
+
### `rendererTypes` — where, or none
|
|
276
|
+
|
|
277
|
+
| Value | Effect |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| left out | `generated/mail.ts`, resolved against the folder `maizzle` runs in, like `dir` and `emails` |
|
|
280
|
+
| `'src/generated/mail.ts'` | That path instead, from the same folder. It must end in `.ts` |
|
|
281
|
+
| `'../api/src/generated/mail.ts'` | Outside the project: into the package that sends, in a monorepo where it is not the Maizzle project |
|
|
282
|
+
| `false` | No file is written |
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
// maizzle.config.ts — next to the code that imports it
|
|
286
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
287
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
288
|
+
|
|
289
|
+
export default defineMailConfig({
|
|
290
|
+
plugins: [i18n({ locales: ['en', 'fr'], rendererTypes: 'src/generated/mail.ts' })],
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Anything else — `true`, an empty string, a path that is not a `.ts` file —
|
|
295
|
+
is refused when the config loads with a `TypeError`:
|
|
296
|
+
[`i18n: rendererTypes must be the path of a .ts file, as generated/mail.ts, or false`](../troubleshooting.md#i18n-renderertypes-must-be-the-path-of-a-ts-file-as-generatedmailts-or-false).
|
|
297
|
+
|
|
298
|
+
Use `false` when nothing in TypeScript sends these e-mails, or when two
|
|
299
|
+
configs build the same project and one of them already writes the file.
|
|
300
|
+
|
|
301
|
+
## Checking your application against it
|
|
302
|
+
|
|
303
|
+
The manifest says what each e-mail needs. The typed renderer above holds the
|
|
304
|
+
two sides together at compile time. Without it — the built project in a
|
|
305
|
+
package the application only installs, or an application not written in
|
|
306
|
+
TypeScript — a test in the application that sends can do the same, so
|
|
307
|
+
renaming a placeholder in a template fails a test rather than an e-mail:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
// test/mail-manifest.spec.ts, in the application that sends
|
|
311
|
+
import { describe, expect, test } from 'bun:test';
|
|
312
|
+
import { readFileSync } from 'node:fs';
|
|
313
|
+
import type { Manifest } from '@nxgt/mail-i18n';
|
|
314
|
+
|
|
315
|
+
const manifest: Manifest = JSON.parse(
|
|
316
|
+
readFileSync('node_modules/acme-mails/dist/mail-manifest.json', 'utf8'),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
/** What the application passes for each e-mail it sends. */
|
|
320
|
+
const sent: Record<string, readonly string[]> = {
|
|
321
|
+
'verify-email': ['name', 'link'],
|
|
322
|
+
'auth/reset-password': ['email', 'resetLink'],
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
describe('the built e-mails', () => {
|
|
326
|
+
test.each(Object.entries(sent))('%s takes what the application passes', (email, variables) => {
|
|
327
|
+
expect(manifest.emails[email]?.variables).toEqual([...variables].sort());
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('are built in every locale the application offers', () => {
|
|
331
|
+
expect(manifest.locales).toEqual(expect.arrayContaining(['en', 'fr']));
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`acme-mails` stands for wherever your built project lives: a package of its
|
|
337
|
+
own, a folder of the repository, a build artefact.
|
|
338
|
+
|
|
339
|
+
## See also
|
|
340
|
+
|
|
341
|
+
- [Rendering](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/rendering.md) in `@nxgt/mail` — the renderer that reads the manifest at send time.
|
|
342
|
+
- [Templates](templates.md) — where placeholders come from.
|
|
343
|
+
- [Catalogues](catalogues.md#the-subject) — the subject of each e-mail.
|