@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steve Tsala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# @nxgt/mail-i18n
|
|
2
|
+
|
|
3
|
+
i18n for a normal [Maizzle](https://maizzle.com) 6 project of transactional
|
|
4
|
+
e-mails: write **one template per e-mail**, its text as keys into
|
|
5
|
+
[ICU](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
|
|
6
|
+
catalogues, and one `maizzle build` writes it in every locale. The build also
|
|
7
|
+
writes a manifest of each e-mail's placeholders and subjects. A catalogue or a
|
|
8
|
+
template that cannot be right **fails the build**, and the error names the
|
|
9
|
+
locale and the key.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// maizzle.config.ts
|
|
13
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
14
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
15
|
+
|
|
16
|
+
export default defineMailConfig({
|
|
17
|
+
plugins: [i18n({ locales: ['en', 'fr'], fallbackLocale: 'en' })],
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Your project stays a Maizzle project: `emails/`, `components/`, `public/`,
|
|
22
|
+
`maizzle serve`, `maizzle build`. The plugin adds `locales/`, and gives each
|
|
23
|
+
template `t`, `locale` and `placeholder`.
|
|
24
|
+
|
|
25
|
+
> **0.x.** A minor version may still change the surface; the changelog says how.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bun add @nxgt/mail-i18n @nxgt/mail-config @maizzle/framework @maizzle/tailwindcss
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Peers:
|
|
34
|
+
|
|
35
|
+
- `@maizzle/framework` (`^6.1.7`), required — Maizzle itself.
|
|
36
|
+
- `@nxgt/mail-config`, required — the i18n plugin is a plugin for its
|
|
37
|
+
`defineMailConfig`. It needs `@maizzle/tailwindcss` (`^1.5.6`) **as a direct
|
|
38
|
+
dependency of your project**; see [Setup](#setup).
|
|
39
|
+
- `typescript` (6), required. Bundler resolution
|
|
40
|
+
(`"moduleResolution": "bundler"`) is what is supported and tested; `nodenext`
|
|
41
|
+
is out of contract.
|
|
42
|
+
- `vue` (`^3.5`), **optional** — Maizzle already brings it. List it in your
|
|
43
|
+
own `package.json` when you want `t`, `locale` and `placeholder` typed in
|
|
44
|
+
templates ([Editor and type checking](#editor-and-type-checking)).
|
|
45
|
+
|
|
46
|
+
## Setup
|
|
47
|
+
|
|
48
|
+
```gitignore
|
|
49
|
+
# .gitignore — the official starter already has it
|
|
50
|
+
.maizzle/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The plugin writes one generated file per template and locale under
|
|
54
|
+
`.maizzle/i18n/`. They are rewritten on every run, never edited, and never
|
|
55
|
+
committed.
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
/* in the <style> your layout imports Tailwind from */
|
|
59
|
+
@import "@maizzle/tailwindcss";
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That import is resolved from your project. Under an isolated install (Bun
|
|
63
|
+
workspaces, pnpm), it fails **silently** unless `@maizzle/tailwindcss` is in
|
|
64
|
+
your own `package.json`, as the `bun add` above makes it: the build succeeds,
|
|
65
|
+
and no Tailwind utility is generated.
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
// tsconfig.json — the official starter's include; keep .maizzle/*.d.ts in it
|
|
69
|
+
{ "include": ["**/*.vue", ".maizzle/*.d.ts"] }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```jsonc
|
|
73
|
+
// package.json — the starter's postinstall
|
|
74
|
+
{ "scripts": { "postinstall": "maizzle prepare" } }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each time the config loads (`maizzle prepare`, `serve`, `build`), the plugin
|
|
78
|
+
writes `.maizzle/nxgt-mail-i18n.d.ts`: the types of `t`, from your catalogues.
|
|
79
|
+
The starter's `tsconfig.json` does not include `maizzle.config.ts`, so this
|
|
80
|
+
file is how the editor learns them. See
|
|
81
|
+
[Editor and type checking](#editor-and-type-checking).
|
|
82
|
+
|
|
83
|
+
## Exports
|
|
84
|
+
|
|
85
|
+
| Export | What it is |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `i18n(options)` | The plugin, for `defineMailConfig({ plugins })` |
|
|
88
|
+
| `createTranslator(catalogues, getLanguage)` | `t(key, args?, language?)` outside templates, shaped like `@nxgt/i18n`, but it throws on a missing key |
|
|
89
|
+
| `emailKey(email)` | Where an e-mail's messages live: `auth/reset-password` → `auth.resetPassword` |
|
|
90
|
+
| `MANIFEST_FILE` | `'mail-manifest.json'`, the manifest's name in the output folder |
|
|
91
|
+
| `WRAPPERS_DIR` | `'.maizzle/i18n'`, where the generated files go |
|
|
92
|
+
| `I18nOptions`, `Layout`, `TemplateSource` | The plugin's options, `'nested' \| 'flat'`, and a package's folder of templates for `templates` |
|
|
93
|
+
| `Catalogue`, `Catalogues`, `ArgumentKind` | A catalogue as written, catalogues by locale, and `'string' \| 'number' \| 'date'` |
|
|
94
|
+
| `Translate`, `LanguageProvider`, `MessageArgs` | What `createTranslator` answers and takes |
|
|
95
|
+
| `Manifest`, `ManifestEmail` | The shape of `dist/mail-manifest.json` |
|
|
96
|
+
| `TemplateMessages`, `TemplateKey`, `TemplateArgs` | The keys of `t` in templates and their arguments, filled from your catalogues by the generated `.maizzle/nxgt-mail-i18n.d.ts` |
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
### A project in two languages
|
|
101
|
+
|
|
102
|
+
A catalogue per locale, nested, `camelCase` keys:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
// locales/en.json — locales/fr.json has the same keys
|
|
106
|
+
{
|
|
107
|
+
"verifyEmail": {
|
|
108
|
+
"subject": "Confirm your e-mail address, {name}",
|
|
109
|
+
"title": "Confirm your e-mail address",
|
|
110
|
+
"greeting": "Hello {name},",
|
|
111
|
+
"expires": "The link expires in {minutes, plural, one {# minute} other {# minutes}}.",
|
|
112
|
+
"action": "Confirm my address"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
One template, every language:
|
|
118
|
+
|
|
119
|
+
```vue
|
|
120
|
+
<!-- emails/verify-email.vue -->
|
|
121
|
+
<template>
|
|
122
|
+
<Html :lang="locale">
|
|
123
|
+
<Body>
|
|
124
|
+
<Container>
|
|
125
|
+
<Heading>{{ t('verifyEmail.title') }}</Heading>
|
|
126
|
+
<Text>{{ t('verifyEmail.greeting', { name: placeholder('name') }) }}</Text>
|
|
127
|
+
<Text>{{ t('verifyEmail.expires', { minutes: 15 }) }}</Text>
|
|
128
|
+
<Button :href="placeholder('link')">{{ t('verifyEmail.action') }}</Button>
|
|
129
|
+
</Container>
|
|
130
|
+
</Body>
|
|
131
|
+
</Html>
|
|
132
|
+
</template>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `t(key, args?)` formats the message in the locale being built.
|
|
136
|
+
- `locale` is that locale, `'en'` or `'fr'`.
|
|
137
|
+
- `placeholder('name')` writes `{{ name }}` in the built file, for a value that
|
|
138
|
+
is only known at send time. Write it in text, in an attribute, or pass it as
|
|
139
|
+
an argument.
|
|
140
|
+
|
|
141
|
+
`maizzle serve` lists the template once per locale. `maizzle build` writes:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
dist/
|
|
145
|
+
en/verify-email.html <p>Hello {{ name }},</p> … <a href="{{ link }}">
|
|
146
|
+
en/verify-email.txt
|
|
147
|
+
fr/verify-email.html <p>Bonjour {{ name }},</p> … Le lien expire dans 15 minutes.
|
|
148
|
+
fr/verify-email.txt
|
|
149
|
+
mail-manifest.json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The `.txt` files come from `@nxgt/mail-config`'s base config. The e-mail is
|
|
153
|
+
named after its template's path, without `.vue`: `emails/auth/reset-password.vue`
|
|
154
|
+
is `auth/reset-password`, written to `dist/<locale>/auth/reset-password.html`.
|
|
155
|
+
See [Templates](docs/guide/templates.md).
|
|
156
|
+
|
|
157
|
+
### Options
|
|
158
|
+
|
|
159
|
+
| Option | Type | Default | Effect |
|
|
160
|
+
| --- | --- | --- | --- |
|
|
161
|
+
| `locales` | `readonly string[]` | — (required) | Every locale to build, as BCP 47 tags: `['en', 'fr', 'pt-BR']` |
|
|
162
|
+
| `fallbackLocale` | `string` | the first of `locales` | The reference catalogue: every other locale must have its keys, and may not add an argument |
|
|
163
|
+
| `dir` | `string` | `'locales'` | The folder of `<locale>.json` catalogues, relative to where `maizzle` runs |
|
|
164
|
+
| `emails` | `string` | `'emails'` | The folder of templates |
|
|
165
|
+
| `layout` | `'nested' \| 'flat'` | `'nested'` | `nested` writes `dist/en/verify-email.html`; `flat` writes `dist/verify-email.en.html` |
|
|
166
|
+
| `catalogues` | `readonly Catalogues[]` | `[]` | Catalogues a package ships, merged in order **under** your `<locale>.json`, key by key |
|
|
167
|
+
| `templates` | `readonly TemplateSource[]` | `[]` | Folders of templates a package ships, built with yours; your template of the same name wins |
|
|
168
|
+
| `rendererTypes` | `string \| false` | `'generated/mail.ts'` | The `.ts` module, relative to where `maizzle` runs, that each build writes `MailEmails` to, for `createMailRenderer<MailEmails>`. `false` writes none |
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// maizzle.config.ts — every file of one e-mail side by side
|
|
172
|
+
export default defineMailConfig({
|
|
173
|
+
plugins: [i18n({ locales: ['en', 'fr'], layout: 'flat' })],
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A wrong option is a bare `TypeError` when the config loads:
|
|
178
|
+
`i18n: fallbackLocale must be one of locales`.
|
|
179
|
+
|
|
180
|
+
### Catalogues from a package
|
|
181
|
+
|
|
182
|
+
A package can ship messages your templates share, such as `@nxgt/mail-ui`'s
|
|
183
|
+
`common.greeting`. List them in `catalogues`. Your own
|
|
184
|
+
`locales/<locale>.json` is still required for every locale, and wins key by
|
|
185
|
+
key:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// maizzle.config.ts
|
|
189
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
190
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
191
|
+
import { uiCatalogues } from '@nxgt/mail-ui';
|
|
192
|
+
|
|
193
|
+
export default defineMailConfig({
|
|
194
|
+
plugins: [i18n({ locales: ['en', 'fr'], catalogues: [uiCatalogues] })],
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
// locales/en.json — common.footer.why and common.footer.ignore stay the package's
|
|
200
|
+
{ "common": { "greeting": "Hi {name}," } }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The merged catalogues are checked like your own. A source's locale your
|
|
204
|
+
project does not build is left out. See
|
|
205
|
+
[Catalogues](docs/guide/catalogues.md#catalogues-from-a-package).
|
|
206
|
+
|
|
207
|
+
### Templates from a package
|
|
208
|
+
|
|
209
|
+
A package can ship whole e-mails, such as `@nxgt/mail-presets`. List its
|
|
210
|
+
folders in `templates`, and its messages in `catalogues`:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// maizzle.config.ts
|
|
214
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
215
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
216
|
+
import { presets } from '@nxgt/mail-presets';
|
|
217
|
+
import { ui, uiCatalogues } from '@nxgt/mail-ui';
|
|
218
|
+
|
|
219
|
+
const mails = presets({ only: ['verify-email', 'reset-password'] });
|
|
220
|
+
// mails.templates → { dir: '/…/node_modules/@nxgt/mail-presets/emails', emails: ['verify-email', 'reset-password'] }
|
|
221
|
+
|
|
222
|
+
export default defineMailConfig({
|
|
223
|
+
plugins: [
|
|
224
|
+
ui({ brand: { name: 'Acme' } }),
|
|
225
|
+
i18n({
|
|
226
|
+
locales: ['en', 'fr'],
|
|
227
|
+
catalogues: [uiCatalogues, mails.catalogues],
|
|
228
|
+
templates: [mails.templates],
|
|
229
|
+
}),
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
A `TemplateSource` is `{ dir, emails? }`: `dir` an absolute folder that holds
|
|
235
|
+
templates, `emails` a non-empty list of the ones to build (every one when left
|
|
236
|
+
out). Your `emails/` is read first: `emails/verify-email.vue` replaces the
|
|
237
|
+
package's `verify-email`. Two packages that ship the same e-mail are refused
|
|
238
|
+
when the config loads. See
|
|
239
|
+
[Templates](docs/guide/templates.md#templates-from-a-package).
|
|
240
|
+
|
|
241
|
+
### The subject
|
|
242
|
+
|
|
243
|
+
Every e-mail needs a subject, in every locale: the message
|
|
244
|
+
`<emailKey>.subject`. `emailKey` turns the template's path into a key:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { emailKey } from '@nxgt/mail-i18n';
|
|
248
|
+
|
|
249
|
+
emailKey('verify-email'); // 'verifyEmail' → verifyEmail.subject
|
|
250
|
+
emailKey('auth/reset-password'); // 'auth.resetPassword' → auth.resetPassword.subject
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The template does not write the subject. The build formats it once per locale
|
|
254
|
+
into the manifest, and turns each argument into a placeholder:
|
|
255
|
+
`"Confirm your e-mail address, {name}"` becomes
|
|
256
|
+
`"Confirm your e-mail address, {{ name }}"`. A template with no subject fails
|
|
257
|
+
the build: `i18n: welcome has no subject — add welcome.subject to the
|
|
258
|
+
catalogues`. See [Catalogues](docs/guide/catalogues.md#the-subject).
|
|
259
|
+
|
|
260
|
+
### The manifest
|
|
261
|
+
|
|
262
|
+
`dist/mail-manifest.json` is written by `maizzle build`, for the code that
|
|
263
|
+
sends. For each e-mail, it records its placeholders, the ones a link or an
|
|
264
|
+
image URL starts with, its subject in each locale, and its files:
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"locales": ["en", "fr"],
|
|
269
|
+
"fallbackLocale": "en",
|
|
270
|
+
"emails": {
|
|
271
|
+
"verify-email": {
|
|
272
|
+
"variables": ["link", "name"],
|
|
273
|
+
"urlVariables": ["link"],
|
|
274
|
+
"subject": {
|
|
275
|
+
"en": "Confirm your e-mail address, {{ name }}",
|
|
276
|
+
"fr": "Confirmez votre adresse e-mail, {{ name }}"
|
|
277
|
+
},
|
|
278
|
+
"files": {
|
|
279
|
+
"en": { "html": "en/verify-email.html", "text": "en/verify-email.txt" },
|
|
280
|
+
"fr": { "html": "fr/verify-email.html", "text": "fr/verify-email.txt" }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`createMailRenderer` from `@nxgt/mail/renderer` reads it at send time:
|
|
288
|
+
`createMailRenderer({ dir: 'dist' }).render('verify-email', { name, link })`
|
|
289
|
+
answers the subject, HTML and text, every value escaped.
|
|
290
|
+
|
|
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. **Commit it**: the
|
|
293
|
+
code that sends then type-checks without running a build.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
// generated/mail.ts — never edited
|
|
297
|
+
export interface MailEmails {
|
|
298
|
+
"verify-email": { readonly link: string; readonly name: string | number };
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// in the code that sends
|
|
304
|
+
import { createMailRenderer } from '@nxgt/mail/renderer';
|
|
305
|
+
import type { MailEmails } from './generated/mail';
|
|
306
|
+
|
|
307
|
+
const mails = createMailRenderer<MailEmails>({ dir: 'dist' });
|
|
308
|
+
mails.render('verify-email', { name: 'Ada', link: 'https://app.example.com/v' });
|
|
309
|
+
// mails.render('verify-emial', …) and a missing `link` no longer compile
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
A URL variable is `string`, any other `string | number`. Set
|
|
313
|
+
`rendererTypes` to write it elsewhere, or `false` for none. See
|
|
314
|
+
[The manifest](docs/guide/manifest.md) and its
|
|
315
|
+
[renderer's types](docs/guide/manifest.md#the-renderers-types--generatedmailts).
|
|
316
|
+
|
|
317
|
+
### Outside templates — `createTranslator`
|
|
318
|
+
|
|
319
|
+
The same catalogues, from your application's code:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { pickLocale } from '@nxgt/mail';
|
|
323
|
+
import { createTranslator } from '@nxgt/mail-i18n';
|
|
324
|
+
import en from './locales/en.json';
|
|
325
|
+
import fr from './locales/fr.json';
|
|
326
|
+
|
|
327
|
+
declare const user: { locale: string | null };
|
|
328
|
+
|
|
329
|
+
const t = createTranslator({ en, fr }, () => pickLocale(user.locale, ['en', 'fr'], 'en'));
|
|
330
|
+
|
|
331
|
+
t('verifyEmail.expires', { minutes: 15 }); // in the user's locale
|
|
332
|
+
t('verifyEmail.expires', { minutes: 15 }, 'fr'); // 'Le lien expire dans 15 minutes.'
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`pickLocale` comes from `@nxgt/mail` (`bun add @nxgt/mail`); `getLanguage`
|
|
336
|
+
can be any function that returns one of the catalogues' locales.
|
|
337
|
+
|
|
338
|
+
`createTranslator(catalogues, getLanguage)` and `t(key, args, language?)`
|
|
339
|
+
have the shape of `@nxgt/i18n`, with one difference: this `t` **throws**
|
|
340
|
+
where `@nxgt/i18n` would answer the key. It throws on a key the catalogue
|
|
341
|
+
does not have, on a language with no catalogue, and on a message that does
|
|
342
|
+
not format. An e-mail never goes out with `verifyEmail.title` or `{link}` in
|
|
343
|
+
it. See [Translating outside templates](docs/guide/translator.md).
|
|
344
|
+
|
|
345
|
+
### Editor and type checking
|
|
346
|
+
|
|
347
|
+
With the [Setup](#setup) above, Vue's language tools (the **Vue - Official**
|
|
348
|
+
extension in the editor, `vue-tsc` in CI) complete the keys of `t` and flag a
|
|
349
|
+
call the build would refuse:
|
|
350
|
+
|
|
351
|
+
```vue
|
|
352
|
+
<!-- emails/verify-email.vue, with the catalogue above -->
|
|
353
|
+
<template>
|
|
354
|
+
<p>{{ t('verifyEmail.titel') }}</p>
|
|
355
|
+
<!-- Argument of type '"verifyEmail.titel"' is not assignable to parameter of type 'keyof TemplateMessages'. -->
|
|
356
|
+
<p>{{ t('verifyEmail.greeting') }}</p>
|
|
357
|
+
<!-- Expected 2 arguments, but got 1. -->
|
|
358
|
+
<p>{{ t('verifyEmail.expires', { minutes: placeholder('minutes') }) }}</p>
|
|
359
|
+
<!-- Type 'string' is not assignable to type 'number'. -->
|
|
360
|
+
</template>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```sh
|
|
364
|
+
bun add -d vue-tsc vue
|
|
365
|
+
bunx vue-tsc --noEmit # after maizzle prepare, as in CI
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The types come from the fallback locale's catalogue, merged with the
|
|
369
|
+
`catalogues` option, and refresh on the next config load: saving a catalogue
|
|
370
|
+
under `maizzle serve`, or running `maizzle prepare` or `maizzle build`.
|
|
371
|
+
Without the generated file, the template checker does not know `t`,
|
|
372
|
+
`locale` or `placeholder` at all (`Property 't' does not exist`); the build is
|
|
373
|
+
not affected. See [Editor and type checking](docs/guide/editor.md).
|
|
374
|
+
|
|
375
|
+
## Traps
|
|
376
|
+
|
|
377
|
+
**Leave `content` to the plugin.** It points Maizzle at the generated files. A
|
|
378
|
+
`content` in your config replaces that, and each template then fails with
|
|
379
|
+
`… is not built through the i18n plugin`.
|
|
380
|
+
|
|
381
|
+
**Run `maizzle` from the project root.** `dir`, `emails` and `.maizzle/i18n`
|
|
382
|
+
are resolved against the directory `maizzle` runs in.
|
|
383
|
+
|
|
384
|
+
**Nest keys; never dot them.** `{ "verifyEmail": { "title": "…" } }`, not
|
|
385
|
+
`{ "verifyEmail.title": "…" }`, which fails the build as not camelCase.
|
|
386
|
+
|
|
387
|
+
**Never write `{{ name }}` by hand in a template.** Vue would evaluate it.
|
|
388
|
+
Write `{{ placeholder('name') }}`.
|
|
389
|
+
|
|
390
|
+
**Never branch on a placeholder.** The template is built before the value
|
|
391
|
+
exists: `v-if="link.startsWith('https:')"` would test the string `{{ link }}`.
|
|
392
|
+
|
|
393
|
+
**Keep `url.base` off for links.** It would prefix `{{ link }}` in every
|
|
394
|
+
`href`.
|
|
395
|
+
|
|
396
|
+
**A subject's arguments are plain `{name}`.** They become placeholders, filled
|
|
397
|
+
as strings at send time. `{count, number}` or a `select` in a subject fails
|
|
398
|
+
the build.
|
|
399
|
+
|
|
400
|
+
**List the plugin that brings a template's components.** A tag that
|
|
401
|
+
resolves to no component renders nothing, and the build fails with
|
|
402
|
+
`i18n: fr/welcome.html is empty — a tag of its template resolved to no
|
|
403
|
+
component; list the plugin that brings it, as ui()`.
|
|
404
|
+
|
|
405
|
+
**Leave the output paths to the plugin.** An `output.path` set in a
|
|
406
|
+
template, or a `plaintext.destination`, moves a file out of the plugin's
|
|
407
|
+
layout, and the build fails when the manifest is written.
|
|
408
|
+
|
|
409
|
+
## Type safety, counted
|
|
410
|
+
|
|
411
|
+
**18 plausible mistakes, 18 refused** at compile time. Each one is measured by
|
|
412
|
+
a `@ts-expect-error` in
|
|
413
|
+
[`test/types/refusals.ts`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/test/types/refusals.ts),
|
|
414
|
+
which fails the typecheck the moment it stops holding:
|
|
415
|
+
|
|
416
|
+
1. The plugin called without `locales`.
|
|
417
|
+
2. `locales` given one locale as a string rather than a list.
|
|
418
|
+
3. A `layout` other than `'nested'` or `'flat'`.
|
|
419
|
+
4. A catalogue with a leaf that is not a message (`expires: 15`).
|
|
420
|
+
5. `createTranslator` given a language that is neither a locale nor a
|
|
421
|
+
function that answers one.
|
|
422
|
+
6. An argument that is an object: only a string, a number or a `Date`.
|
|
423
|
+
7. `placeholder` given something other than a name.
|
|
424
|
+
8. A template that assigns `locale`.
|
|
425
|
+
9. A `fallbackLocale` that is not a string.
|
|
426
|
+
10. A folder option (`dir`, `emails`) that is not a string.
|
|
427
|
+
11. `t` called in a template with a key that is not a string.
|
|
428
|
+
12. `createTranslator`'s `t` given a language, for one call, that is neither
|
|
429
|
+
a locale nor a function that answers one.
|
|
430
|
+
13. `catalogues` given one catalogue by locale rather than a list of them.
|
|
431
|
+
14. `catalogues` holding a locale whose value is a message, not a catalogue.
|
|
432
|
+
15. `templates` given one folder rather than a list of them.
|
|
433
|
+
16. A template folder's `emails` given as one name rather than a list.
|
|
434
|
+
17. A template folder's `emails` given as an empty list.
|
|
435
|
+
18. `rendererTypes` given as `true` rather than a path.
|
|
436
|
+
|
|
437
|
+
The same file holds the calls that must keep compiling: a refusal that refuses
|
|
438
|
+
the correct call is a bug.
|
|
439
|
+
|
|
440
|
+
**7 template mistakes, 7 refused** by the types generated from the
|
|
441
|
+
catalogues, each measured by a `@vue-expect-error` in
|
|
442
|
+
[`test/fixture/types/refusals.vue`](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/test/fixture/types/refusals.vue),
|
|
443
|
+
checked by `vue-tsc` after `maizzle prepare` (`bun run typecheck:templates`):
|
|
444
|
+
|
|
445
|
+
1. A key the catalogues do not have (`t('verifyEmail.titel')`).
|
|
446
|
+
2. A message's argument left out.
|
|
447
|
+
3. An argument the message does not use.
|
|
448
|
+
4. A plural's count given as a placeholder, which is text.
|
|
449
|
+
5. A key written in `snake_case` (`t('verify_email.title')`).
|
|
450
|
+
6. A key that may be a message without arguments or one with
|
|
451
|
+
(`t(ok ? 'verifyEmail.title' : 'verifyEmail.expires')`).
|
|
452
|
+
7. The same, given the arguments of only one of them.
|
|
453
|
+
|
|
454
|
+
The same file holds the template calls that must keep compiling. The build
|
|
455
|
+
still checks every call against every locale's catalogue; the types report
|
|
456
|
+
the same mistakes earlier.
|
|
457
|
+
|
|
458
|
+
## Documentation
|
|
459
|
+
|
|
460
|
+
- [The guides](docs/README.md) — one page per area, with every option and error.
|
|
461
|
+
- [Troubleshooting](docs/troubleshooting.md) — an error message, its cause and
|
|
462
|
+
its fix.
|
|
463
|
+
- [Roadmap](docs/roadmap.md) — what is next, and what is deliberately not
|
|
464
|
+
planned.
|
|
465
|
+
- [Vocabulary](https://github.com/softistx/nxgt-mail/blob/develop/docs/vocabulary.md)
|
|
466
|
+
— the words these pages use, defined once.
|
|
467
|
+
|
|
468
|
+
## Licence
|
|
469
|
+
|
|
470
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A catalogue as written: nested objects whose leaves are ICU messages, the
|
|
3
|
+
* conventions of `@nxgt/i18n`.
|
|
4
|
+
*
|
|
5
|
+
* ```json
|
|
6
|
+
* { "verifyEmail": { "subject": "Confirm your e-mail address" } }
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export interface Catalogue {
|
|
10
|
+
readonly [key: string]: string | Catalogue;
|
|
11
|
+
}
|
|
12
|
+
/** A catalogue per locale: `{ en: {...}, fr: {...} }`. */
|
|
13
|
+
export type Catalogues = Readonly<Record<string, Catalogue>>;
|
|
14
|
+
/**
|
|
15
|
+
* What an argument is, from the way a message uses it: `{n, number}` and
|
|
16
|
+
* `{n, plural, …}` a number, `{at, date}` a date, anything else a string.
|
|
17
|
+
*/
|
|
18
|
+
export type ArgumentKind = 'string' | 'number' | 'date';
|
|
19
|
+
/** One message, checked, with the kind of each argument it uses. */
|
|
20
|
+
export interface Message {
|
|
21
|
+
readonly text: string;
|
|
22
|
+
readonly args: ReadonlyMap<string, ArgumentKind>;
|
|
23
|
+
/** The arguments a `{x, select, …}` chooses on: a placeholder would always choose `other`. */
|
|
24
|
+
readonly selects: ReadonlySet<string>;
|
|
25
|
+
}
|
|
26
|
+
/** Every message of one locale, by dotted key: `verifyEmail.subject`. */
|
|
27
|
+
export type Messages = ReadonlyMap<string, Message>;
|
|
28
|
+
/**
|
|
29
|
+
* Each of `project`'s locales, with `sources` merged under it in order: a
|
|
30
|
+
* source's locale the project does not have is left out.
|
|
31
|
+
*/
|
|
32
|
+
export declare function layerCatalogues(sources: readonly Catalogues[], project: Record<string, Catalogue>): Record<string, Catalogue>;
|
|
33
|
+
/**
|
|
34
|
+
* Checks the catalogues of every locale and answers their messages. A
|
|
35
|
+
* catalogue that is not objects of camelCase keys, a message that does not
|
|
36
|
+
* parse, and a locale that differs from the fallback locale in its keys or
|
|
37
|
+
* its arguments **throw**, naming the locale and the key.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkCatalogues(catalogues: Catalogues, locales: readonly string[], fallbackLocale: string): ReadonlyMap<string, Messages>;
|
|
40
|
+
//# sourceMappingURL=catalogues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalogues.d.ts","sourceRoot":"","sources":["../src/catalogues.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACzB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,0DAA0D;AAC1D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAE7D;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExD,oEAAoE;AACpE,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,8FAA8F;IAC9F,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACtC;AAED,yEAAyE;AACzE,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA2BpD;;;GAGG;AACH,wBAAgB,eAAe,CAC9B,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAQ3B;AAsID;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,cAAc,EAAE,MAAM,GACpB,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAmB/B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nxgt/mail-i18n` — i18n for a Maizzle project, shaped like `@nxgt/i18n`.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // maizzle.config.ts
|
|
6
|
+
* import { defineMailConfig } from '@nxgt/mail-config';
|
|
7
|
+
* import { i18n } from '@nxgt/mail-i18n';
|
|
8
|
+
*
|
|
9
|
+
* export default defineMailConfig({
|
|
10
|
+
* plugins: [i18n({ locales: ['en', 'fr'] })],
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A template writes `{{ t('verifyEmail.title') }}`; `maizzle build` writes
|
|
15
|
+
* `dist/en/verify-email.html`, `dist/fr/verify-email.html` and
|
|
16
|
+
* `dist/mail-manifest.json`.
|
|
17
|
+
*/
|
|
18
|
+
import './vue';
|
|
19
|
+
export type { ArgumentKind, Catalogue, Catalogues, } from './catalogues';
|
|
20
|
+
export { emailKey, type Manifest, type ManifestEmail, } from './manifest';
|
|
21
|
+
export { type I18nOptions, i18n, MANIFEST_FILE, WRAPPERS_DIR, } from './plugin';
|
|
22
|
+
export type { TemplateSource } from './sources';
|
|
23
|
+
export { createTranslator, type LanguageProvider, type MessageArgs, type Translate, } from './translator';
|
|
24
|
+
export type { TemplateArgs, TemplateKey, TemplateMessages } from './vue';
|
|
25
|
+
export type { Layout } from './wrappers';
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,OAAO,CAAC;AAEf,YAAY,EACX,YAAY,EACZ,SAAS,EACT,UAAU,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,aAAa,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,KAAK,WAAW,EAChB,IAAI,EACJ,aAAa,EACb,YAAY,GACZ,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACN,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,SAAS,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACzE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
|