@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,1096 @@
|
|
|
1
|
+
# Troubleshooting `@nxgt/mail-i18n`
|
|
2
|
+
|
|
3
|
+
Each entry is headed by the message you see. The last section holds the traps
|
|
4
|
+
that fail no build — a build that succeeds and is wrong. Search this page for
|
|
5
|
+
the words of your message.
|
|
6
|
+
|
|
7
|
+
How the messages are shaped:
|
|
8
|
+
|
|
9
|
+
- **A wiring mistake is a `TypeError`**, from the call you wrote: `i18n: …`
|
|
10
|
+
when `maizzle.config.ts` loads, `createTranslator: …` when your server
|
|
11
|
+
creates its translator. Fix the call.
|
|
12
|
+
- **A build failure is a plain `Error` starting `i18n:`**, naming what to fix
|
|
13
|
+
(the locale, the template, the key or the file). It has no `code`: it is a
|
|
14
|
+
mistake in the templates or the catalogues, to fix, not a condition to
|
|
15
|
+
catch. The catalogues are checked when `maizzle.config.ts` loads, before
|
|
16
|
+
any template is built; a template is checked while it is rendered; the
|
|
17
|
+
subjects and the files after the build.
|
|
18
|
+
- **A run-time failure is a plain `Error` starting `t:`**, from the `t` that
|
|
19
|
+
`createTranslator` answers, and like a build failure it is a mistake to
|
|
20
|
+
fix. It throws where `@nxgt/i18n` would answer the key: an e-mail is not
|
|
21
|
+
sent with a key in it.
|
|
22
|
+
- **The error's own message never holds the text of a message.** Its
|
|
23
|
+
`cause` may: a "could not be formatted" error keeps the formatter's error
|
|
24
|
+
as its `cause`, and that error quotes the message, for debugging. A
|
|
25
|
+
catalogue's text is not a secret; the values filled at send time never
|
|
26
|
+
reach these errors.
|
|
27
|
+
|
|
28
|
+
The samples below use the locales `en` (the fallback locale) and `fr`, the
|
|
29
|
+
template `emails/verify-email.vue`, and keys such as `verifyEmail.title`.
|
|
30
|
+
|
|
31
|
+
## Index
|
|
32
|
+
|
|
33
|
+
**Install and types**
|
|
34
|
+
- [Which `moduleResolution` is supported](#install-and-types)
|
|
35
|
+
|
|
36
|
+
**Wiring**
|
|
37
|
+
- [`i18n: options must be an object, as { locales: ['en', 'fr'] }`](#i18n-options-must-be-an-object-as--locales-en-fr-)
|
|
38
|
+
- [`i18n: locales must hold at least one locale, as ['en', 'fr']`](#i18n-locales-must-hold-at-least-one-locale-as-en-fr)
|
|
39
|
+
- [`i18n: locales holds something that is not a locale — write each as a BCP 47 tag, as en or pt-BR`](#i18n-locales-holds-something-that-is-not-a-locale--write-each-as-a-bcp-47-tag-as-en-or-pt-br)
|
|
40
|
+
- [`i18n: locales holds the same locale twice`](#i18n-locales-holds-the-same-locale-twice)
|
|
41
|
+
- [`i18n: fallbackLocale must be one of locales`](#i18n-fallbacklocale-must-be-one-of-locales)
|
|
42
|
+
- [`i18n: dir must be a folder of the project`](#i18n-dir-must-be-a-folder-of-the-project)
|
|
43
|
+
- [`i18n: layout must be 'nested' or 'flat'`](#i18n-layout-must-be-nested-or-flat)
|
|
44
|
+
- [`i18n: catalogues must be a list of catalogues by locale, as [{ en: {...}, fr: {...} }]`](#i18n-catalogues-must-be-a-list-of-catalogues-by-locale-as--en--fr--)
|
|
45
|
+
- [`i18n: templates must be a list of template folders, as [{ dir: '/abs/path/emails' }] — emails, when given, names at least one, each once`](#i18n-templates-must-be-a-list-of-template-folders-as--dir-abspathemails---emails-when-given-names-at-least-one-each-once)
|
|
46
|
+
- [`i18n: templates[0] has no template sign-in.vue — name one of its e-mails`](#i18n-templates0-has-no-template-sign-invue--name-one-of-its-e-mails)
|
|
47
|
+
- [`i18n: templates[0] holds no template — is … the folder of a package's e-mails?`](#i18n-templates0-holds-no-template--is--the-folder-of-a-packages-e-mails)
|
|
48
|
+
- [`i18n: templates[0] and templates[1] both have welcome.vue — keep one with emails: [...], or write the project's own in its folder`](#i18n-templates0-and-templates1-both-have-welcomevue--keep-one-with-emails--or-write-the-projects-own-in-its-folder)
|
|
49
|
+
- [`i18n: rendererTypes must be the path of a .ts file, as generated/mail.ts, or false`](#i18n-renderertypes-must-be-the-path-of-a-ts-file-as-generatedmailts-or-false)
|
|
50
|
+
- [`createTranslator: catalogues must be an object of catalogues by locale, as { en, fr }`](#createtranslator-catalogues-must-be-an-object-of-catalogues-by-locale-as--en-fr-)
|
|
51
|
+
- [`createTranslator: getLanguage must be a locale or a function that answers one`](#createtranslator-getlanguage-must-be-a-locale-or-a-function-that-answers-one)
|
|
52
|
+
|
|
53
|
+
**Catalogues** — when `maizzle.config.ts` loads
|
|
54
|
+
- [`i18n: locales/fr.json is missing — every locale has a catalogue`](#i18n-localesfrjson-is-missing--every-locale-has-a-catalogue)
|
|
55
|
+
- [`i18n: locales/fr.json is not valid JSON`](#i18n-localesfrjson-is-not-valid-json)
|
|
56
|
+
- [`i18n: fr: the catalogue must be an object of messages`](#i18n-fr-the-catalogue-must-be-an-object-of-messages)
|
|
57
|
+
- [`i18n: en: verifyEmail.title must be a message (a string) or an object of messages`](#i18n-en-verifyemailtitle-must-be-a-message-a-string-or-an-object-of-messages)
|
|
58
|
+
- [`i18n: en: verify-email is not camelCase — every segment of a key is camelCase, and nested rather than dotted, as verifyEmail.title`](#i18n-en-verify-email-is-not-camelcase--every-segment-of-a-key-is-camelcase-and-nested-rather-than-dotted-as-verifyemailtitle)
|
|
59
|
+
- [`i18n: en: verifyEmail.greeting is not a valid ICU message (EXPECT_ARGUMENT_CLOSING_BRACE)`](#i18n-en-verifyemailgreeting-is-not-a-valid-icu-message-expect_argument_closing_brace)
|
|
60
|
+
- [`i18n: en: verifyEmail.greeting uses {first_name}, which is not camelCase — an argument is a camelCase name, as {firstName}`](#i18n-en-verifyemailgreeting-uses-first_name-which-is-not-camelcase--an-argument-is-a-camelcase-name-as-firstname)
|
|
61
|
+
- [`i18n: en: verifyEmail.expires uses {minutes} as number and as date`](#i18n-en-verifyemailexpires-uses-minutes-as-number-and-as-date)
|
|
62
|
+
- [`i18n: fr: verifyEmail.title is missing — en, the fallback locale, has it`](#i18n-fr-verifyemailtitle-is-missing--en-the-fallback-locale-has-it)
|
|
63
|
+
- [`i18n: fr: verifyEmail.titel is not a key of en, the fallback locale`](#i18n-fr-verifyemailtitel-is-not-a-key-of-en-the-fallback-locale)
|
|
64
|
+
- [`i18n: fr: verifyEmail.title uses {name}, which en does not declare`](#i18n-fr-verifyemailtitle-uses-name-which-en-does-not-declare)
|
|
65
|
+
- [`i18n: fr: verifyEmail.expires uses {minutes} as date, and en declares it as number`](#i18n-fr-verifyemailexpires-uses-minutes-as-date-and-en-declares-it-as-number)
|
|
66
|
+
|
|
67
|
+
**Templates** — while `maizzle build` renders
|
|
68
|
+
- [`[Vue warn]: Unhandled error during execution of render function`](#vue-warn-unhandled-error-during-execution-of-render-function)
|
|
69
|
+
- [`i18n: emails/Welcome.vue is not a kebab-case name — name a template as verify-email.vue`](#i18n-emailswelcomevue-is-not-a-kebab-case-name--name-a-template-as-verify-emailvue)
|
|
70
|
+
- [`i18n: emails/verify-email.vue is not built through the i18n plugin — leave content to it, and put templates in emails/`](#i18n-emailsverify-emailvue-is-not-built-through-the-i18n-plugin--leave-content-to-it-and-put-templates-in-emails)
|
|
71
|
+
- [`i18n: en: verify-email calls t('verifyEmail.titel'), which is not a key of the catalogues`](#i18n-en-verify-email-calls-tverifyemailtitel-which-is-not-a-key-of-the-catalogues)
|
|
72
|
+
- [`i18n: en: verify-email calls t('verifyEmail.expires') without {minutes}`](#i18n-en-verify-email-calls-tverifyemailexpires-without-minutes)
|
|
73
|
+
- [`i18n: en: verify-email passes {minutes} to verifyEmail.expires as a string — the message uses it as a number`](#i18n-en-verify-email-passes-minutes-to-verifyemailexpires-as-a-string--the-message-uses-it-as-a-number)
|
|
74
|
+
- [`i18n: en: verify-email passes {name} to verifyEmail.title, which does not use it`](#i18n-en-verify-email-passes-name-to-verifyemailtitle-which-does-not-use-it)
|
|
75
|
+
- [`i18n: en: verify-email calls t('verifyEmail.title') with arguments that are not an object, as { name: placeholder('name') }`](#i18n-en-verify-email-calls-tverifyemailtitle-with-arguments-that-are-not-an-object-as--name-placeholdername-)
|
|
76
|
+
- [`i18n: en: verify-email passes a placeholder to {plan}, which verifyEmail.title chooses on with a select — a placeholder always chooses other`](#i18n-en-verify-email-passes-a-placeholder-to-plan-which-verifyemailtitle-chooses-on-with-a-select--a-placeholder-always-chooses-other)
|
|
77
|
+
- [`i18n: en: verify-email calls placeholder() with a name that is not camelCase — as placeholder('firstName')`](#i18n-en-verify-email-calls-placeholder-with-a-name-that-is-not-camelcase--as-placeholderfirstname)
|
|
78
|
+
- [`i18n: en: verifyEmail.sentOn could not be formatted`](#i18n-en-verifyemailsenton-could-not-be-formatted)
|
|
79
|
+
|
|
80
|
+
**Manifest and subject** — after the build
|
|
81
|
+
- [`i18n: welcome has no subject — add welcome.subject to the catalogues`](#i18n-welcome-has-no-subject--add-welcomesubject-to-the-catalogues)
|
|
82
|
+
- [`i18n: en: verifyEmail.subject uses {minutes} as a number — a subject's arguments are placeholders, filled at send time as strings`](#i18n-en-verifyemailsubject-uses-minutes-as-a-number--a-subjects-arguments-are-placeholders-filled-at-send-time-as-strings)
|
|
83
|
+
- [`i18n: en: welcome.subject chooses on {kind} with a select — a subject's arguments are placeholders, which always choose other`](#i18n-en-welcomesubject-chooses-on-kind-with-a-select--a-subjects-arguments-are-placeholders-which-always-choose-other)
|
|
84
|
+
- [`i18n: fr/welcome.html is empty — a tag of its template resolved to no component; list the plugin that brings it, as ui()`](#i18n-frwelcomehtml-is-empty--a-tag-of-its-template-resolved-to-no-component-list-the-plugin-that-brings-it-as-ui)
|
|
85
|
+
- [`i18n: welcome was not built in fr`](#i18n-welcome-was-not-built-in-fr)
|
|
86
|
+
- [`i18n: ../text/welcome.en.txt was written outside the output folder — …`](#i18n-textwelcomeentxt-was-written-outside-the-output-folder--the-i18n-plugin-lays-out-every-e-mail-set-no-plaintextdestination-and-no-output-path-in-a-template)
|
|
87
|
+
- [`i18n: custom/welcome.html is not where the i18n plugin puts an e-mail — set no output path in a template`](#i18n-customwelcomehtml-is-not-where-the-i18n-plugin-puts-an-e-mail--set-no-output-path-in-a-template)
|
|
88
|
+
- [`i18n: src/index.ts was not written by i18n() — point rendererTypes at a file of its own`](#i18n-srcindexts-was-not-written-by-i18n--point-renderertypes-at-a-file-of-its-own)
|
|
89
|
+
|
|
90
|
+
**Run time** — `createTranslator`'s `t`
|
|
91
|
+
- [`t: the language is not a locale of the catalogues — pick one with pickLocale`](#t-the-language-is-not-a-locale-of-the-catalogues--pick-one-with-picklocale)
|
|
92
|
+
- [`t: fr: verifyEmail.titel is not a key`](#t-fr-verifyemailtitel-is-not-a-key)
|
|
93
|
+
- [`t: en: verifyEmail.expires could not be formatted`](#t-en-verifyemailexpires-could-not-be-formatted)
|
|
94
|
+
|
|
95
|
+
**Traps: a build that succeeds and is wrong**
|
|
96
|
+
- [`[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
|
+
- [A link's placeholder is prefixed with a domain](#a-links-placeholder-is-prefixed-with-a-domain)
|
|
98
|
+
- [`.maizzle/` shows up in `git status`](#maizzle-shows-up-in-git-status)
|
|
99
|
+
- [`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
|
+
- [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
|
+
- [The editor flags a key you just added to a catalogue](#the-editor-flags-a-key-you-just-added-to-a-catalogue)
|
|
102
|
+
- [Biome reports `parse` errors in a template as soon as you edit it](#biome-reports-parse-errors-in-a-template-as-soon-as-you-edit-it)
|
|
103
|
+
- [A bug in `@nxgt/mail-i18n` itself](#a-bug-in-nxgtmail-i18n-itself)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Install and types
|
|
108
|
+
|
|
109
|
+
Resolve as a bundler does (`"moduleResolution": "bundler"`, as Maizzle's jiti
|
|
110
|
+
loader does): that is the supported contract. `nodenext` and `node16` are out
|
|
111
|
+
of contract — they may work today, and are not tested.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Wiring
|
|
116
|
+
|
|
117
|
+
### `i18n: options must be an object, as { locales: ['en', 'fr'] }`
|
|
118
|
+
|
|
119
|
+
**When:** loading `maizzle.config.ts`, when `i18n` is called with nothing,
|
|
120
|
+
`null`, or a string such as `i18n('en')`.
|
|
121
|
+
**Why:** `i18n` takes one object of options; `locales` is the one it
|
|
122
|
+
requires.
|
|
123
|
+
**Fix:**
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// maizzle.config.ts
|
|
127
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
128
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
129
|
+
|
|
130
|
+
export default defineMailConfig({
|
|
131
|
+
plugins: [i18n({ locales: ['en', 'fr'] })], // not i18n('en')
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `i18n: locales must hold at least one locale, as ['en', 'fr']`
|
|
136
|
+
|
|
137
|
+
**When:** loading `maizzle.config.ts`, when `locales` is missing, empty, or a
|
|
138
|
+
single string — or the locales passed alone, `i18n(['en', 'fr'])`: a list is
|
|
139
|
+
an object, and it has no `locales`.
|
|
140
|
+
**Why:** the plugin builds each template once per locale; with none there is
|
|
141
|
+
nothing to build.
|
|
142
|
+
**Fix:**
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
i18n({ locales: ['en'] }); // not locales: 'en', not i18n(['en'])
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `i18n: locales holds something that is not a locale — write each as a BCP 47 tag, as en or pt-BR`
|
|
149
|
+
|
|
150
|
+
**When:** loading `maizzle.config.ts`, for a locale such as `'EN'`, `'pt_BR'`,
|
|
151
|
+
`'french'` or `''`.
|
|
152
|
+
**Why:** a locale names a catalogue (`locales/pt-BR.json`) and an output
|
|
153
|
+
folder, and is handed to `Intl` to format numbers and dates. It is a BCP 47
|
|
154
|
+
tag: a lowercase language of two or three letters, then `-` and subtags.
|
|
155
|
+
**Fix:**
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
i18n({ locales: ['en', 'pt-BR'] }); // not 'EN', not 'pt_BR'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `i18n: locales holds the same locale twice`
|
|
162
|
+
|
|
163
|
+
**When:** loading `maizzle.config.ts`, when a locale is listed twice —
|
|
164
|
+
often after joining two lists.
|
|
165
|
+
**Why:** each locale is built once, into its own files; a duplicate would
|
|
166
|
+
write the same files twice.
|
|
167
|
+
**Fix:**
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
i18n({ locales: [...new Set([...ours, ...shared])] });
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `i18n: fallbackLocale must be one of locales`
|
|
174
|
+
|
|
175
|
+
**When:** loading `maizzle.config.ts`, when `fallbackLocale` is set to a
|
|
176
|
+
locale `locales` does not list.
|
|
177
|
+
**Why:** every other catalogue is checked against the fallback locale's; it
|
|
178
|
+
must have a catalogue itself.
|
|
179
|
+
**Fix:**
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
i18n({ locales: ['en', 'fr'], fallbackLocale: 'en' });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Leave `fallbackLocale` out to use the first locale of `locales`.
|
|
186
|
+
|
|
187
|
+
### `i18n: dir must be a folder of the project`
|
|
188
|
+
|
|
189
|
+
The option named is `dir` or `emails`.
|
|
190
|
+
|
|
191
|
+
**When:** loading `maizzle.config.ts`, when `dir` (the catalogues' folder) or
|
|
192
|
+
`emails` (the templates' folder) is not a string, or is empty.
|
|
193
|
+
**Why:** both are paths from the project's root, where `maizzle` runs.
|
|
194
|
+
**Fix:**
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
i18n({ locales: ['en', 'fr'], dir: 'i18n', emails: 'templates' });
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Leave them out for the defaults, `locales` and `emails`.
|
|
201
|
+
|
|
202
|
+
### `i18n: layout must be 'nested' or 'flat'`
|
|
203
|
+
|
|
204
|
+
**When:** loading `maizzle.config.ts`, for any other `layout`.
|
|
205
|
+
**Why:** `nested` writes `dist/en/verify-email.html`; `flat` writes
|
|
206
|
+
`dist/verify-email.en.html`. There is no third way.
|
|
207
|
+
**Fix:**
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
i18n({ locales: ['en', 'fr'], layout: 'flat' });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `i18n: catalogues must be a list of catalogues by locale, as [{ en: {...}, fr: {...} }]`
|
|
214
|
+
|
|
215
|
+
**When:** loading `maizzle.config.ts`, when `catalogues` is one set of
|
|
216
|
+
catalogues rather than a list of them — `catalogues: uiCatalogues` — or the
|
|
217
|
+
list holds something else than an object of catalogues keyed by locale:
|
|
218
|
+
`null`, or a locale whose catalogue is a string (`[{ en: 'Hello' }]`).
|
|
219
|
+
**Why:** `catalogues` is a list, so that several packages can each ship
|
|
220
|
+
their messages. Each entry is keyed by locale, like the project's
|
|
221
|
+
`locales/<locale>.json`; each is merged key by key under the next, and the
|
|
222
|
+
project's own catalogues over all of them.
|
|
223
|
+
**Fix:** wrap it in a list, even when there is one:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// maizzle.config.ts
|
|
227
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
228
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
229
|
+
import { ui, uiCatalogues } from '@nxgt/mail-ui';
|
|
230
|
+
|
|
231
|
+
export default defineMailConfig({
|
|
232
|
+
plugins: [
|
|
233
|
+
ui({ brand: { name: 'Acme' } }),
|
|
234
|
+
i18n({ locales: ['en', 'fr'], catalogues: [uiCatalogues] }), // not catalogues: uiCatalogues
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `i18n: templates must be a list of template folders, as [{ dir: '/abs/path/emails' }] — emails, when given, names at least one, each once`
|
|
240
|
+
|
|
241
|
+
**When:** loading `maizzle.config.ts`, when `templates` is one folder rather
|
|
242
|
+
than a list of them, as in `templates: mails.templates`. It also appears when
|
|
243
|
+
an entry of the list is not `{ dir, emails? }` with `dir` an absolute path
|
|
244
|
+
and `emails` a list of at least one name, each once. Examples:
|
|
245
|
+
`[{ dir: 'presets' }]`, `[{ dir, emails: 'welcome' }]`, `[{ dir, emails: [] }]`,
|
|
246
|
+
`[{ dir, emails: ['welcome', 'welcome'] }]`, or the folder alone as a string.
|
|
247
|
+
**Why:** `templates` is a list, so that several packages can each ship
|
|
248
|
+
templates, and each `dir` is absolute because a package answers where it is
|
|
249
|
+
installed, not a path from your project. An empty `emails` would build
|
|
250
|
+
nothing from the folder; leave it out to build every e-mail of it. The
|
|
251
|
+
project's `emails/` comes first, so a template of the same name there
|
|
252
|
+
replaces a package's.
|
|
253
|
+
**Fix:** wrap it in a list, even when there is one:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// maizzle.config.ts
|
|
257
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
258
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
259
|
+
import { presets } from '@nxgt/mail-presets';
|
|
260
|
+
import { ui, uiCatalogues } from '@nxgt/mail-ui';
|
|
261
|
+
|
|
262
|
+
const mails = presets();
|
|
263
|
+
|
|
264
|
+
export default defineMailConfig({
|
|
265
|
+
plugins: [
|
|
266
|
+
ui({ brand: { name: 'Acme' } }),
|
|
267
|
+
i18n({
|
|
268
|
+
locales: ['en', 'fr'],
|
|
269
|
+
catalogues: [uiCatalogues, mails.catalogues],
|
|
270
|
+
templates: [mails.templates], // not templates: mails.templates
|
|
271
|
+
}),
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### `i18n: templates[0] has no template sign-in.vue — name one of its e-mails`
|
|
277
|
+
|
|
278
|
+
**When:** loading `maizzle.config.ts`, so `maizzle build` stops before any
|
|
279
|
+
template is built. It happens when an entry of `templates` lists in `emails`
|
|
280
|
+
a name its folder has no `.vue` file for. Common causes are a typo, a name
|
|
281
|
+
with `.vue`, or an e-mail the package renamed or does not ship. The message
|
|
282
|
+
names the entry by its place in `templates`.
|
|
283
|
+
**Why:** `emails` keeps the templates it names and no other, so a name with
|
|
284
|
+
no template would silently build nothing.
|
|
285
|
+
**Fix:** name a template of that folder, without `.vue`. For
|
|
286
|
+
`@nxgt/mail-presets`, let `presets({ only })` write the entry. It checks the
|
|
287
|
+
names when it is called, and TypeScript checks them too:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { presets } from '@nxgt/mail-presets';
|
|
291
|
+
|
|
292
|
+
const mails = presets({ only: ['verify-email', 'sign-in-code'] });
|
|
293
|
+
|
|
294
|
+
i18n({
|
|
295
|
+
locales: ['en', 'fr'],
|
|
296
|
+
catalogues: [uiCatalogues, mails.catalogues],
|
|
297
|
+
templates: [mails.templates], // not [{ dir: TEMPLATES_DIR, emails: ['sign-in'] }]
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### `i18n: templates[0] holds no template — is … the folder of a package's e-mails?`
|
|
302
|
+
|
|
303
|
+
The message names the folder, as an absolute path, where `…` is here.
|
|
304
|
+
|
|
305
|
+
**When:** loading `maizzle.config.ts`, so `maizzle build` stops before any
|
|
306
|
+
template is built. It happens when an entry of `templates` points at a folder
|
|
307
|
+
that does not exist, or that holds no `.vue` file. Common causes are a `dir`
|
|
308
|
+
written by hand that names the package's root rather than its `emails/`
|
|
309
|
+
folder, a typo in the path, or a package installed without its templates.
|
|
310
|
+
**Why:** an entry of `templates` is a package's folder of e-mails. One that
|
|
311
|
+
holds none would build nothing from it, and the build would pass.
|
|
312
|
+
**Fix:** take the entry from the package rather than writing its path. For
|
|
313
|
+
`@nxgt/mail-presets`, `presets().templates` is the folder its templates ship
|
|
314
|
+
in:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { presets } from '@nxgt/mail-presets';
|
|
318
|
+
|
|
319
|
+
const mails = presets();
|
|
320
|
+
|
|
321
|
+
i18n({
|
|
322
|
+
locales: ['en', 'fr'],
|
|
323
|
+
catalogues: [uiCatalogues, mails.catalogues],
|
|
324
|
+
templates: [mails.templates], // not [{ dir: '/…/node_modules/@nxgt/mail-presets' }]
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
When the entry comes from the package and the error remains, reinstall it:
|
|
329
|
+
its templates are missing from `node_modules`.
|
|
330
|
+
|
|
331
|
+
### `i18n: templates[0] and templates[1] both have welcome.vue — keep one with emails: [...], or write the project's own in its folder`
|
|
332
|
+
|
|
333
|
+
**When:** loading `maizzle.config.ts`, so `maizzle build` stops before any
|
|
334
|
+
template is built. It happens when two entries of `templates` each ship an
|
|
335
|
+
e-mail of the same name.
|
|
336
|
+
**Why:** each e-mail is built once, under its name. Between two packages,
|
|
337
|
+
neither is the obvious one, so the build refuses to pick. The project's own
|
|
338
|
+
`emails/` is not concerned: a template there replaces a package's of the same
|
|
339
|
+
name, on purpose.
|
|
340
|
+
**Fix:** keep the e-mail from one package only, with `emails` on the other,
|
|
341
|
+
or write the project's own `emails/welcome.vue`, which replaces both:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
i18n({
|
|
345
|
+
locales: ['en', 'fr'],
|
|
346
|
+
templates: [
|
|
347
|
+
{ dir: onboardingDir }, // its welcome.vue is the one built
|
|
348
|
+
{ dir: accountDir, emails: ['verify-email', 'reset-password'] }, // not its welcome.vue
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
With `@nxgt/mail-presets`, `presets({ only: [...] })` writes that entry, and
|
|
354
|
+
leaves out the presets not named.
|
|
355
|
+
|
|
356
|
+
### `i18n: rendererTypes must be the path of a .ts file, as generated/mail.ts, or false`
|
|
357
|
+
|
|
358
|
+
**When:** loading `maizzle.config.ts`, when `rendererTypes` is `true`, an
|
|
359
|
+
empty string, a number, or a path that does not end in `.ts`
|
|
360
|
+
(`'generated/mail.d.json'`, `'generated/'`).
|
|
361
|
+
**Why:** `rendererTypes` says where each build writes `MailEmails`, the type
|
|
362
|
+
`createMailRenderer<MailEmails>` takes: a TypeScript module the code that
|
|
363
|
+
sends imports, resolved against the folder `maizzle` runs in. It is written
|
|
364
|
+
by default; there is nothing to turn on.
|
|
365
|
+
**Fix:** leave it out for `generated/mail.ts`, give the path of a `.ts` file,
|
|
366
|
+
or `false` for no file:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
i18n({ locales: ['en', 'fr'] }); // generated/mail.ts
|
|
370
|
+
i18n({ locales: ['en', 'fr'], rendererTypes: 'src/generated/mail.ts' }); // elsewhere
|
|
371
|
+
i18n({ locales: ['en', 'fr'], rendererTypes: false }); // none
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
See [The manifest — the renderer's types](guide/manifest.md#the-renderers-types--generatedmailts).
|
|
375
|
+
|
|
376
|
+
### `createTranslator: catalogues must be an object of catalogues by locale, as { en, fr }`
|
|
377
|
+
|
|
378
|
+
**When:** calling `createTranslator`, with `null`, `undefined` or no argument
|
|
379
|
+
— typically a JSON import that answered nothing.
|
|
380
|
+
**Why:** the first argument is the catalogues, keyed by locale.
|
|
381
|
+
**Fix:**
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { createTranslator } from '@nxgt/mail-i18n';
|
|
385
|
+
import en from './locales/en.json';
|
|
386
|
+
import fr from './locales/fr.json';
|
|
387
|
+
|
|
388
|
+
const t = createTranslator({ en, fr }, 'en');
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### `createTranslator: getLanguage must be a locale or a function that answers one`
|
|
392
|
+
|
|
393
|
+
**When:** calling `createTranslator`, with a second argument that is neither a
|
|
394
|
+
string nor a function — often the result of calling the function instead of
|
|
395
|
+
passing it, when that result is `undefined`.
|
|
396
|
+
**Why:** the language is read at each call of `t`, from a locale or from a
|
|
397
|
+
function; it is not fixed from a value that may be missing.
|
|
398
|
+
**Fix:**
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { pickLocale } from '@nxgt/mail';
|
|
402
|
+
import { createTranslator } from '@nxgt/mail-i18n';
|
|
403
|
+
|
|
404
|
+
const t = createTranslator({ en, fr }, () =>
|
|
405
|
+
pickLocale(user.locale, ['en', 'fr'], 'en'),
|
|
406
|
+
);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Catalogues
|
|
412
|
+
|
|
413
|
+
These fail when `maizzle.config.ts` loads: `maizzle build` and `maizzle serve`
|
|
414
|
+
stop before any template is built. The locale in the message is the
|
|
415
|
+
catalogue's; fix that file.
|
|
416
|
+
|
|
417
|
+
### `i18n: locales/fr.json is missing — every locale has a catalogue`
|
|
418
|
+
|
|
419
|
+
**When:** loading `maizzle.config.ts`, when a locale of `locales` has no
|
|
420
|
+
`<dir>/<locale>.json`.
|
|
421
|
+
**Why:** every locale is built from its own catalogue; a missing one would
|
|
422
|
+
leave every key untranslated.
|
|
423
|
+
**Fix:** add the file, with every key of the fallback locale's catalogue —
|
|
424
|
+
or remove the locale from `locales`. A catalogue is named by its locale
|
|
425
|
+
exactly: `locales/pt-BR.json` for `pt-BR`.
|
|
426
|
+
|
|
427
|
+
### `i18n: locales/fr.json is not valid JSON`
|
|
428
|
+
|
|
429
|
+
**When:** loading `maizzle.config.ts`, for an empty file, a trailing comma, a
|
|
430
|
+
comment, or single quotes.
|
|
431
|
+
**Why:** a catalogue is read with `JSON.parse`; JSON5 and JSONC are not
|
|
432
|
+
accepted.
|
|
433
|
+
**Fix:** find the mistake with a JSON parser, which points at it:
|
|
434
|
+
|
|
435
|
+
```sh
|
|
436
|
+
bun -e "JSON.parse(await Bun.file('locales/fr.json').text())"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### `i18n: fr: the catalogue must be an object of messages`
|
|
440
|
+
|
|
441
|
+
**When:** loading `maizzle.config.ts`, when a catalogue's JSON is an array, a
|
|
442
|
+
string, or `null`.
|
|
443
|
+
**Why:** a catalogue is an object: its keys are the first segments of the
|
|
444
|
+
message keys.
|
|
445
|
+
**Fix:**
|
|
446
|
+
|
|
447
|
+
```json
|
|
448
|
+
{ "verifyEmail": { "title": "Confirmez votre adresse" } }
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### `i18n: en: verifyEmail.title must be a message (a string) or an object of messages`
|
|
452
|
+
|
|
453
|
+
**When:** loading `maizzle.config.ts`, for a value that is a number, a
|
|
454
|
+
boolean, `null` or an array.
|
|
455
|
+
**Why:** a leaf is an ICU message, always a string; a number goes in the
|
|
456
|
+
message as an argument, not as its value.
|
|
457
|
+
**Fix:**
|
|
458
|
+
|
|
459
|
+
```json
|
|
460
|
+
{ "verifyEmail": { "expires": "The link expires in {minutes, plural, one {# minute} other {# minutes}}." } }
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### `i18n: en: verify-email is not camelCase — every segment of a key is camelCase, and nested rather than dotted, as verifyEmail.title`
|
|
464
|
+
|
|
465
|
+
**When:** loading `maizzle.config.ts`, for a key in kebab-case or snake_case,
|
|
466
|
+
starting with a capital, or written with a dot (`"verifyEmail.title"` as one
|
|
467
|
+
key).
|
|
468
|
+
**Why:** a key is camelCase segments, one object per segment, as in
|
|
469
|
+
`@nxgt/i18n`. The subject of `emails/verify-email.vue` is looked up as
|
|
470
|
+
`verifyEmail.subject`, so the template's kebab-case name is not a key.
|
|
471
|
+
**Fix:**
|
|
472
|
+
|
|
473
|
+
```json
|
|
474
|
+
{ "verifyEmail": { "title": "Confirm your e-mail address" } }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
not `{ "verify-email": { … } }`, and not `{ "verifyEmail.title": "…" }`.
|
|
478
|
+
|
|
479
|
+
### `i18n: en: verifyEmail.greeting is not a valid ICU message (EXPECT_ARGUMENT_CLOSING_BRACE)`
|
|
480
|
+
|
|
481
|
+
The reason in brackets is the ICU parser's own code.
|
|
482
|
+
|
|
483
|
+
**When:** loading `maizzle.config.ts`, for a message the ICU parser refuses:
|
|
484
|
+
an unclosed `{`, a `plural` without an `other` case, a literal `{` that is not
|
|
485
|
+
quoted.
|
|
486
|
+
**Why:** every message is parsed at build time, so none fails later. Only
|
|
487
|
+
the parser's code is kept: the error names the key, not the message's text.
|
|
488
|
+
**Fix:** close the argument, give each `plural` and `select` an `other` case,
|
|
489
|
+
and quote a literal brace with apostrophes:
|
|
490
|
+
|
|
491
|
+
```json
|
|
492
|
+
{
|
|
493
|
+
"verifyEmail": {
|
|
494
|
+
"greeting": "Hello {name},",
|
|
495
|
+
"code": "Your code is '{'{code}'}'."
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### `i18n: en: verifyEmail.greeting uses {first_name}, which is not camelCase — an argument is a camelCase name, as {firstName}`
|
|
501
|
+
|
|
502
|
+
**When:** loading `maizzle.config.ts`, for an argument in snake_case,
|
|
503
|
+
kebab-case, or starting with a capital.
|
|
504
|
+
**Why:** an argument is also the name of a placeholder, and every name is
|
|
505
|
+
camelCase.
|
|
506
|
+
**Fix:**
|
|
507
|
+
|
|
508
|
+
```json
|
|
509
|
+
{ "verifyEmail": { "greeting": "Hello {firstName}," } }
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### `i18n: en: verifyEmail.expires uses {minutes} as number and as date`
|
|
513
|
+
|
|
514
|
+
**When:** loading `maizzle.config.ts`, for a message that uses one argument
|
|
515
|
+
as two kinds — `{minutes, number}` and `{minutes, date}`, or a `plural` and a
|
|
516
|
+
`date`.
|
|
517
|
+
**Why:** each argument has one kind, read from the message, and a template's
|
|
518
|
+
value is checked against it; one value cannot be both.
|
|
519
|
+
**Fix:** use two arguments:
|
|
520
|
+
|
|
521
|
+
```json
|
|
522
|
+
{ "verifyEmail": { "expires": "Expires in {minutes, number} minutes, at {at, time, short}." } }
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### `i18n: fr: verifyEmail.title is missing — en, the fallback locale, has it`
|
|
526
|
+
|
|
527
|
+
**When:** loading `maizzle.config.ts`, when a key of the fallback locale's
|
|
528
|
+
catalogue is not in another locale's.
|
|
529
|
+
**Why:** every locale is built from its own catalogue, with no fallback at
|
|
530
|
+
build time: a missing translation fails the build rather than writing the
|
|
531
|
+
fallback's text, or the key, into a French e-mail.
|
|
532
|
+
**Fix:** add the key to `locales/fr.json`. Keys are sorted in the check, so
|
|
533
|
+
the first missing key is reported; there may be others.
|
|
534
|
+
|
|
535
|
+
### `i18n: fr: verifyEmail.titel is not a key of en, the fallback locale`
|
|
536
|
+
|
|
537
|
+
**When:** loading `maizzle.config.ts`, when a locale's catalogue has a key the
|
|
538
|
+
fallback locale's does not — usually a typo, or a key renamed in one file
|
|
539
|
+
only.
|
|
540
|
+
**Why:** the fallback locale's catalogue is the reference; a key only a
|
|
541
|
+
translation has cannot be used by a template.
|
|
542
|
+
**Fix:** rename the key to match, or add it to the fallback locale's
|
|
543
|
+
catalogue first.
|
|
544
|
+
|
|
545
|
+
### `i18n: fr: verifyEmail.title uses {name}, which en does not declare`
|
|
546
|
+
|
|
547
|
+
**When:** loading `maizzle.config.ts`, when a translation uses an argument the
|
|
548
|
+
fallback locale's message does not.
|
|
549
|
+
**Why:** the fallback locale's message declares the arguments a template
|
|
550
|
+
passes; one only the translation uses would never be given a value. A
|
|
551
|
+
translation may leave an argument out.
|
|
552
|
+
**Fix:** add the argument to the fallback locale's message, or remove it from
|
|
553
|
+
the translation.
|
|
554
|
+
|
|
555
|
+
### `i18n: fr: verifyEmail.expires uses {minutes} as date, and en declares it as number`
|
|
556
|
+
|
|
557
|
+
**When:** loading `maizzle.config.ts`, when a translation uses an argument as
|
|
558
|
+
another kind than the fallback locale's message does.
|
|
559
|
+
**Why:** a template passes one value for every locale; it cannot be a number
|
|
560
|
+
in one and a date in the other.
|
|
561
|
+
**Fix:** use the same kind in both:
|
|
562
|
+
|
|
563
|
+
```json
|
|
564
|
+
{ "verifyEmail": { "expires": "Le lien expire dans {minutes, plural, one {# minute} other {# minutes}}." } }
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Templates
|
|
570
|
+
|
|
571
|
+
These fail while `maizzle build` renders a template. Maizzle builds the
|
|
572
|
+
locales in no set order, so the locale in the message is either one — `en`
|
|
573
|
+
in one run, `fr` in the next — and it names the first build that failed, not
|
|
574
|
+
the only locale concerned.
|
|
575
|
+
|
|
576
|
+
### `[Vue warn]: Unhandled error during execution of render function`
|
|
577
|
+
|
|
578
|
+
**When:** `maizzle build` stops with this warning, a stack trace, and the
|
|
579
|
+
Node.js version as the last line.
|
|
580
|
+
**Why:** a template's `t()` or `placeholder()` threw while Vue rendered it.
|
|
581
|
+
Vue prints its own warning, then the file and line of the `throw` in this
|
|
582
|
+
package, then the error — the useful line is in the middle.
|
|
583
|
+
**Fix:** look for the line starting `Error: i18n:`:
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
[Vue warn]: Unhandled error during execution of render function
|
|
587
|
+
at <VerifyEmail >
|
|
588
|
+
…
|
|
589
|
+
Error: i18n: en: verify-email calls t('verifyEmail.titel'), which is not a key of the catalogues
|
|
590
|
+
at Proxy.<anonymous> (…)
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
and find that message on this page.
|
|
594
|
+
|
|
595
|
+
### `i18n: emails/Welcome.vue is not a kebab-case name — name a template as verify-email.vue`
|
|
596
|
+
|
|
597
|
+
**When:** loading `maizzle.config.ts`, so `maizzle build` stops before any
|
|
598
|
+
template is built. For a template or a folder in PascalCase, camelCase,
|
|
599
|
+
snake_case, or with a dot in its name. Under `maizzle serve`, a file so named
|
|
600
|
+
added while the server runs is printed with `console.error`, and the server
|
|
601
|
+
keeps running without it; the next `maizzle build` fails on it.
|
|
602
|
+
**Why:** the name becomes the output file (`dist/en/welcome.html`), the key
|
|
603
|
+
of its messages (`welcome.subject`) and its name in the manifest; a
|
|
604
|
+
kebab-case name gives all three.
|
|
605
|
+
**Fix:** rename it, and its folders, in kebab-case:
|
|
606
|
+
|
|
607
|
+
```
|
|
608
|
+
emails/welcome.vue
|
|
609
|
+
emails/auth/reset-password.vue → messages under auth.resetPassword
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### `i18n: emails/verify-email.vue is not built through the i18n plugin — leave content to it, and put templates in emails/`
|
|
613
|
+
|
|
614
|
+
**When:** `maizzle build`, on the first template, when the project sets
|
|
615
|
+
`content` itself.
|
|
616
|
+
**Why:** the plugin builds each template through one generated wrapper per
|
|
617
|
+
locale, under `.maizzle/i18n/`, and lists those in `content`. A `content`
|
|
618
|
+
set in the project **replaces** the plugin's — arrays are not joined — so
|
|
619
|
+
Maizzle builds the templates directly, with no locale.
|
|
620
|
+
**Fix:** leave `content` out, and put every template in the `emails` folder
|
|
621
|
+
(or the folder the `emails` option names):
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
export default defineMailConfig({
|
|
625
|
+
plugins: [i18n({ locales: ['en', 'fr'], emails: 'templates' })],
|
|
626
|
+
// no content: the plugin sets it
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### `i18n: en: verify-email calls t('verifyEmail.titel'), which is not a key of the catalogues`
|
|
631
|
+
|
|
632
|
+
**When:** `maizzle build`, rendering a template that calls `t()` with a key
|
|
633
|
+
no catalogue has — a typo, a key renamed in the catalogues, or a key that is
|
|
634
|
+
an object of messages (`t('verifyEmail')`).
|
|
635
|
+
**Why:** every locale has the same keys, so a key missing in one is missing
|
|
636
|
+
in all; the build fails rather than writing the key into the e-mail.
|
|
637
|
+
**Fix:** use a key of the catalogues, down to its message:
|
|
638
|
+
|
|
639
|
+
```vue
|
|
640
|
+
<Heading>{{ t('verifyEmail.title') }}</Heading>
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### `i18n: en: verify-email calls t('verifyEmail.expires') without {minutes}`
|
|
644
|
+
|
|
645
|
+
**When:** `maizzle build`, when a template calls `t()` without an argument
|
|
646
|
+
the fallback locale's message uses.
|
|
647
|
+
**Why:** a message is formatted at build time; an argument it uses must have
|
|
648
|
+
a value then. A value only known at send time is a placeholder.
|
|
649
|
+
**Fix:**
|
|
650
|
+
|
|
651
|
+
```vue
|
|
652
|
+
<Text>{{ t('verifyEmail.expires', { minutes: 15 }) }}</Text>
|
|
653
|
+
<Text>{{ t('verifyEmail.greeting', { name: placeholder('name') }) }}</Text>
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### `i18n: en: verify-email passes {minutes} to verifyEmail.expires as a string — the message uses it as a number`
|
|
657
|
+
|
|
658
|
+
The kinds are `string`, `number` and `date`, as the message uses the argument
|
|
659
|
+
and as the template passes it.
|
|
660
|
+
|
|
661
|
+
**When:** `maizzle build`, when a template passes a value of another kind
|
|
662
|
+
than the message uses: most often a `placeholder()` — always a string — to a
|
|
663
|
+
`plural`, a `number` or a `date`.
|
|
664
|
+
**Why:** a plural is chosen, and a number or a date is formatted, at build
|
|
665
|
+
time. A placeholder is only filled at send time, when there is no more ICU
|
|
666
|
+
to apply. A number is accepted for a string, and for a date (as a
|
|
667
|
+
timestamp).
|
|
668
|
+
**Fix:** pass the value itself when the build knows it; when only send time
|
|
669
|
+
knows it, write the message with a plain argument:
|
|
670
|
+
|
|
671
|
+
```vue
|
|
672
|
+
<Text>{{ t('verifyEmail.expires', { minutes: 15 }) }}</Text>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
```json
|
|
676
|
+
{ "verifyEmail": { "expiresAt": "The link expires at {time}." } }
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
```vue
|
|
680
|
+
<Text>{{ t('verifyEmail.expiresAt', { time: placeholder('time') }) }}</Text>
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### `i18n: en: verify-email passes {name} to verifyEmail.title, which does not use it`
|
|
684
|
+
|
|
685
|
+
**When:** `maizzle build`, when a template passes an argument the fallback
|
|
686
|
+
locale's message does not use.
|
|
687
|
+
**Why:** an argument no message uses is a value lost without a word — often
|
|
688
|
+
the sign that the message or the argument was renamed.
|
|
689
|
+
**Fix:** remove the argument, or add it to the message in every locale:
|
|
690
|
+
|
|
691
|
+
```vue
|
|
692
|
+
<Heading>{{ t('verifyEmail.title') }}</Heading>
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### `i18n: en: verify-email calls t('verifyEmail.title') with arguments that are not an object, as { name: placeholder('name') }`
|
|
696
|
+
|
|
697
|
+
**When:** `maizzle build`, when a template passes its arguments as something
|
|
698
|
+
other than an object — a placeholder or a value on its own, or `null`.
|
|
699
|
+
**Why:** a message's arguments are named; `t` takes them as `{ name: value }`.
|
|
700
|
+
**Fix:**
|
|
701
|
+
|
|
702
|
+
```vue
|
|
703
|
+
<Text>{{ t('verifyEmail.greeting', { name: placeholder('name') }) }}</Text>
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
not `t('verifyEmail.greeting', placeholder('name'))`.
|
|
707
|
+
|
|
708
|
+
### `i18n: en: verify-email passes a placeholder to {plan}, which verifyEmail.title chooses on with a select — a placeholder always chooses other`
|
|
709
|
+
|
|
710
|
+
**When:** `maizzle build`, when a template passes `placeholder()` to an
|
|
711
|
+
argument the message chooses on with a `select`:
|
|
712
|
+
`t('verifyEmail.title', { plan: placeholder('plan') })` for
|
|
713
|
+
`"{plan, select, pro {Pro} other {Free}}"`.
|
|
714
|
+
**Why:** a `select` chooses at build time, and a placeholder is the string
|
|
715
|
+
`{{ plan }}` then: it always matches `other`, and the `pro` branch would never
|
|
716
|
+
be sent.
|
|
717
|
+
**Fix:** choose at build time with a value the build knows, or make one
|
|
718
|
+
e-mail (or one message) per case, and choose which to send in your
|
|
719
|
+
application:
|
|
720
|
+
|
|
721
|
+
```vue
|
|
722
|
+
<Heading>{{ t('verifyEmail.title', { plan: 'pro' }) }}</Heading>
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### `i18n: en: verify-email calls placeholder() with a name that is not camelCase — as placeholder('firstName')`
|
|
726
|
+
|
|
727
|
+
**When:** `maizzle build`, for `placeholder()` called with no name, a name
|
|
728
|
+
with a space, in snake_case or kebab-case, or starting with a capital.
|
|
729
|
+
**Why:** the name is the variable the renderer fills at send time, and
|
|
730
|
+
`{{ first name }}` could not be read back from the built file.
|
|
731
|
+
**Fix:**
|
|
732
|
+
|
|
733
|
+
```vue
|
|
734
|
+
<Text>{{ t('verifyEmail.greeting', { name: placeholder('firstName') }) }}</Text>
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### `i18n: en: verifyEmail.sentOn could not be formatted`
|
|
738
|
+
|
|
739
|
+
**When:** `maizzle build`, when a message is given a value that passes the
|
|
740
|
+
checks and still cannot be formatted — typically an invalid `Date`
|
|
741
|
+
(`new Date('')`) for a `date` or `time` argument.
|
|
742
|
+
**Why:** the formatter refused the value; its own error is the `cause` of
|
|
743
|
+
this one, printed under it. The `cause` may quote the message's text, which
|
|
744
|
+
this error's own message never does.
|
|
745
|
+
**Fix:** pass a valid date or timestamp:
|
|
746
|
+
|
|
747
|
+
```vue
|
|
748
|
+
<Text>{{ t('verifyEmail.sentOn', { at: new Date(Date.UTC(2026, 0, 2)) }) }}</Text>
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## Manifest and subject
|
|
754
|
+
|
|
755
|
+
These fail after every template is built, while `dist/mail-manifest.json` is
|
|
756
|
+
written.
|
|
757
|
+
|
|
758
|
+
### `i18n: welcome has no subject — add welcome.subject to the catalogues`
|
|
759
|
+
|
|
760
|
+
**When:** `maizzle build`, after the templates, for a template whose key has
|
|
761
|
+
no `subject` — a new template, or one renamed.
|
|
762
|
+
**Why:** the subject is a message like any other, looked up from the
|
|
763
|
+
template's name: `emails/welcome.vue` reads `welcome.subject`,
|
|
764
|
+
`emails/auth/reset-password.vue` reads `auth.resetPassword.subject`. An
|
|
765
|
+
e-mail is not sent without one.
|
|
766
|
+
**Fix:** add it to every catalogue:
|
|
767
|
+
|
|
768
|
+
```json
|
|
769
|
+
{ "welcome": { "subject": "Welcome, {name}" } }
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### `i18n: en: verifyEmail.subject uses {minutes} as a number — a subject's arguments are placeholders, filled at send time as strings`
|
|
773
|
+
|
|
774
|
+
The kind is `number` or `date`.
|
|
775
|
+
|
|
776
|
+
**When:** `maizzle build`, after the templates, for a subject with a
|
|
777
|
+
`plural`, a `number` or a `date` argument.
|
|
778
|
+
**Why:** a subject is not rendered by a template, so its arguments get no
|
|
779
|
+
value at build time: each one becomes a placeholder, filled at send time
|
|
780
|
+
with a string. No ICU can be applied to it then.
|
|
781
|
+
**Fix:** use plain arguments in a subject:
|
|
782
|
+
|
|
783
|
+
```json
|
|
784
|
+
{ "verifyEmail": { "subject": "Confirm your e-mail address, {name}" } }
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### `i18n: en: welcome.subject chooses on {kind} with a select — a subject's arguments are placeholders, which always choose other`
|
|
788
|
+
|
|
789
|
+
**When:** `maizzle build`, after the templates, for a subject with a
|
|
790
|
+
`select`.
|
|
791
|
+
**Why:** each argument of a subject becomes a placeholder, `{{ kind }}`, and a
|
|
792
|
+
`select` given that string always chooses `other`: the other branches would
|
|
793
|
+
never be sent.
|
|
794
|
+
**Fix:** write one subject, with plain arguments. When the subjects really
|
|
795
|
+
differ, make one e-mail per case:
|
|
796
|
+
|
|
797
|
+
```json
|
|
798
|
+
{ "welcome": { "subject": "Welcome, {name}" } }
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### `i18n: fr/welcome.html is empty — a tag of its template resolved to no component; list the plugin that brings it, as ui()`
|
|
802
|
+
|
|
803
|
+
The message names the built file, in `en` or in `fr` from one run to the
|
|
804
|
+
next. It follows `[Vue warn]: Failed to resolve component: NxLayout`, one
|
|
805
|
+
such warning for each tag that did not resolve.
|
|
806
|
+
|
|
807
|
+
**When:** `maizzle build`, after the templates, when a template's outermost
|
|
808
|
+
tag is a component that nothing resolves. Most often `ui()` is missing from
|
|
809
|
+
the plugins while a template uses `<NxLayout>`. It also happens when a
|
|
810
|
+
package's template, under `node_modules`, uses a component that no plugin in
|
|
811
|
+
the list resolves for it.
|
|
812
|
+
**Why:** Vue renders a component it cannot resolve as nothing, and Maizzle
|
|
813
|
+
still writes the file, with its doctype alone. The e-mail would go out
|
|
814
|
+
blank, so the build stops.
|
|
815
|
+
**Fix:** list the plugin that brings the components. For the `Nx*`
|
|
816
|
+
components, it is `ui()`, which also resolves the tags of templates
|
|
817
|
+
installed from npm:
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
// maizzle.config.ts
|
|
821
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
822
|
+
import { i18n } from '@nxgt/mail-i18n';
|
|
823
|
+
import { ui, uiCatalogues } from '@nxgt/mail-ui';
|
|
824
|
+
|
|
825
|
+
export default defineMailConfig({
|
|
826
|
+
plugins: [
|
|
827
|
+
ui({ brand: { name: 'Acme' } }),
|
|
828
|
+
i18n({ locales: ['en', 'fr'], catalogues: [uiCatalogues] }),
|
|
829
|
+
],
|
|
830
|
+
});
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
A template that reads `brand` stops earlier, while it renders, on
|
|
834
|
+
`TypeError: Cannot read properties of undefined (reading 'name')`. The cause
|
|
835
|
+
and the fix are the same.
|
|
836
|
+
|
|
837
|
+
### `i18n: welcome was not built in fr`
|
|
838
|
+
|
|
839
|
+
**When:** `maizzle build`, after the templates, when Maizzle wrote no HTML for
|
|
840
|
+
an e-mail in one of `locales`.
|
|
841
|
+
**Why:** the manifest lists every e-mail in every locale, and the sending
|
|
842
|
+
side relies on that. The build's files for that e-mail and locale hold no
|
|
843
|
+
file with the HTML extension (`output.extension`, `html` by default): the
|
|
844
|
+
file was not written, or a template changed its own output settings.
|
|
845
|
+
**Fix:** leave `content` and the output settings to the plugin and the
|
|
846
|
+
project config, and set none in a template.
|
|
847
|
+
|
|
848
|
+
### `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`
|
|
849
|
+
|
|
850
|
+
The path is relative to the output folder.
|
|
851
|
+
|
|
852
|
+
**When:** `maizzle build`, after the templates, when a built file is outside
|
|
853
|
+
`output.path`.
|
|
854
|
+
**Why:** the plugin decides where each e-mail's files go, per locale, and the
|
|
855
|
+
manifest points at them inside the output folder. A `plaintext.destination`,
|
|
856
|
+
or an output path set in a template, writes elsewhere.
|
|
857
|
+
**Fix:** remove `plaintext.destination` from the config and any output path
|
|
858
|
+
from the templates. To build somewhere else, set the project's `output.path`:
|
|
859
|
+
|
|
860
|
+
```ts
|
|
861
|
+
export default defineMailConfig({
|
|
862
|
+
plugins: [i18n({ locales: ['en', 'fr'] })],
|
|
863
|
+
output: { path: 'build/mails' },
|
|
864
|
+
});
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### `i18n: custom/welcome.html is not where the i18n plugin puts an e-mail — set no output path in a template`
|
|
868
|
+
|
|
869
|
+
**When:** `maizzle build`, after the templates, when a file inside the output
|
|
870
|
+
folder is not at `<locale>/<email>.html` (or `<email>.<locale>.html` with
|
|
871
|
+
`layout: 'flat'`).
|
|
872
|
+
**Why:** the manifest reads the locale and the e-mail back from each file's
|
|
873
|
+
path. A template that sets its own output path breaks that.
|
|
874
|
+
**Fix:** remove the output path from the template, and use `layout` to choose
|
|
875
|
+
between the two layouts.
|
|
876
|
+
|
|
877
|
+
### `i18n: src/index.ts was not written by i18n() — point rendererTypes at a file of its own`
|
|
878
|
+
|
|
879
|
+
The path is `rendererTypes` as given.
|
|
880
|
+
|
|
881
|
+
**When:** `maizzle build`, after the manifest, when the file `rendererTypes`
|
|
882
|
+
names exists and does not start with the plugin's header line,
|
|
883
|
+
`// Generated by @nxgt/mail-i18n from the build`.
|
|
884
|
+
**Why:** each build rewrites that file whole. The plugin replaces only a file
|
|
885
|
+
it wrote, so a path that points at a hand-written module — `src/index.ts`, a
|
|
886
|
+
typo, a shared `types.ts` — keeps it rather than losing its code.
|
|
887
|
+
**Fix:** give `rendererTypes` a file of its own, and import `MailEmails` from
|
|
888
|
+
it:
|
|
889
|
+
|
|
890
|
+
```ts
|
|
891
|
+
i18n({ locales: ['en', 'fr'], rendererTypes: 'src/generated/mail.ts' });
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
---
|
|
895
|
+
|
|
896
|
+
## Run time
|
|
897
|
+
|
|
898
|
+
### `t: the language is not a locale of the catalogues — pick one with pickLocale`
|
|
899
|
+
|
|
900
|
+
**When:** calling `t`, when the language — the second argument of
|
|
901
|
+
`createTranslator`, or the third of `t` — is not a key of the catalogues:
|
|
902
|
+
a recipient's `'de'`, a region such as `'fr-CA'`, or `undefined`. The
|
|
903
|
+
language is left out of the message.
|
|
904
|
+
**Why:** `t` does not choose a locale; given one it has no catalogue for, it
|
|
905
|
+
throws rather than answering the key.
|
|
906
|
+
**Fix:** choose the locale with `pickLocale`, which answers one of those you
|
|
907
|
+
support:
|
|
908
|
+
|
|
909
|
+
```ts
|
|
910
|
+
import { pickLocale } from '@nxgt/mail';
|
|
911
|
+
import { createTranslator } from '@nxgt/mail-i18n';
|
|
912
|
+
import en from './locales/en.json';
|
|
913
|
+
import fr from './locales/fr.json';
|
|
914
|
+
|
|
915
|
+
const t = createTranslator({ en, fr }, () =>
|
|
916
|
+
pickLocale(user.locale, ['en', 'fr'], 'en'),
|
|
917
|
+
);
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### `t: fr: verifyEmail.titel is not a key`
|
|
921
|
+
|
|
922
|
+
**When:** calling `t` with a key the language's catalogue does not have — a
|
|
923
|
+
typo, or a key that is an object of messages (`t('verifyEmail')`).
|
|
924
|
+
**Why:** where `@nxgt/i18n` answers the key, this `t` throws: an e-mail is
|
|
925
|
+
not sent with a key in it.
|
|
926
|
+
**Fix:** use a key of the catalogues, down to its message:
|
|
927
|
+
|
|
928
|
+
```ts
|
|
929
|
+
t('verifyEmail.subject', { name: 'Ada' });
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
### `t: en: verifyEmail.expires could not be formatted`
|
|
933
|
+
|
|
934
|
+
**When:** calling `t`, when the message cannot be formatted with the
|
|
935
|
+
arguments given — most often an argument left out, or an invalid `Date` for a
|
|
936
|
+
`date`.
|
|
937
|
+
**Why:** the formatter refused; its own error, which names the argument and
|
|
938
|
+
may quote the message's text, is the `cause` of this one. `createTranslator` does not check arguments as the
|
|
939
|
+
build does.
|
|
940
|
+
**Fix:** pass every argument the message uses:
|
|
941
|
+
|
|
942
|
+
```ts
|
|
943
|
+
t('verifyEmail.expires', { minutes: 15 });
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
## Traps: a build that succeeds and is wrong
|
|
949
|
+
|
|
950
|
+
### `[Vue warn]: Property "name" was accessed during render but is not defined on instance.`
|
|
951
|
+
|
|
952
|
+
**When:** `maizzle build` succeeds, prints this warning once per locale, and
|
|
953
|
+
the built e-mail has nothing where `{{ name }}` was written in the template.
|
|
954
|
+
**Why:** a template is Vue: `{{ name }}` in its markup is an expression,
|
|
955
|
+
evaluated at build time, and `name` has no value then. Nothing is left for
|
|
956
|
+
the renderer to fill. When `name` does exist — a prop, a global property — it
|
|
957
|
+
is written in, silently.
|
|
958
|
+
**Fix:** write the placeholder with `placeholder()`, which puts `{{ name }}`
|
|
959
|
+
in the built file:
|
|
960
|
+
|
|
961
|
+
```vue
|
|
962
|
+
<Text>{{ placeholder('name') }}</Text>
|
|
963
|
+
<Text>{{ t('verifyEmail.greeting', { name: placeholder('name') }) }}</Text>
|
|
964
|
+
<Button :href="placeholder('link')">{{ t('verifyEmail.action') }}</Button>
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### A link's placeholder is prefixed with a domain
|
|
968
|
+
|
|
969
|
+
**When:** `maizzle build` succeeds, and a link written as
|
|
970
|
+
`:href="placeholder('link')"` comes out as
|
|
971
|
+
`href="https://example.com/{{ link }}"`.
|
|
972
|
+
**Why:** Maizzle's `url.base` prefixes every relative URL, and a placeholder
|
|
973
|
+
looks like one. At send time the link is then the domain followed by the
|
|
974
|
+
whole URL filled in.
|
|
975
|
+
**Fix:** leave `url.base` off, and write the absolute URL of an image or a
|
|
976
|
+
static link in the template:
|
|
977
|
+
|
|
978
|
+
```ts
|
|
979
|
+
export default defineMailConfig({
|
|
980
|
+
plugins: [i18n({ locales: ['en', 'fr'] })],
|
|
981
|
+
// no url.base
|
|
982
|
+
});
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### `.maizzle/` shows up in `git status`
|
|
986
|
+
|
|
987
|
+
**When:** after the first `maizzle build` or `maizzle serve`, `git status`
|
|
988
|
+
lists `.maizzle/i18n/en/verify-email.vue` and one file per template and
|
|
989
|
+
locale.
|
|
990
|
+
**Why:** those are the wrappers the plugin generates, one per template and
|
|
991
|
+
locale, so that one build writes every locale. They are rewritten on every
|
|
992
|
+
build, and removed when their template is.
|
|
993
|
+
**Fix:** ignore the folder:
|
|
994
|
+
|
|
995
|
+
```gitignore
|
|
996
|
+
# .gitignore
|
|
997
|
+
.maizzle/
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
Never edit a wrapper: the change is lost on the next build.
|
|
1001
|
+
|
|
1002
|
+
### `No templates found`, or old templates, when Maizzle is built from a worker thread
|
|
1003
|
+
|
|
1004
|
+
**When:** Maizzle's `build()` is called from code that runs in a worker
|
|
1005
|
+
thread, such as a job runner or a Vitest `threads` pool. The build prints
|
|
1006
|
+
`No templates found`, or builds templates that were since renamed or
|
|
1007
|
+
removed, without the ones added.
|
|
1008
|
+
**Why:** the plugin writes the wrappers under `.maizzle/i18n/` only on the
|
|
1009
|
+
main thread, so that the workers of a parallel build never write the same
|
|
1010
|
+
file twice. From a worker thread it writes none, and the build finds whatever
|
|
1011
|
+
wrappers are already there, or none at all.
|
|
1012
|
+
**Fix:** run the build on the main thread. Spawn the command:
|
|
1013
|
+
|
|
1014
|
+
```ts
|
|
1015
|
+
const child = Bun.spawn(['bunx', 'maizzle', 'build'], { cwd: 'mails', stdout: 'inherit', stderr: 'inherit' });
|
|
1016
|
+
if ((await child.exited) !== 0) throw new Error('maizzle build failed');
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
or, under Vitest, run those tests in processes rather than threads:
|
|
1020
|
+
|
|
1021
|
+
```ts
|
|
1022
|
+
// vitest.config.ts
|
|
1023
|
+
export default { test: { pool: 'forks' } };
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### The editor says `Property 't' does not exist` in a template, or completes no key
|
|
1027
|
+
|
|
1028
|
+
The same for `placeholder` or `locale`: `Property 'placeholder' does not exist
|
|
1029
|
+
on type 'ComponentPublicInstance<…>'`. The build is not affected.
|
|
1030
|
+
|
|
1031
|
+
**When:** editing a template, in an editor with Vue's language tools, or in
|
|
1032
|
+
`vue-tsc`.
|
|
1033
|
+
**Why:** the types of `t` are in `.maizzle/nxgt-mail-i18n.d.ts`, which the
|
|
1034
|
+
plugin writes each time the config loads. Either it has not been written yet
|
|
1035
|
+
— a fresh clone, before any `maizzle prepare`, `serve` or `build` — or your
|
|
1036
|
+
`tsconfig.json` does not include `.maizzle/*.d.ts`. A project that sets
|
|
1037
|
+
Maizzle's `root`, or a Laravel project, has its `.maizzle/` elsewhere: include
|
|
1038
|
+
that one.
|
|
1039
|
+
**Fix:** keep the starter's include and write the file once:
|
|
1040
|
+
|
|
1041
|
+
```json
|
|
1042
|
+
{ "include": ["**/*.vue", ".maizzle/*.d.ts"] }
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
```sh
|
|
1046
|
+
bunx maizzle prepare
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
See [Editor and type checking](guide/editor.md).
|
|
1050
|
+
|
|
1051
|
+
### The editor flags a key you just added to a catalogue
|
|
1052
|
+
|
|
1053
|
+
`Argument of type '"welcome.footer"' is not assignable to parameter of type
|
|
1054
|
+
'keyof TemplateMessages'`, for a key that is in `locales/en.json`.
|
|
1055
|
+
|
|
1056
|
+
**When:** right after adding a key, before the config loads again.
|
|
1057
|
+
**Why:** the file lists the keys of the catalogues as they were when the
|
|
1058
|
+
config last loaded.
|
|
1059
|
+
**Fix:** save the catalogue under `maizzle serve`, which reloads the config,
|
|
1060
|
+
or run `bunx maizzle prepare`.
|
|
1061
|
+
|
|
1062
|
+
### Biome reports `parse` errors in a template as soon as you edit it
|
|
1063
|
+
|
|
1064
|
+
`Expected a property, a shorthand property, a getter, a setter, or a method but
|
|
1065
|
+
instead found '{ t('verifyEmail.title')'`, `type assertion are a TypeScript
|
|
1066
|
+
only feature`, or `This class property name should be in camelCase` on a
|
|
1067
|
+
component's tag — in the editor only; `biome check` reports nothing.
|
|
1068
|
+
|
|
1069
|
+
**When:** typing in a template that has no `<script>`, with Biome 2.5 as the
|
|
1070
|
+
editor's linter.
|
|
1071
|
+
**Why:** Biome's language server reads only the `<script>` of a `.vue` file
|
|
1072
|
+
when it opens it, but re-reads a file without one as JavaScript from the first
|
|
1073
|
+
change on. The template is fine; the editor's Biome is not reading it as Vue.
|
|
1074
|
+
**Fix:** let Biome parse Vue templates — add this key to your existing
|
|
1075
|
+
`biome.json` — then run **Biome: Restart** in the editor:
|
|
1076
|
+
|
|
1077
|
+
```json
|
|
1078
|
+
{ "html": { "experimentalFullSupportEnabled": true } }
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
Biome then lints the templates too. A rule that cannot see a slot's text,
|
|
1082
|
+
such as `useAnchorContent` on `<a><slot /></a>`, is silenced on its element:
|
|
1083
|
+
|
|
1084
|
+
```vue
|
|
1085
|
+
<!-- biome-ignore lint/a11y/useAnchorContent: the link's text is the slot. -->
|
|
1086
|
+
<a :href="href"><slot /></a>
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
### A bug in `@nxgt/mail-i18n` itself
|
|
1090
|
+
|
|
1091
|
+
A refusal of a catalogue or a template this page says is valid, a locale
|
|
1092
|
+
built with another locale's text, or a manifest that does not list what the
|
|
1093
|
+
build wrote, is a bug in this package. Open an issue on
|
|
1094
|
+
[`softistx/nxgt-mail`](https://github.com/softistx/nxgt-mail/issues) with the
|
|
1095
|
+
message, the package version, the Maizzle version, and the smallest
|
|
1096
|
+
catalogue and template that reproduce it — never a real address or link.
|