@nxgt/janus-mail 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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/generated/locales.d.ts +5 -0
  4. package/dist/generated/locales.d.ts.map +1 -0
  5. package/dist/generated/mail.d.ts +29 -0
  6. package/dist/generated/mail.d.ts.map +1 -0
  7. package/dist/index.d.ts +20 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +234 -0
  10. package/dist/index.js.map +14 -0
  11. package/dist/janus-mail.d.ts +24 -0
  12. package/dist/janus-mail.d.ts.map +1 -0
  13. package/dist/mails.d.ts +26 -0
  14. package/dist/mails.d.ts.map +1 -0
  15. package/dist/options.d.ts +27 -0
  16. package/dist/options.d.ts.map +1 -0
  17. package/dist/templates.d.ts +15 -0
  18. package/dist/templates.d.ts.map +1 -0
  19. package/dist/types.d.ts +145 -0
  20. package/dist/types.d.ts.map +1 -0
  21. package/docs/README.md +34 -0
  22. package/docs/guide/building.md +130 -0
  23. package/docs/guide/locales.md +113 -0
  24. package/docs/guide/sending.md +242 -0
  25. package/docs/guide/templates.md +183 -0
  26. package/docs/roadmap.md +52 -0
  27. package/docs/troubleshooting.md +313 -0
  28. package/mails/en/email-changed.html +42 -0
  29. package/mails/en/email-changed.txt +21 -0
  30. package/mails/en/password-changed.html +42 -0
  31. package/mails/en/password-changed.txt +21 -0
  32. package/mails/en/reset-password.html +40 -0
  33. package/mails/en/reset-password.txt +22 -0
  34. package/mails/en/sign-in-code.html +36 -0
  35. package/mails/en/sign-in-code.txt +15 -0
  36. package/mails/en/verify-email.html +40 -0
  37. package/mails/en/verify-email.txt +22 -0
  38. package/mails/fr/email-changed.html +42 -0
  39. package/mails/fr/email-changed.txt +21 -0
  40. package/mails/fr/password-changed.html +41 -0
  41. package/mails/fr/password-changed.txt +21 -0
  42. package/mails/fr/reset-password.html +39 -0
  43. package/mails/fr/reset-password.txt +22 -0
  44. package/mails/fr/sign-in-code.html +36 -0
  45. package/mails/fr/sign-in-code.txt +15 -0
  46. package/mails/fr/verify-email.html +40 -0
  47. package/mails/fr/verify-email.txt +22 -0
  48. package/mails/mail-manifest.json +128 -0
  49. package/package.json +71 -0
@@ -0,0 +1,24 @@
1
+ import type { JanusMail, JanusMailLocale, JanusMailOptions } from './types';
2
+ /**
3
+ * The e-mails of Janus's flows, over your mailer:
4
+ *
5
+ * ```ts
6
+ * const mail = janusMail({
7
+ * mailer,
8
+ * from: 'noreply@acme.example',
9
+ * brand: 'Acme',
10
+ * links: {
11
+ * verifyEmail: (token) => `https://acme.example/verify?token=${token}`,
12
+ * resetPassword: (token) => `https://acme.example/reset?token=${token}`,
13
+ * secureAccount: () => 'https://acme.example/account/security',
14
+ * },
15
+ * });
16
+ *
17
+ * await mail.verifyEmail(await auth.verifyEmail.send(user), { name: user.name, locale: user.locale });
18
+ * ```
19
+ *
20
+ * Reads nothing when called: the default e-mails are read on the first one
21
+ * sent. A wrong option is a bare `TypeError`, thrown here.
22
+ */
23
+ export declare function janusMail<const L extends string = JanusMailLocale>(options: JanusMailOptions<L>): JanusMail<L>;
24
+ //# sourceMappingURL=janus-mail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"janus-mail.d.ts","sourceRoot":"","sources":["../src/janus-mail.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,SAAS,EACT,eAAe,EACf,gBAAgB,EAGhB,MAAM,SAAS,CAAC;AAqJjB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,eAAe,EACjE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC1B,SAAS,CAAC,CAAC,CAAC,CASd"}
@@ -0,0 +1,26 @@
1
+ import { type MailRenderer } from '@nxgt/mail/renderer';
2
+ import type { MailEmails } from './generated/mail';
3
+ /**
4
+ * The folder of the built e-mails, resolved from this module rather than from
5
+ * the working directory, which is the consumer's.
6
+ *
7
+ * **This file must stay directly under `src/`.** `../mails/` is right from
8
+ * `src/mails.ts` in the specs, and from `dist/index.js` in the tarball,
9
+ * because the build bundles this module into the single entry
10
+ * `dist/index.js`, one level under the package as `src/` is. Moved into a
11
+ * subfolder, or bundled into `dist/chunks/` by a second entry point, it would
12
+ * resolve a folder that does not exist — which the artifact spec, rendering
13
+ * from `../dist`, would catch.
14
+ */
15
+ export declare const MAILS_DIR: string;
16
+ /**
17
+ * The renderer of the default e-mails, created on first use and shared by
18
+ * every `janusMail()` and `janusTemplates()` after it.
19
+ *
20
+ * Lazy, so importing the package reads nothing: an application that
21
+ * overrides every template never opens `mails/`. The first default template
22
+ * rendered reads the whole build once — a missing `mails/` throws there, with
23
+ * `createMailRenderer`'s own message.
24
+ */
25
+ export declare function defaultRenderer(): MailRenderer<MailEmails>;
26
+ //# sourceMappingURL=mails.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mails.d.ts","sourceRoot":"","sources":["../src/mails.ts"],"names":[],"mappings":"AAQA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,QAAuD,CAAC;AAI9E;;;;;;;;GAQG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAAC,UAAU,CAAC,CAG1D"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `janusMail()`'s options, checked once when it is called. A mistake is a
3
+ * wiring mistake, so a bare `TypeError` naming the option — never its value —
4
+ * thrown before anything is sent.
5
+ */
6
+ import type { Address, Mailer } from '@nxgt/mail';
7
+ import type { JanusMailLinks, JanusMailTemplateName, JanusMailTemplates } from './types';
8
+ /** The templates in the order the documentation lists them. */
9
+ export declare const TEMPLATE_NAMES: readonly JanusMailTemplateName[];
10
+ /** The options once checked, with `string` for the locales: the degenericised mirror. */
11
+ export interface ResolvedOptions {
12
+ readonly mailer: Mailer;
13
+ readonly from: Address;
14
+ readonly replyTo: Address | undefined;
15
+ readonly brand: string;
16
+ readonly links: JanusMailLinks;
17
+ readonly locales: readonly string[];
18
+ readonly fallbackLocale: string;
19
+ readonly templates: JanusMailTemplates<string>;
20
+ }
21
+ /**
22
+ * Checks `janusMail()`'s options and answers them resolved, or throws a
23
+ * `TypeError`. What was checked is copied and frozen, so the guarantee made
24
+ * here holds at every send.
25
+ */
26
+ export declare function resolveOptions(options: unknown): ResolvedOptions;
27
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGlD,OAAO,KAAK,EACX,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,MAAM,SAAS,CAAC;AAEjB,+DAA+D;AAC/D,eAAO,MAAM,cAAc,EAAE,SAAS,qBAAqB,EAM1D,CAAC;AAIF,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;CAC/C;AAkJD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAmBhE"}
@@ -0,0 +1,15 @@
1
+ import type { JanusMailTemplates } from './types';
2
+ /**
3
+ * The five default templates, in the locales they are built in. The
4
+ * renderer behind them is created on the first render, not here.
5
+ *
6
+ * ```ts
7
+ * const defaults = janusTemplates();
8
+ * const { subject } = await defaults.verifyEmail({ brand: 'Acme', name: 'Ada', link, locale: 'fr' });
9
+ * ```
10
+ *
11
+ * A URL that is not `http:`, `https:` or `mailto:` throws `MailRefused`; a
12
+ * missing `mails/` folder throws when the first one renders.
13
+ */
14
+ export declare function janusTemplates(): JanusMailTemplates;
15
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,IAAI,kBAAkB,CA6BnD"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The public types of `@nxgt/janus-mail`, in their generic form. The
3
+ * implementation works on a degenericised mirror (`string` locales) and casts
4
+ * once, in `janusMail()`.
5
+ */
6
+ import type { IssuedCode, IssuedToken } from '@nxgt/janus';
7
+ import type { Address, Mailer, Rendered, SentMail, WantedLocales } from '@nxgt/mail';
8
+ import type { JanusMailLocale } from './generated/locales';
9
+ export type { JanusMailLocale } from './generated/locales';
10
+ /**
11
+ * What each e-mail's template is given, by template name. Every value is
12
+ * text: `brand` is the name `janusMail({ brand })` was given, `name` the
13
+ * recipient's, `link` an absolute URL from `links`.
14
+ *
15
+ * Held equal to the build's variables by `test/types/variables.ts`: an e-mail
16
+ * of the build that gains or loses a variable fails the typecheck there.
17
+ */
18
+ export interface JanusMailVariables {
19
+ readonly verifyEmail: {
20
+ readonly brand: string;
21
+ readonly name: string;
22
+ readonly link: string;
23
+ };
24
+ readonly resetPassword: {
25
+ readonly brand: string;
26
+ readonly name: string;
27
+ readonly link: string;
28
+ };
29
+ /** No `challenge`, ever: it is the visitor's secret, and never goes in an e-mail. */
30
+ readonly signInCode: {
31
+ readonly brand: string;
32
+ readonly code: string;
33
+ };
34
+ readonly passwordChanged: {
35
+ readonly brand: string;
36
+ readonly name: string;
37
+ readonly link: string;
38
+ };
39
+ readonly emailChanged: {
40
+ readonly brand: string;
41
+ readonly name: string;
42
+ readonly link: string;
43
+ readonly newEmail: string;
44
+ };
45
+ }
46
+ /** The name of one of the five templates: `verifyEmail`, `resetPassword`, … */
47
+ export type JanusMailTemplateName = keyof JanusMailVariables;
48
+ /**
49
+ * One e-mail: its variables and the locale picked for the recipient, in; its
50
+ * `subject`, `html` and `text`, out — escaping is then the function's job. A
51
+ * default, from the prebuilt e-mails, or yours: React Email, a string, or
52
+ * another Maizzle build.
53
+ */
54
+ export type JanusMailTemplate<V, L extends string = JanusMailLocale> = (variables: V & {
55
+ readonly locale: L;
56
+ }) => Rendered | PromiseLike<Rendered>;
57
+ /** The five templates, each for the locales `L`. */
58
+ export type JanusMailTemplates<L extends string = JanusMailLocale> = {
59
+ readonly [K in JanusMailTemplateName]: JanusMailTemplate<JanusMailVariables[K], L>;
60
+ };
61
+ /**
62
+ * Who an e-mail is for, besides the address: the name it greets, and the
63
+ * locales they want, most wanted first — their stored locale, then their
64
+ * `Accept-Language` when they are the visitor. Picked with `pickLocale`:
65
+ * `fr-CA` sends `fr`, and nothing matching sends the fallback locale.
66
+ */
67
+ export interface Recipient {
68
+ readonly name: string;
69
+ readonly locale?: WantedLocales;
70
+ }
71
+ /**
72
+ * The links of the e-mails, each an absolute `http(s)` URL of your
73
+ * application. Each is called at send time and must answer a string
74
+ * synchronously: an `async` function or a `URL` object is a compile error,
75
+ * and from JavaScript, or through a cast, the send throws a `TypeError`
76
+ * naming the call. A
77
+ * `mailto:` URL is accepted too — `secureAccount: () => 'mailto:security@acme.example'`
78
+ * sends the user to your support desk; any other scheme is refused by the
79
+ * renderer with a `MailRefused`. The functions are copied when `janusMail()`
80
+ * is called: replacing one afterwards changes nothing.
81
+ */
82
+ export interface JanusMailLinks {
83
+ /** The page that confirms an e-mail, given the one-time token: `(token) => \`https://app.example/verify?token=${token}\`` */
84
+ readonly verifyEmail: (token: string) => string;
85
+ /** The page that sets a new password, given the one-time token. */
86
+ readonly resetPassword: (token: string) => string;
87
+ /** Where a user who did not make a change secures their account — `passwordChanged` and `emailChanged` link to it. */
88
+ readonly secureAccount: () => string;
89
+ }
90
+ /** The options every locale set shares. */
91
+ interface JanusMailBaseOptions<L extends string> {
92
+ /** The transport — `@nxgt/mail-smtp`, `@nxgt/mail-resend`, or `createMemoryMailer()` in tests. */
93
+ readonly mailer: Mailer;
94
+ /** The sender of every e-mail. */
95
+ readonly from: Address;
96
+ /** Where a reply goes, when not to `from`. */
97
+ readonly replyTo?: Address;
98
+ /** The name the e-mails show — in the header, the body and the footer. Text only. */
99
+ readonly brand: string;
100
+ readonly links: JanusMailLinks;
101
+ /** The locales to send in. Default the built ones, `['en', 'fr']`. */
102
+ readonly locales?: readonly L[];
103
+ /** The locale when the recipient wants none of `locales`. Default `en`, else the first of `locales`. */
104
+ readonly fallbackLocale?: NoInfer<L>;
105
+ }
106
+ /**
107
+ * `janusMail()`'s options. `templates` replaces any of the defaults — and
108
+ * **every** one once `locales` holds a locale the defaults are not built in:
109
+ * a default could not render it.
110
+ */
111
+ export type JanusMailOptions<L extends string = JanusMailLocale> = JanusMailBaseOptions<L> & ([L] extends [JanusMailLocale] ? {
112
+ readonly templates?: Partial<JanusMailTemplates<L>>;
113
+ } : {
114
+ readonly templates: JanusMailTemplates<L>;
115
+ });
116
+ /**
117
+ * The e-mails of Janus's flows, each sent to the address the flow answered.
118
+ * Each method resolves once the mailer took the e-mail, with its answer, and
119
+ * rejects with the mailer's `MailFailure` or `MailRefused`, untouched.
120
+ */
121
+ export interface JanusMail<L extends string = JanusMailLocale> {
122
+ /** What `auth.verifyEmail.send(user)` answered, to `issued.email`. */
123
+ verifyEmail(issued: IssuedToken, to: Recipient): Promise<SentMail>;
124
+ /** What `auth.resetPassword.request(email)` answered, once checked for `null`, to `issued.email`. */
125
+ resetPassword(issued: IssuedToken, to: Recipient): Promise<SentMail>;
126
+ /**
127
+ * The code `auth.signInCode.request(email)` answered, to `issued.email`.
128
+ * Reads `code` and `email` only: the challenge never reaches the e-mail.
129
+ */
130
+ signInCode(issued: Pick<IssuedCode<unknown>, 'code' | 'email'>, to?: Pick<Recipient, 'locale'>): Promise<SentMail>;
131
+ /** Tells `to.email` their password was changed, with a link to secure the account. */
132
+ passwordChanged(to: Recipient & {
133
+ readonly email: string;
134
+ }): Promise<SentMail>;
135
+ /** Tells the **former** address that the e-mail changed to `newEmail`, with a link to secure the account. */
136
+ emailChanged(to: Recipient & {
137
+ readonly formerEmail: string;
138
+ readonly newEmail: string;
139
+ }): Promise<SentMail>;
140
+ /** The templates in use: the defaults, with `templates` over them. */
141
+ readonly templates: JanusMailTemplates<L>;
142
+ /** The locales sent in. */
143
+ readonly locales: readonly L[];
144
+ }
145
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/docs/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @nxgt/janus-mail — documentation
2
+
3
+ The [README](../README.md) shows each e-mail in one example; these pages give
4
+ the detail.
5
+
6
+ | Page | Read it when |
7
+ | --- | --- |
8
+ | [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, each method, where each e-mail goes, what a failure looks like, and a test |
9
+ | [Templates](guide/templates.md) | You want an e-mail of your own: what a template takes and answers, a partial override, `janusTemplates()`, and what the defaults say |
10
+ | [Locales](guide/locales.md) | You send in more than one language: how the locale is picked, `fallbackLocale`, fewer locales, and adding one |
11
+ | [Building](guide/building.md) | You work on this package: the Maizzle project in `mail/`, `mails/`, the generated files, and why none of it reaches a consumer's build |
12
+ | [Troubleshooting](troubleshooting.md) | A `TypeError` at wiring or at a call, a `MailRefused`, a `MailFailure`, or a compile error |
13
+ | [Roadmap](roadmap.md) | You want to know what is coming, and what is deliberately not planned |
14
+
15
+ ## Words
16
+
17
+ The words **user**, **one-time token**, **one-time code**, **challenge** and
18
+ **e-mail flow** are
19
+ [`@nxgt/janus`'s vocabulary](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/vocabulary.md);
20
+ **mailer**, **transport**, **rendered**, **placeholder** and **manifest** are
21
+ [`@nxgt/mail`'s](https://github.com/softistx/nxgt-mail/blob/develop/docs/vocabulary.md).
22
+ They mean the same here. These pages add:
23
+
24
+ | Word | Means | Not |
25
+ | --- | --- | --- |
26
+ | **e-mail** | One of the five this package sends: `verify-email`, `reset-password`, `sign-in-code`, `password-changed`, `email-changed` — by the name of its built file | "mail", "message" — a *message* is what a mailer sends, addressed |
27
+ | **template** | The function that renders one e-mail: `(variables & { locale }) => Rendered`. Named in camelCase, after the method that sends it: `verifyEmail`, `signInCode` | "renderer" — the renderer is `@nxgt/mail`'s, and reads the build |
28
+ | **default template** | A template of `janusTemplates()`: the prebuilt e-mail, filled by the renderer | "preset" — a preset is `@nxgt/mail-presets`' source, which the build compiles |
29
+ | **override** | A template of yours, passed in `templates`, used instead of the default | "custom template" |
30
+ | **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send` or `resetPassword.request`, an `IssuedCode` from `signInCode.request` | "result", "payload" |
31
+ | **recipient** | Who an e-mail is for besides the address: a `Recipient`, `{ name, locale? }` | "user" — a user is a record of `@nxgt/janus`; "to" is the address |
32
+ | **notice** | An e-mail that tells, and asks for nothing: `passwordChanged`, `emailChanged`. It links to `links.secureAccount()` | "alert", "notification" |
33
+ | **brand** | The name the e-mails show, `janusMail({ brand })`: text, filled at send time | "logo", "company" |
34
+ | **the build** | The e-mails as `scripts/build-mail.ts` writes them to `mails/`: HTML and text per locale, and the manifest | "dist" — `mails/` is not `dist/` |
@@ -0,0 +1,130 @@
1
+ # Building
2
+
3
+ This page is for working on the package itself: how the default e-mails are
4
+ built, what is committed and what is shipped, and why a consumer needs none
5
+ of it. An application that uses `@nxgt/janus-mail` never runs this build.
6
+
7
+ ```sh
8
+ cd packages/janus-mail
9
+ bun run build # build:mail, then the TypeScript build
10
+ bun run build:mail # the e-mails alone
11
+ ```
12
+
13
+ ## Maizzle runs at our build, never at the consumer's
14
+
15
+ The e-mails are compiled once, when this package is built: Maizzle, Vue,
16
+ Tailwind CSS and the `@nxgt/mail-*` build packages are **devDependencies**.
17
+ The tarball carries their output — HTML and text per locale, and a manifest —
18
+ and at send time `@nxgt/mail/renderer` fills its placeholders. A consumer
19
+ installs `@nxgt/mail` and nothing that builds.
20
+
21
+ ```text
22
+ packages/janus-mail/
23
+ mail/ the Maizzle project — not shipped
24
+ maizzle.config.ts presets({ only: the 5 }), ui(), i18n()
25
+ locales/en.json {} — the presets' messages, unchanged
26
+ locales/fr.json
27
+ mails/ what the build wrote — shipped, never committed
28
+ mail-manifest.json
29
+ en/verify-email.html en/verify-email.txt …
30
+ fr/verify-email.html fr/verify-email.txt …
31
+ scripts/build-mail.ts runs maizzle build, checks the manifest, writes locales.ts
32
+ src/generated/ written by the build — committed
33
+ mail.ts MailEmails: each e-mail and its variables
34
+ locales.ts LOCALES and JanusMailLocale
35
+ ```
36
+
37
+ ## What `build:mail` does
38
+
39
+ 1. `maizzle build` in `mail/`, with `productionConfig`: minified HTML, a
40
+ text part for each e-mail, written to `../mails/`. Maizzle empties the
41
+ folder first, so an e-mail removed leaves no file behind.
42
+ 2. `@nxgt/mail-i18n` writes `mails/mail-manifest.json` and
43
+ `src/generated/mail.ts` (`rendererTypes`), from the build.
44
+ 3. `scripts/build-mail.ts` reads the manifest and **fails unless its
45
+ format is one every `@nxgt/mail` the peer admits reads** — see below —
46
+ and **unless exactly the five e-mails were built**: a sixth from a new
47
+ `@nxgt/mail-presets`, or one missing, stops the build.
48
+ 4. It writes `src/generated/locales.ts` from the manifest's locales, only
49
+ when its content changed.
50
+
51
+ Then `../../build.ts` builds `dist/`, as for every package.
52
+
53
+ ## The manifest's format keeps the peer honest
54
+
55
+ The package peers `@nxgt/mail` at `>=0.1.0 <1`, so the build it ships must
56
+ be readable by `@nxgt/mail` 0.1.0. The manifest says which format it is in —
57
+ `formatVersion`, its first key, `MANIFEST_FORMAT` of the `@nxgt/mail-i18n`
58
+ that built it — and within 0.x a renderer reads every format up to its own.
59
+ `@nxgt/mail` 0.1.0 through 0.5.0 read format 1, and a manifest without the
60
+ field is format 1.
61
+
62
+ `formatProblem` in `scripts/build-mail.ts` (spec'd beside it) fails the build
63
+ when:
64
+
65
+ | Message | Cause | Fix |
66
+ | --- | --- | --- |
67
+ | `build-mail: mails/mail-manifest.json is manifest format 2, where @nxgt/mail-i18n writes 1 — a mails/ left by another build; run the build again` | `mails/` is not what this `@nxgt/mail-i18n` wrote | Run `bun run build:mail` again |
68
+ | `build-mail: mails/mail-manifest.json is manifest format 2, and @nxgt/mail 0.1.0, the peer's floor, reads up to 1 — raise the @nxgt/mail peer's floor to the first version that reads it` | A new `@nxgt/mail-i18n` writes a format the floor cannot read | Raise the peer's floor, then `PEER_FLOOR_READS` with it |
69
+
70
+ `PEER_FLOOR_READS` is a constant, `1`, rather than `MANIFEST_FORMAT` from
71
+ `@nxgt/mail/renderer`: the devDependency is newer than the floor, and
72
+ `@nxgt/mail` 0.5.0 and earlier export no `MANIFEST_FORMAT`. The pattern is
73
+ `@nxgt/mail-i18n`'s
74
+ [Shipping a build in a package](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/manifest.md#shipping-a-build-in-a-package).
75
+
76
+ ## The brand is a placeholder
77
+
78
+ `maizzle.config.ts` gives `ui()` the brand `{ name: '{{ brand }}' }`. Every
79
+ place `@nxgt/mail-ui`'s layout and the presets write the brand's name — the
80
+ header, the body, the footer — holds the placeholder `{{ brand }}`, so the
81
+ manifest lists `brand` among each e-mail's variables, and the renderer
82
+ requires it and escapes it like any other value. That is why the brand is
83
+ text only: a URL or a logo must be absolute when `ui()` is called.
84
+
85
+ The build's check is the renderer's own: a variable left out of `render`
86
+ throws, so no e-mail can go out with `{{ brand }}` in it. The specs render
87
+ each e-mail in both locales and search every part for `{{`.
88
+
89
+ ## The locales are the catalogues
90
+
91
+ `locales` is read from `mail/locales/*.json`, `en` first — the fallback
92
+ locale every other catalogue is checked against. The catalogues are `{}`:
93
+ the presets' messages are used as they are. A new file adds a locale, and
94
+ the build fails until it holds every message the presets use in `en` (see
95
+ `@nxgt/mail-presets`'
96
+ [Another locale](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-presets/docs/guide/presets.md#another-locale)).
97
+
98
+ ## Why `mails/` is not in `dist/`
99
+
100
+ The root `build.ts` removes from `dist/` every file it did not write itself,
101
+ so a module moved or deleted never ships. The built e-mails would be removed
102
+ on every TypeScript build. They live beside it, in `mails/`, listed in
103
+ `files`, and ignored by the package's `.gitignore`.
104
+
105
+ `src/mails.ts` resolves the folder from its own URL,
106
+ `new URL('../mails/', import.meta.url)`, which is right from `src/` in the
107
+ specs and from `dist/index.js` in the tarball — because the module sits
108
+ directly under `src/` and the package has one entry point. A second entry
109
+ point would move it into `dist/chunks/`, one level deeper: the artifact spec
110
+ renders from `../dist` and would fail.
111
+
112
+ `scripts/verify-artifacts.ts` checks that every `files` entry is in the
113
+ packed tarball, so a `mails/` that was not built, or that a packer left out,
114
+ fails before a release.
115
+
116
+ ## What is committed
117
+
118
+ | Path | Committed | Why |
119
+ | --- | --- | --- |
120
+ | `mail/` | yes | The source of the build |
121
+ | `mails/` | no | Rebuilt by every build; shipped in the tarball |
122
+ | `mail/.maizzle/` | no | Maizzle's own generated files |
123
+ | `src/generated/` | yes | So the package type-checks without a Maizzle build. CI rebuilds it and runs `git diff --exit-code` on it: a stale copy fails. `generated/mail.ts`'s header says *never committed* — `@nxgt/mail-i18n`'s advice to an application; this package commits it on purpose |
124
+
125
+ ## See also
126
+
127
+ - `@nxgt/mail-i18n`'s [manifest guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/manifest.md)
128
+ — the manifest and `generated/mail.ts`.
129
+ - `@nxgt/mail-ui`'s [plugin guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/docs/guide/plugin.md)
130
+ — `ui({ brand, theme })`.
@@ -0,0 +1,113 @@
1
+ # Locales
2
+
3
+ This page is for sending in the recipient's language: how the locale of each
4
+ e-mail is picked, what `locales` and `fallbackLocale` change, and how to add a
5
+ language the defaults are not built in.
6
+
7
+ ```ts
8
+ import { parseAcceptLanguage } from '@nxgt/mail';
9
+
10
+ await mail.verifyEmail(issued, {
11
+ name: user.name,
12
+ locale: [user.locale, ...parseAcceptLanguage(request.headers.get('accept-language'))],
13
+ });
14
+ // user.locale 'fr-CA' → fr; null, with 'de,en;q=0.8' → en; nothing matching → the fallback
15
+ ```
16
+
17
+ ## How the locale is picked
18
+
19
+ Each method passes `to.locale` to `@nxgt/mail`'s `pickLocale`, with
20
+ `locales` and `fallbackLocale`:
21
+
22
+ 1. For each wanted locale, in order, an exact match wins (case, `_` and `-`
23
+ do not matter), then a match on the language: `fr-CA` sends `fr`.
24
+ 2. Nothing matching — or `null`, `undefined`, an empty list — sends
25
+ `fallbackLocale`.
26
+
27
+ `to.locale` is a `WantedLocales`: a string, `null`, `undefined`, or a list of
28
+ them, most wanted first. **The locale is the recipient's**, not the
29
+ request's: an administrator who resets a user's password sends in the
30
+ *user's* locale. Put their stored locale first, and the visitor's
31
+ `Accept-Language` after it only when the recipient is the visitor.
32
+
33
+ | `to.locale` | Sent in |
34
+ | --- | --- |
35
+ | `'fr'`, `'fr-CA'`, `'FR_be'` | `fr` |
36
+ | `'en-GB'` | `en` |
37
+ | `'de'` | the fallback, `en` |
38
+ | `null`, `undefined`, `[]` | the fallback |
39
+ | `[null, 'de-DE', 'fr']` | `fr` |
40
+
41
+ The template is given the locale picked — `'fr'`, never `'fr-CA'` — so an
42
+ override can branch on it.
43
+
44
+ ## `locales` and `fallbackLocale`
45
+
46
+ | Option | Default | Effect |
47
+ | --- | --- | --- |
48
+ | `locales` | `['en', 'fr']`, the built ones | The locales an e-mail can be sent in. Its type is inferred as written: `['en']` makes `L` `'en'` |
49
+ | `fallbackLocale` | `'en'` when in `locales`, else the first of them | Sent when the recipient wants none of `locales`. Must be one of them: another is a compile error, and a `TypeError` |
50
+
51
+ **Fewer locales.** `locales: ['en']` sends English to everyone, a French
52
+ speaker included; the defaults still fit, and `templates` stays partial.
53
+
54
+ ```ts
55
+ const mail = janusMail({ mailer, from, brand: 'Acme', links, locales: ['en'] });
56
+ mail.locales; // readonly 'en'[]
57
+ ```
58
+
59
+ **Another fallback.** A service whose users are mostly French:
60
+
61
+ ```ts
62
+ janusMail({ mailer, from, brand: 'Acme', links, fallbackLocale: 'fr' });
63
+ ```
64
+
65
+ ## Adding a locale
66
+
67
+ The defaults are built in `en` and `fr` only. A locale beyond them needs
68
+ **every template**: a default could not render it. `JanusMailOptions` makes
69
+ `templates` required and whole once `locales` holds one:
70
+
71
+ ```ts
72
+ import { janusMail, type JanusMailTemplates } from '@nxgt/janus-mail';
73
+
74
+ type Locale = 'en' | 'fr' | 'de';
75
+
76
+ const templates: JanusMailTemplates<Locale> = {
77
+ verifyEmail: ({ name, link, locale }) => render('verify-email', locale, { name, link }),
78
+ resetPassword: ({ name, link, locale }) => render('reset-password', locale, { name, link }),
79
+ signInCode: ({ code, locale }) => render('sign-in-code', locale, { code }),
80
+ passwordChanged: ({ name, link, locale }) => render('password-changed', locale, { name, link }),
81
+ emailChanged: ({ name, link, newEmail, locale }) => render('email-changed', locale, { name, link, newEmail }),
82
+ };
83
+
84
+ const mail = janusMail({ mailer, from, brand: 'Acme', links, locales: ['en', 'fr', 'de'], templates });
85
+ mail.locales; // readonly ('en' | 'fr' | 'de')[]
86
+ ```
87
+
88
+ `render` here is yours — a renderer over a Maizzle build of your own, with
89
+ `@nxgt/mail-presets` and a `locales/de.json`, is the closest to the defaults.
90
+ With only some templates, the call does not compile — the error lands on
91
+ `templates`:
92
+
93
+ ```text
94
+ TS2739: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': resetPassword, signInCode, passwordChanged, emailChanged
95
+ ```
96
+
97
+ and in JavaScript it throws when `janusMail()` is called:
98
+
99
+ ```text
100
+ TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, passwordChanged, emailChanged missing
101
+ ```
102
+
103
+ A default template is typed for `'en' | 'fr'`, so reusing one in a wider set
104
+ is refused too: `verifyEmail: janusTemplates().verifyEmail` in a
105
+ `JanusMailTemplates<'en' | 'fr' | 'de'>` is a compile error.
106
+
107
+ Built-in locales for more languages are on the [roadmap](../roadmap.md).
108
+
109
+ ## See also
110
+
111
+ - `@nxgt/mail`'s [locales guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/locales.md)
112
+ — `pickLocale` and `parseAcceptLanguage` in full.
113
+ - [Templates](templates.md) — writing the five for a new locale.