best-email-builder 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +11 -1
- package/dist/{chunk-2PMZMNKA.js → chunk-MTALS5GC.js} +98 -3
- package/dist/chunk-MTALS5GC.js.map +1 -0
- package/dist/index.js +977 -299
- package/dist/index.js.map +1 -1
- package/dist/server.js +13 -13
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +1 -1
- package/dist/types/adapters/grapesjs/unwrapModule.d.ts +18 -0
- package/dist/types/blocks/columns/index.d.ts +3 -0
- package/dist/types/blocks/heading/index.d.ts +3 -0
- package/dist/types/blocks/hero/index.d.ts +3 -0
- package/dist/types/blocks/navbar/index.d.ts +6 -0
- package/dist/types/blocks/social/index.d.ts +13 -0
- package/dist/types/core/EmailEditor.d.ts +12 -4
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/features/templates/TemplateGallery.d.ts +3 -0
- package/dist/types/features/templates/gallery.d.ts +41 -0
- package/dist/types/features/templates/index.d.ts +2 -0
- package/dist/types/features/templates/starter.d.ts +22 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/server/compileMjml.d.ts +3 -3
- package/dist/types/server/render.d.ts +4 -4
- package/dist/types/server/templates.d.ts +2 -2
- package/dist/types/ui/BlockSidebar.d.ts +2 -1
- package/dist/types/ui/Drawer.d.ts +19 -0
- package/dist/types/ui/EmailBuilder.d.ts +2 -0
- package/dist/types/ui/PropertiesPanel.d.ts +2 -1
- package/dist/types/ui/Toolbar.d.ts +9 -2
- package/dist/types/ui/primitives/button.d.ts +1 -1
- package/dist/types/ui/useLayoutWidth.d.ts +10 -0
- package/package.json +8 -7
- package/dist/chunk-2PMZMNKA.js.map +0 -1
- package/dist/chunk-3EOCXTI2.js +0 -652
- package/dist/chunk-3EOCXTI2.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ All notable changes are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] — 2026-10-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Blocks: **Heading**, **Social links** (Facebook, Instagram, LinkedIn, X,
|
|
11
|
+
YouTube, GitHub — each icon selectable, duplicable, its network and link
|
|
12
|
+
editable), **Menu** (a row of links, each label edited in the panel),
|
|
13
|
+
**4 columns**, **Image + text** and **Hero banner** (a section with a
|
|
14
|
+
background image under a title, a sentence and a button).
|
|
15
|
+
- Sections take a background image, with its size and position; the color
|
|
16
|
+
underneath stays for clients that block images.
|
|
17
|
+
- Text: a *Weight* property (normal / bold).
|
|
18
|
+
- **Template gallery**: a *Templates* button opens six ready-made emails
|
|
19
|
+
(blank, simple, newsletter, welcome, promotion, event invitation), shown as
|
|
20
|
+
rendered thumbnails. Picking one replaces the email (after confirmation if
|
|
21
|
+
it was modified) and fills the subject and preview text when empty.
|
|
22
|
+
`templates` prop to offer your own, `[]` to hide it; `TEMPLATE_GALLERY` and
|
|
23
|
+
`buildGallery` are exported.
|
|
24
|
+
- **Phones and tablets**: the layout follows the builder's own width. Below
|
|
25
|
+
1180 px the properties open in a drawer and the toolbar takes two rows (its
|
|
26
|
+
actions scroll sideways); below 760 px the blocks move to a drawer too and a
|
|
27
|
+
bar under the canvas offers *Add a block* and *Edit: <element>*. Adding a
|
|
28
|
+
block opens its properties; saving without a name opens the settings.
|
|
29
|
+
- `editor.exportHtml(mjml)` compiles the given MJML with the canvas compiler
|
|
30
|
+
(display only — what is stored and sent is still compiled by the server).
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **Breaking:** `compileMjml`, `compileTemplate` and `renderEmail` from
|
|
34
|
+
`best-email-builder/server` now return a Promise — MJML 5 compiles
|
|
35
|
+
asynchronously. Add `await` where you call them.
|
|
36
|
+
- MJML 5.4 on the server (htmlnano/cssnano instead of html-minifier, stricter
|
|
37
|
+
includes), GrapesJS 0.23.6 in the canvas, lucide-react 1.x, radix-ui 1.8.
|
|
38
|
+
Templates saved with GrapesJS 0.22 open unchanged.
|
|
39
|
+
|
|
40
|
+
## [0.1.1] — 2026-10-12
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- The builder did not start under webpack (Next.js): `grapesjs-mjml` arrived
|
|
44
|
+
wrapped twice by the bundler's CommonJS interop ("mjmlPlugin is not a
|
|
45
|
+
function"), and its French labels were lost the same way. Every interop
|
|
46
|
+
layer is now unwrapped, whatever the bundler.
|
|
47
|
+
- The published package no longer carries stale chunks from earlier builds:
|
|
48
|
+
`npm run build` starts from an empty `dist/`.
|
|
49
|
+
|
|
7
50
|
## [0.1.0] — 2026-10-10
|
|
8
51
|
|
|
9
52
|
First public release.
|
package/README.md
CHANGED
|
@@ -32,12 +32,18 @@ Best Email Builder is the second option done once, properly, and given away:
|
|
|
32
32
|
|
|
33
33
|
- **MJML under the hood** — every block compiles to battle-tested, table-based
|
|
34
34
|
email HTML. You never write a `<table>` by hand.
|
|
35
|
+
- **The blocks emails are made of**: heading, text, image, button, divider,
|
|
36
|
+
spacer, social links, menu, 1 to 4 columns, image + text, and a hero banner
|
|
37
|
+
with a background image — sections take a background image too.
|
|
35
38
|
- **Your UI, your storage, your rendering.** The builder is a React component
|
|
36
39
|
with its own interface; GrapesJS is only the internal canvas engine, hidden
|
|
37
40
|
behind an adapter. Templates are stored by *your* backend, as a versioned JSON
|
|
38
41
|
project (the source of truth) plus server-compiled HTML.
|
|
39
42
|
- **Server-side rendering only.** The browser never sends HTML: the server
|
|
40
43
|
validates the project and the MJML, compiles, and stores its own HTML.
|
|
44
|
+
- **A gallery of ready-made emails** — newsletter, welcome, promotion, event
|
|
45
|
+
invitation… each one with the sender's footer and the unsubscribe link, so
|
|
46
|
+
nobody starts from a blank page or forgets the legal mentions.
|
|
41
47
|
- **Production features**: undo/redo, desktop/mobile canvas, autosave and local
|
|
42
48
|
drafts, versions, conflict detection between concurrent editors, image
|
|
43
49
|
uploads, server preview with quality checks (missing alt text, missing
|
|
@@ -89,7 +95,10 @@ export default function TemplateEditor({ templateId, onClose }) {
|
|
|
89
95
|
}
|
|
90
96
|
```
|
|
91
97
|
|
|
92
|
-
The builder fills its parent: give the parent a height. It
|
|
98
|
+
The builder fills its parent: give the parent a height. It adapts to the
|
|
99
|
+
width it is given — not the window's: three columns from 1180 px, properties
|
|
100
|
+
in a drawer below that, and on a phone the canvas alone with *Add a block* /
|
|
101
|
+
*Edit* drawers. It is a client-only
|
|
93
102
|
component (the canvas needs the DOM). With Next.js, load it with
|
|
94
103
|
`dynamic(() => import(...), { ssr: false })` or render it from a
|
|
95
104
|
`'use client'` component.
|
|
@@ -170,6 +179,7 @@ With an email provider that does its own merge, map the tags instead:
|
|
|
170
179
|
| `variables` | `DEFAULT_VARIABLES` | Merge tags offered in the *Variables* menu. Pass the same list to the server. |
|
|
171
180
|
| `sender` | — | `{ name, address, logoUrl, logoWidth }` used by the starter template footer. |
|
|
172
181
|
| `starterMjml` | — | Replace the starter template entirely. |
|
|
182
|
+
| `templates` | `TEMPLATE_GALLERY` | Ready-made emails offered by the *Templates* button (blank, simple, newsletter, welcome, promotion, event invitation). Pass your own `{ id, name, description, create({ sender, t }), meta?(t) }` entries, or `[]` to hide the gallery. |
|
|
173
183
|
| `initialMjml`, `initialMeta` | — | Open a NEW template on given MJML and metadata (`{ name, subject, preheader, description }`) — e.g. a template converted from another format. Ignored with `templateId`. |
|
|
174
184
|
| `testRecipients` | `[]` | Addresses pre-filled in the *Send test* dialog. |
|
|
175
185
|
| `onClose` | — | Shows a *Back* button; called after confirming unsaved changes. |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/core/constants.js
|
|
2
|
-
var EMAIL_BUILDER_VERSION = "0.
|
|
2
|
+
var EMAIL_BUILDER_VERSION = "0.2.0";
|
|
3
3
|
var EMAIL_DOCUMENT_SCHEMA_VERSION = 1;
|
|
4
4
|
var EMAIL_DEVICES = ["desktop", "mobile"];
|
|
5
5
|
var EMAIL_MOBILE_WIDTH = 375;
|
|
@@ -77,6 +77,99 @@ function serializeDocument(document) {
|
|
|
77
77
|
|
|
78
78
|
// src/i18n/fr.js
|
|
79
79
|
var fr = {
|
|
80
|
+
"1. Complete your profile": "1. Compl\xE9tez votre profil",
|
|
81
|
+
"2. Explore": "2. Explorez",
|
|
82
|
+
"3. Ask us anything": "3. Posez-nous vos questions",
|
|
83
|
+
"30% off everything": "-30 % sur tout",
|
|
84
|
+
"30% off this weekend only": "-30 % ce week-end seulement",
|
|
85
|
+
"4 columns": "4 colonnes",
|
|
86
|
+
"4-column section": "Section en 4 colonnes",
|
|
87
|
+
"A new feature": "Une nouveaut\xE9",
|
|
88
|
+
"A sale with a discount code and two products": "Une vente avec un code promo et deux produits",
|
|
89
|
+
"A tip": "Une astuce",
|
|
90
|
+
"Add a block": "Ajouter un bloc",
|
|
91
|
+
"An empty page to build from scratch": "Une page vide pour partir de z\xE9ro",
|
|
92
|
+
"An evening of talks and drinks with the team and our customers.": "Une soir\xE9e de rencontres autour d'un verre avec l'\xE9quipe et nos clients.",
|
|
93
|
+
"An event": "Un \xE9v\xE9nement",
|
|
94
|
+
"Article {n}": "Article {n}",
|
|
95
|
+
"Background image": "Image de fond",
|
|
96
|
+
"Background image with a title and a button": "Image de fond avec un titre et un bouton",
|
|
97
|
+
"Blank": "Vierge",
|
|
98
|
+
"Book my seat": "Je r\xE9serve ma place",
|
|
99
|
+
"Buy": "Acheter",
|
|
100
|
+
"Date, place and a button to book a seat": "Date, lieu et un bouton pour r\xE9server",
|
|
101
|
+
"Direction": "Disposition",
|
|
102
|
+
"Edit: {element}": "Modifier : {element}",
|
|
103
|
+
"Email settings": "R\xE9glages de l'e-mail",
|
|
104
|
+
"Event invitation": "Invitation",
|
|
105
|
+
"Facebook": "Facebook",
|
|
106
|
+
"Fill the section": "Remplir la section",
|
|
107
|
+
"Get started": "Commencer",
|
|
108
|
+
"GitHub": "GitHub",
|
|
109
|
+
"Greets a new customer and lists the first steps": "Accueille un nouveau client et liste les premi\xE8res \xE9tapes",
|
|
110
|
+
"Heading": "Titre",
|
|
111
|
+
"Hero banner": "Banni\xE8re",
|
|
112
|
+
"Horizontal": "Horizontale",
|
|
113
|
+
"Icon background": "Fond de l'ic\xF4ne",
|
|
114
|
+
"Icon size": "Taille des ic\xF4nes",
|
|
115
|
+
"Icons linking to your profiles": "Ic\xF4nes vers vos profils",
|
|
116
|
+
"Image + text": "Image + texte",
|
|
117
|
+
"Image beside a title, text and button": "Image \xE0 c\xF4t\xE9 d'un titre, d'un texte et d'un bouton",
|
|
118
|
+
"Image position": "Position de l'image",
|
|
119
|
+
"Image size": "Taille de l'image",
|
|
120
|
+
"In brief": "En bref",
|
|
121
|
+
"Instagram": "Instagram",
|
|
122
|
+
"Large bold title": "Grand titre en gras",
|
|
123
|
+
"LinkedIn": "LinkedIn",
|
|
124
|
+
"Logo, headline, text and a button": "Logo, titre, texte et un bouton",
|
|
125
|
+
"Menu": "Menu",
|
|
126
|
+
"Menu link": "Lien du menu",
|
|
127
|
+
"Monthly news: banner, two articles, highlights": "Les nouvelles du mois : banni\xE8re, deux articles, br\xE8ves",
|
|
128
|
+
"Network": "R\xE9seau",
|
|
129
|
+
"Newsletter": "Newsletter",
|
|
130
|
+
"Offer valid until Sunday midnight, while stocks last.": "Offre valable jusqu'\xE0 dimanche minuit, dans la limite des stocks.",
|
|
131
|
+
"One sentence about it.": "Une phrase pour en parler.",
|
|
132
|
+
"Original size": "Taille d'origine",
|
|
133
|
+
"Our offices, 1 Main Street": "Nos bureaux, 1 rue Principale",
|
|
134
|
+
"Picking a template replaces the email being edited.": "Choisir un mod\xE8le remplace l'e-mail en cours de modification.",
|
|
135
|
+
"Product one": "Produit un",
|
|
136
|
+
"Product two": "Produit deux",
|
|
137
|
+
"Profile URL": "Adresse du profil",
|
|
138
|
+
"Promotion": "Promotion",
|
|
139
|
+
"Read more": "Lire la suite",
|
|
140
|
+
"Replace": "Remplacer",
|
|
141
|
+
"Replace the current email?": "Remplacer l'e-mail actuel ?",
|
|
142
|
+
"Reply to this email: a person reads it.": "R\xE9pondez \xE0 cet e-mail : une personne le lit.",
|
|
143
|
+
"Row of links to your site": "Rang\xE9e de liens vers votre site",
|
|
144
|
+
"Save the date \u2014 seats are limited.": "Notez la date \u2014 les places sont limit\xE9es.",
|
|
145
|
+
"Seats are limited: book yours now, it takes ten seconds.": "Les places sont limit\xE9es : r\xE9servez la v\xF4tre, cela prend dix secondes.",
|
|
146
|
+
"Shop the sale": "Voir les offres",
|
|
147
|
+
"Simple": "Simple",
|
|
148
|
+
"So we can tailor what we send you.": "Pour adapter ce que nous vous envoyons.",
|
|
149
|
+
"Social link": "R\xE9seau social",
|
|
150
|
+
"Social links": "R\xE9seaux sociaux",
|
|
151
|
+
"Space around each icon": "Espace autour de chaque ic\xF4ne",
|
|
152
|
+
"Start from a template": "Partir d'un mod\xE8le",
|
|
153
|
+
"Take a tour of what you can do.": "Faites le tour de ce que vous pouvez faire.",
|
|
154
|
+
"Templates": "Mod\xE8les",
|
|
155
|
+
"Thank you for joining us. Here is how to get the most out of your account.": "Merci de nous avoir rejoints. Voici comment tirer le meilleur de votre compte.",
|
|
156
|
+
"The news of the month": "Les nouvelles du mois",
|
|
157
|
+
"This weekend only, with the code SPRING30.": "Ce week-end seulement, avec le code SPRING30.",
|
|
158
|
+
"Three steps to get started.": "Trois \xE9tapes pour bien commencer.",
|
|
159
|
+
"Thursday, 7 pm": "Jeudi, 19 h",
|
|
160
|
+
"Two or three sentences that sum up the story and make people want the rest.": "Deux ou trois phrases qui r\xE9sument l'article et donnent envie de lire la suite.",
|
|
161
|
+
"Vertical": "Verticale",
|
|
162
|
+
"Welcome": "Bienvenue",
|
|
163
|
+
"Welcome aboard!": "Bienvenue \xE0 bord !",
|
|
164
|
+
"Welcome, {name}!": "Bienvenue, {name} !",
|
|
165
|
+
"What changed, what is coming, and two stories worth your time.": "Ce qui a chang\xE9, ce qui arrive, et deux articles qui valent le d\xE9tour.",
|
|
166
|
+
"When": "Quand",
|
|
167
|
+
"Where": "O\xF9",
|
|
168
|
+
"Whole image": "Image enti\xE8re",
|
|
169
|
+
"With the code SPRING30, until Sunday midnight.": "Avec le code SPRING30, jusqu'\xE0 dimanche minuit.",
|
|
170
|
+
"X": "X",
|
|
171
|
+
"You're invited": "On vous invite !",
|
|
172
|
+
"YouTube": "YouTube",
|
|
80
173
|
"{label} \u2014 {side}": "{label} \u2014 {side}",
|
|
81
174
|
"{max} test addresses at most": "{max} adresses de test au maximum",
|
|
82
175
|
"{message} Your version can be saved as a copy.": "{message} Votre version peut \xEAtre enregistr\xE9e comme copie.",
|
|
@@ -362,7 +455,9 @@ var fr = {
|
|
|
362
455
|
"Width": "Largeur",
|
|
363
456
|
"Your company": "Votre soci\xE9t\xE9",
|
|
364
457
|
"Your headline": "Titre du message",
|
|
365
|
-
"Your text.": "Votre texte."
|
|
458
|
+
"Your text.": "Votre texte.",
|
|
459
|
+
"\u201C{name}\u201D template applied.": "Mod\xE8le \xAB {name} \xBB appliqu\xE9.",
|
|
460
|
+
"\u20AC49 instead of \u20AC70": "49 \u20AC au lieu de 70 \u20AC"
|
|
366
461
|
};
|
|
367
462
|
|
|
368
463
|
// src/i18n/index.js
|
|
@@ -649,4 +744,4 @@ export {
|
|
|
649
744
|
unknownVariables,
|
|
650
745
|
sampleValues
|
|
651
746
|
};
|
|
652
|
-
//# sourceMappingURL=chunk-
|
|
747
|
+
//# sourceMappingURL=chunk-MTALS5GC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/constants.js","../src/core/document.js","../src/i18n/fr.js","../src/i18n/index.js","../src/features/variables/catalog.js","../src/renderer/links.js","../src/features/variables/service.js","../src/validation/images.js","../src/validation/urls.js"],"sourcesContent":["// @ts-check\n/**\n * Email builder constants.\n *\n * `EMAIL_BUILDER_VERSION` versions THIS package (UI, blocks, rendering);\n * `EMAIL_DOCUMENT_SCHEMA_VERSION` versions the stored JSON envelope (the\n * project JSON). Both are written with every template, so a document read by\n * a newer version knows whether it must be migrated (see `core/document.js`).\n */\n\nexport const EMAIL_BUILDER_VERSION = '0.2.0';\n\nexport const EMAIL_DOCUMENT_SCHEMA_VERSION = 1;\n\n/** @typedef {'desktop'|'mobile'} EmailDevice */\n\n/** @type {EmailDevice[]} */\nexport const EMAIL_DEVICES = ['desktop', 'mobile'];\n\n/** Content column width — the email client standard. */\nexport const EMAIL_CONTENT_WIDTH = 600;\n\n/** Mobile preview frame width (standard iPhone, the common worst case). */\nexport const EMAIL_MOBILE_WIDTH = 375;\n","// @ts-check\n/**\n * DOCUMENT envelope — what gets stored (the project JSON).\n *\n * HTML is only a compilation output; what gets edited is the engine's\n * project. It is not stored bare but wrapped with:\n *\n * schemaVersion the shape of THIS envelope (migrations, see MIGRATIONS);\n * engine the engine that can read `project` back;\n * engineVersion its version at write time;\n * builderVersion the builder version that wrote it;\n * project the engine data, opaque to the rest of the code.\n *\n * A document from another engine, a future schema or a malformed one is\n * REJECTED on read rather than half-loaded: a template silently emptied and\n * then saved again would be lost.\n *\n * Dependency-free module: testable outside the browser.\n */\n\nimport { EMAIL_BUILDER_VERSION, EMAIL_DOCUMENT_SCHEMA_VERSION } from './constants.js';\n\n/**\n * @typedef {{\n * schemaVersion: number,\n * engine: string,\n * engineVersion: string,\n * builderVersion: string,\n * project: Record<string, any>,\n * }} EmailDocument\n */\n\n/**\n * Envelope migrations, from N to N+1. Empty while only one schema exists; the\n * first entry will be `1: (doc) => ({ ...doc, schemaVersion: 2 })`.\n *\n * @type {Record<number, (document: any) => any>}\n */\nconst MIGRATIONS = {};\n\nexport class EmailDocumentError extends Error {\n /** @param {string} message */\n constructor(message) {\n super(message);\n this.name = 'EmailDocumentError';\n this.status = 422;\n }\n}\n\nfunction isPlainObject(value) {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * @param {{engine: string, engineVersion: string, project: object}} parts\n * @returns {EmailDocument}\n */\nexport function createDocument({ engine, engineVersion, project }) {\n if (!engine) throw new EmailDocumentError('Document: missing engine');\n if (!isPlainObject(project)) throw new EmailDocumentError('Document: invalid engine project');\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine,\n engineVersion: String(engineVersion || ''),\n builderVersion: EMAIL_BUILDER_VERSION,\n project: /** @type {Record<string, any>} */ (project),\n };\n}\n\n/**\n * Reads a document (object or JSON string), migrates it to the current schema\n * and validates it.\n *\n * @param {unknown} raw\n * @returns {EmailDocument}\n * @throws {EmailDocumentError}\n */\nexport function readDocument(raw) {\n let value = raw;\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value);\n } catch {\n throw new EmailDocumentError('Unreadable document: invalid JSON');\n }\n }\n if (!isPlainObject(value)) throw new EmailDocumentError('Unreadable document: object expected');\n\n /** @type {any} */\n let document = { ...(/** @type {object} */ (value)) };\n const version = Number(document.schemaVersion);\n if (!Number.isInteger(version) || version < 1) {\n throw new EmailDocumentError('Unreadable document: missing schema version');\n }\n if (version > EMAIL_DOCUMENT_SCHEMA_VERSION) {\n throw new EmailDocumentError(\n `Document written by a newer version of the builder (schema ${version}) — ` +\n 'reload the page'\n );\n }\n for (let v = version; v < EMAIL_DOCUMENT_SCHEMA_VERSION; v += 1) {\n const migrate = MIGRATIONS[v];\n if (!migrate) throw new EmailDocumentError(`Document: no migration from schema ${v}`);\n document = migrate(document);\n }\n\n if (typeof document.engine !== 'string' || !document.engine) {\n throw new EmailDocumentError('Unreadable document: missing engine');\n }\n if (!isPlainObject(document.project)) {\n throw new EmailDocumentError('Unreadable document: missing engine project');\n }\n\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine: document.engine,\n engineVersion: String(document.engineVersion || ''),\n builderVersion: String(document.builderVersion || ''),\n project: document.project,\n };\n}\n\n/** @param {EmailDocument} document */\nexport function serializeDocument(document) {\n return JSON.stringify(readDocument(document));\n}\n","// French locale. Keys are the English source strings (see src/i18n/index.js).\n// Missing keys: `node scripts/i18n-keys.mjs fr`.\n\n/** @type {Record<string, string>} */\nexport const fr = {\n \"1. Complete your profile\": \"1. Complétez votre profil\",\n \"2. Explore\": \"2. Explorez\",\n \"3. Ask us anything\": \"3. Posez-nous vos questions\",\n \"30% off everything\": \"-30 % sur tout\",\n \"30% off this weekend only\": \"-30 % ce week-end seulement\",\n \"4 columns\": \"4 colonnes\",\n \"4-column section\": \"Section en 4 colonnes\",\n \"A new feature\": \"Une nouveauté\",\n \"A sale with a discount code and two products\": \"Une vente avec un code promo et deux produits\",\n \"A tip\": \"Une astuce\",\n \"Add a block\": \"Ajouter un bloc\",\n \"An empty page to build from scratch\": \"Une page vide pour partir de zéro\",\n \"An evening of talks and drinks with the team and our customers.\": \"Une soirée de rencontres autour d'un verre avec l'équipe et nos clients.\",\n \"An event\": \"Un événement\",\n \"Article {n}\": \"Article {n}\",\n \"Background image\": \"Image de fond\",\n \"Background image with a title and a button\": \"Image de fond avec un titre et un bouton\",\n \"Blank\": \"Vierge\",\n \"Book my seat\": \"Je réserve ma place\",\n \"Buy\": \"Acheter\",\n \"Date, place and a button to book a seat\": \"Date, lieu et un bouton pour réserver\",\n \"Direction\": \"Disposition\",\n \"Edit: {element}\": \"Modifier : {element}\",\n \"Email settings\": \"Réglages de l'e-mail\",\n \"Event invitation\": \"Invitation\",\n \"Facebook\": \"Facebook\",\n \"Fill the section\": \"Remplir la section\",\n \"Get started\": \"Commencer\",\n \"GitHub\": \"GitHub\",\n \"Greets a new customer and lists the first steps\": \"Accueille un nouveau client et liste les premières étapes\",\n \"Heading\": \"Titre\",\n \"Hero banner\": \"Bannière\",\n \"Horizontal\": \"Horizontale\",\n \"Icon background\": \"Fond de l'icône\",\n \"Icon size\": \"Taille des icônes\",\n \"Icons linking to your profiles\": \"Icônes vers vos profils\",\n \"Image + text\": \"Image + texte\",\n \"Image beside a title, text and button\": \"Image à côté d'un titre, d'un texte et d'un bouton\",\n \"Image position\": \"Position de l'image\",\n \"Image size\": \"Taille de l'image\",\n \"In brief\": \"En bref\",\n \"Instagram\": \"Instagram\",\n \"Large bold title\": \"Grand titre en gras\",\n \"LinkedIn\": \"LinkedIn\",\n \"Logo, headline, text and a button\": \"Logo, titre, texte et un bouton\",\n \"Menu\": \"Menu\",\n \"Menu link\": \"Lien du menu\",\n \"Monthly news: banner, two articles, highlights\": \"Les nouvelles du mois : bannière, deux articles, brèves\",\n \"Network\": \"Réseau\",\n \"Newsletter\": \"Newsletter\",\n \"Offer valid until Sunday midnight, while stocks last.\": \"Offre valable jusqu'à dimanche minuit, dans la limite des stocks.\",\n \"One sentence about it.\": \"Une phrase pour en parler.\",\n \"Original size\": \"Taille d'origine\",\n \"Our offices, 1 Main Street\": \"Nos bureaux, 1 rue Principale\",\n \"Picking a template replaces the email being edited.\": \"Choisir un modèle remplace l'e-mail en cours de modification.\",\n \"Product one\": \"Produit un\",\n \"Product two\": \"Produit deux\",\n \"Profile URL\": \"Adresse du profil\",\n \"Promotion\": \"Promotion\",\n \"Read more\": \"Lire la suite\",\n \"Replace\": \"Remplacer\",\n \"Replace the current email?\": \"Remplacer l'e-mail actuel ?\",\n \"Reply to this email: a person reads it.\": \"Répondez à cet e-mail : une personne le lit.\",\n \"Row of links to your site\": \"Rangée de liens vers votre site\",\n \"Save the date — seats are limited.\": \"Notez la date — les places sont limitées.\",\n \"Seats are limited: book yours now, it takes ten seconds.\": \"Les places sont limitées : réservez la vôtre, cela prend dix secondes.\",\n \"Shop the sale\": \"Voir les offres\",\n \"Simple\": \"Simple\",\n \"So we can tailor what we send you.\": \"Pour adapter ce que nous vous envoyons.\",\n \"Social link\": \"Réseau social\",\n \"Social links\": \"Réseaux sociaux\",\n \"Space around each icon\": \"Espace autour de chaque icône\",\n \"Start from a template\": \"Partir d'un modèle\",\n \"Take a tour of what you can do.\": \"Faites le tour de ce que vous pouvez faire.\",\n \"Templates\": \"Modèles\",\n \"Thank you for joining us. Here is how to get the most out of your account.\": \"Merci de nous avoir rejoints. Voici comment tirer le meilleur de votre compte.\",\n \"The news of the month\": \"Les nouvelles du mois\",\n \"This weekend only, with the code SPRING30.\": \"Ce week-end seulement, avec le code SPRING30.\",\n \"Three steps to get started.\": \"Trois étapes pour bien commencer.\",\n \"Thursday, 7 pm\": \"Jeudi, 19 h\",\n \"Two or three sentences that sum up the story and make people want the rest.\": \"Deux ou trois phrases qui résument l'article et donnent envie de lire la suite.\",\n \"Vertical\": \"Verticale\",\n \"Welcome\": \"Bienvenue\",\n \"Welcome aboard!\": \"Bienvenue à bord !\",\n \"Welcome, {name}!\": \"Bienvenue, {name} !\",\n \"What changed, what is coming, and two stories worth your time.\": \"Ce qui a changé, ce qui arrive, et deux articles qui valent le détour.\",\n \"When\": \"Quand\",\n \"Where\": \"Où\",\n \"Whole image\": \"Image entière\",\n \"With the code SPRING30, until Sunday midnight.\": \"Avec le code SPRING30, jusqu'à dimanche minuit.\",\n \"X\": \"X\",\n \"You're invited\": \"On vous invite !\",\n \"YouTube\": \"YouTube\",\n \"{label} — {side}\": \"{label} — {side}\",\n \"{max} test addresses at most\": \"{max} adresses de test au maximum\",\n \"{message} Your version can be saved as a copy.\": \"{message} Votre version peut être enregistrée comme copie.\",\n \"{name} (copy)\": \"{name} (copie)\",\n \"{sent} sent, {failed} failed.\": \"{sent} parti(s), {failed} en échec.\",\n \"{sent} test(s) sent — check your inbox.\": \"{sent} test(s) parti(s) — ouvrez votre messagerie avant de continuer.\",\n \"1 column\": \"1 colonne\",\n \"2 columns\": \"2 colonnes\",\n \"2-column section\": \"Section en 2 colonnes\",\n \"25%\": \"25 %\",\n \"3 columns\": \"3 colonnes\",\n \"3-column section\": \"Section en 3 colonnes\",\n \"50%\": \"50 %\",\n \"600 px (standard)\": \"600 px (standard)\",\n \"640 px\": \"640 px\",\n \"75%\": \"75 %\",\n \"Address not allowed\": \"Adresse refusée\",\n \"Addresses\": \"Adresses\",\n \"Alignment\": \"Alignement\",\n \"Alt text\": \"Texte alternatif\",\n \"An https:// address is required\": \"Une adresse https:// est requise\",\n \"attribute {name} not allowed\": \"attribut {name} interdit\",\n \"Authentication required\": \"Authentification requise\",\n \"Autosave\": \"Sauvegarde automatique\",\n \"Autosave paused\": \"Sauvegarde automatique suspendue\",\n \"Autosaved at {time}\": \"Enregistré automatiquement à {time}\",\n \"Autosaving…\": \"Sauvegarde automatique…\",\n \"Available in every template.\": \"Disponible dans tous les modèles.\",\n \"Back\": \"Retour\",\n \"Background\": \"Fond\",\n \"base tag not allowed\": \"balise base interdite\",\n \"Blocks\": \"Blocs\",\n \"Bold\": \"Gras\",\n \"Bottom\": \"Bas\",\n \"Button\": \"Bouton\",\n \"Button background\": \"Fond du bouton\",\n \"Button padding\": \"Marges du bouton\",\n \"Button without link ({count})\": \"Bouton sans lien ({count})\",\n \"Call to action\": \"Appel à l’action\",\n \"Campaign\": \"Campagne\",\n \"Campaign name\": \"Nom de la campagne\",\n \"Cancel\": \"Annuler\",\n \"CDATA section not allowed\": \"section CDATA interdite\",\n \"Center\": \"Centre\",\n \"Close\": \"Fermer\",\n \"Colors\": \"Couleurs\",\n \"Column\": \"Colonne\",\n \"Company\": \"Société\",\n \"component script not allowed\": \"script de composant interdit\",\n \"conditional comment not allowed\": \"commentaire conditionnel interdit\",\n \"Confirm\": \"Confirmer\",\n \"Content\": \"Contenu\",\n \"Content width\": \"Largeur du contenu\",\n \"Copy saved: “{name}”.\": \"Copie enregistrée : « {name} ».\",\n \"Corner radius\": \"Arrondi\",\n \"Create\": \"Créer\",\n \"Create “{name}”\": \"Créer « {name} »\",\n \"current\": \"actuelle\",\n \"Custom\": \"Personnalisées\",\n \"Custom variables are not configured\": \"Les variables personnalisées ne sont pas configurées\",\n \"Dashed\": \"Tirets\",\n \"Default\": \"Par défaut\",\n \"Delete\": \"Supprimer\",\n \"Delete (Del)\": \"Supprimer (Suppr)\",\n \"Delete the variable “{name}”?\": \"Supprimer la variable « {name} » ?\",\n \"Describes the image if it is blocked\": \"Décrit l’image si elle est bloquée\",\n \"Deselect\": \"Désélectionner\",\n \"Desktop\": \"Bureau\",\n \"Device\": \"Appareil\",\n \"Dismiss\": \"Masquer\",\n \"Divider\": \"Séparateur\",\n \"Dotted\": \"Pointillés\",\n \"Double-click the text to edit it.\": \"Double-cliquez sur le texte pour le modifier.\",\n \"Duplicate\": \"Dupliquer\",\n \"Edit\": \"Modifier\",\n \"Edit “{name}”\": \"Modifier « {name} »\",\n \"Edit variable\": \"Modifier la variable\",\n \"Element actions\": \"Actions sur l’élément\",\n \"Email\": \"Courriel\",\n \"Email address\": \"Adresse e-mail\",\n \"Email preview\": \"Aperçu du courriel\",\n \"embed not allowed\": \"embed interdit\",\n \"Empty file\": \"Fichier vide\",\n \"Empty MJML\": \"MJML vide\",\n \"Empty MJML compilation\": \"Compilation MJML vide\",\n \"Empty subject\": \"Objet vide\",\n \"executable content not allowed\": \"contenu exécutable interdit\",\n \"external resource not allowed\": \"ressource externe interdite\",\n \"File missing\": \"Fichier manquant\",\n \"Final rendering, variables filled with samples.\": \"Rendu final, variables remplies d’exemples.\",\n \"First name\": \"Prénom\",\n \"Fit to text\": \"Ajustée au texte\",\n \"Font\": \"Police\",\n \"forbidden element in project: {tag}\": \"élément interdit dans le projet : {tag}\",\n \"form field not allowed\": \"champ de formulaire interdit\",\n \"form not allowed\": \"formulaire interdit\",\n \"Format rejected: PNG, JPEG or GIF expected\": \"Format refusé : PNG, JPEG ou GIF attendu\",\n \"frame not allowed\": \"frame interdite\",\n \"Full length\": \"Toute la largeur\",\n \"Full width\": \"Pleine largeur\",\n \"Full-width section\": \"Section pleine largeur\",\n \"Give the template a name before saving it.\": \"Donnez un libellé au modèle avant de l’enregistrer.\",\n \"Group\": \"Groupe\",\n \"Height\": \"Hauteur\",\n \"Hello {name},\": \"Bonjour {name},\",\n \"Hex code\": \"Code hexadécimal\",\n \"Horizontal rule\": \"Filet horizontal\",\n \"HTML button not allowed (use the Button block)\": \"bouton HTML interdit (utiliser le bloc Bouton)\",\n \"iframe not allowed\": \"iframe interdite\",\n \"Ignored (invalid): {email}\": \"Ignorée (invalide) : {email}\",\n \"Image\": \"Image\",\n \"image rejected ({name})\": \"image refusée ({name})\",\n \"image rejected ({name}): https only\": \"image refusée ({name}) : https seulement\",\n \"Image too large: {max} MB maximum\": \"Image trop lourde : {max} Mo au maximum\",\n \"Image without alt text ({count})\": \"Texte alternatif manquant ({count} image(s))\",\n \"Image without file ({count})\": \"Image sans fichier ({count})\",\n \"Inbox\": \"Boîte de réception\",\n \"Indicative preview — check it with “Send test”.\": \"Aperçu indicatif — validez avec « Envoyer un test ».\",\n \"inline style rejected\": \"style en ligne refusé\",\n \"Insecure http link ({count})\": \"Lien non sécurisé en http ({count})\",\n \"Insert a variable in the preview text\": \"Insérer une variable dans le texte d’aperçu\",\n \"Insert a variable in the subject\": \"Insérer une variable dans l’objet\",\n \"Internal error\": \"Erreur interne\",\n \"Invalid JSON\": \"JSON invalide\",\n \"Justify\": \"Justifié\",\n \"Key is required\": \"Balise requise\",\n \"Last name\": \"Nom\",\n \"Layout\": \"Mise en page\",\n \"Learn more\": \"Découvrir\",\n \"Leave\": \"Quitter\",\n \"Leave the builder?\": \"Quitter le constructeur ?\",\n \"Left\": \"Gauche\",\n \"Length\": \"Longueur\",\n \"Line color\": \"Couleur du filet\",\n \"Line height\": \"Interligne\",\n \"Line style\": \"Trait\",\n \"Link\": \"Lien\",\n \"Link on click\": \"Lien au clic\",\n \"Link URL\": \"Adresse du lien\",\n \"Links\": \"Liens\",\n \"Loading the builder\": \"Chargement du constructeur\",\n \"Loading…\": \"Chargement…\",\n \"Lowercase letters, digits, \\\"_\\\" and \\\".\\\" only, starting with a letter\": \"Minuscules, chiffres, « _ » et « . » seulement, en commençant par une lettre\",\n \"MathML not allowed\": \"MathML interdit\",\n \"Message is {size} KB: Gmail clips messages over {limit} KB\": \"Message de {size} Ko : Gmail le coupe au-delà de {limit} Ko\",\n \"meta tag not allowed\": \"balise meta interdite\",\n \"Middle\": \"Milieu\",\n \"missing <mj-body>\": \"<mj-body> manquant\",\n \"Missing unsubscribe link\": \"Lien de désinscription absent\",\n \"mj-font (external font) is outside the builder\": \"mj-font (police externe) est hors du constructeur\",\n \"mj-include would read a file from the server\": \"mj-include lirait un fichier du serveur\",\n \"mj-raw (free HTML) is not allowed\": \"mj-raw (HTML libre) est refusé\",\n \"mj-style (free CSS) is outside the builder\": \"mj-style (CSS libre) est hors du constructeur\",\n \"MJML too large ({size} KB, maximum {max} KB)\": \"MJML trop volumineux ({size} ko, maximum {max} ko)\",\n \"Mobile\": \"Mobile\",\n \"Move down\": \"Descendre\",\n \"Move left\": \"Déplacer à gauche\",\n \"Move right\": \"Déplacer à droite\",\n \"Move up\": \"Monter\",\n \"Multipart form expected\": \"Formulaire multipart attendu\",\n \"Name\": \"Libellé\",\n \"Name is required\": \"Libellé requis\",\n \"Name required\": \"Libellé requis\",\n \"New template\": \"Nouveau modèle\",\n \"New variable\": \"Nouvelle variable\",\n \"No changes\": \"Aucune modification\",\n \"No preview text\": \"Aucun texte d’aperçu\",\n \"No subject\": \"Sans objet\",\n \"No usable test address\": \"Aucune adresse de test exploitable\",\n \"No variable found.\": \"Aucune variable trouvée.\",\n \"No versions yet.\": \"Aucune version.\",\n \"Normal\": \"Normale\",\n \"Nothing to fix.\": \"Aucun point à corriger.\",\n \"Nothing to show yet.\": \"Rien à afficher pour l’instant.\",\n \"object not allowed\": \"object interdit\",\n \"Order number\": \"Numéro de commande\",\n \"Padding\": \"Marges\",\n \"Page background\": \"Fond de page\",\n \"Paragraph or heading\": \"Paragraphe ou titre\",\n \"Photo, visual or logo\": \"Photo, visuel ou logo\",\n \"Pick a colour\": \"Choisir une couleur\",\n \"Points to check\": \"Points à vérifier\",\n \"Preview\": \"Aperçu\",\n \"Preview and tests\": \"Aperçu et tests\",\n \"Preview text\": \"Texte d’aperçu\",\n \"Project rejected: {issues}\": \"Projet refusé : {issues}\",\n \"project too deep\": \"projet trop profond\",\n \"project too large ({size} KB, maximum {max} KB)\": \"projet trop volumineux ({size} ko, maximum {max} ko)\",\n \"Properties\": \"Propriétés\",\n \"Recipient\": \"Destinataire\",\n \"Redo\": \"Rétablir\",\n \"Redo (Ctrl+Y)\": \"Rétablir (Ctrl+Y)\",\n \"Refresh\": \"Rafraîchir\",\n \"Rendering…\": \"Rendu en cours…\",\n \"Request too large\": \"Requête trop volumineuse\",\n \"Restore\": \"Recharger\",\n \"Restore version {n}\": \"Recharger la version {n}\",\n \"Restore version {n}?\": \"Recharger la version {n} ?\",\n \"Results\": \"Résultats\",\n \"Resume\": \"Reprendre\",\n \"Resume the draft?\": \"Reprendre le brouillon ?\",\n \"Retry\": \"Réessayer\",\n \"Right\": \"Droite\",\n \"Route not found\": \"Route introuvable\",\n \"Sample value\": \"Valeur d’exemple\",\n \"Save\": \"Enregistrer\",\n \"Save a copy\": \"Enregistrer une copie\",\n \"Save the template first\": \"Enregistrez d’abord le modèle\",\n \"Saved\": \"Enregistré\",\n \"Saved — version {n}.\": \"Enregistré — version {n}.\",\n \"Saved at {time}\": \"Enregistré à {time}\",\n \"Saved, with {count} MJML warning(s): {message}\": \"Enregistré, avec {count} avertissement(s) MJML : {message}\",\n \"Saving…\": \"Enregistrement…\",\n \"script not allowed\": \"script interdit\",\n \"Search variables…\": \"Rechercher une variable…\",\n \"Section\": \"Section\",\n \"Select a text or a button first.\": \"Sélectionnez d’abord un texte ou un bouton.\",\n \"Send\": \"Envoyer\",\n \"Send a test\": \"Envoyer un test\",\n \"Shown after the subject in the inbox\": \"Affiché après l’objet dans la boîte de réception\",\n \"Size\": \"Taille\",\n \"Solid\": \"Plein\",\n \"Spacer\": \"Espace\",\n \"Spacing\": \"Espacement\",\n \"Start over\": \"Partir de zéro\",\n \"style sheet not allowed\": \"feuille de style interdite\",\n \"Subject\": \"Objet\",\n \"SVG not allowed\": \"SVG interdit\",\n \"Tag\": \"Balise\",\n \"tag not allowed: <{tag}>\": \"balise non admise : <{tag}>\",\n \"Template\": \"Modèle\",\n \"Template changed in the meantime\": \"Modèle modifié entre-temps\",\n \"Template changed in the meantime — reload it before saving\": \"Modèle modifié entre-temps — rechargez-le avant d’enregistrer\",\n \"Template changed in the meantime by {user} — reload it before saving\": \"Modèle modifié entre-temps par {user} — rechargez-le avant d’enregistrer\",\n \"Template not found\": \"Modèle introuvable\",\n \"Template rejected: {issues}\": \"Modèle refusé : {issues}\",\n \"Template rejected: {issues} (+{more})\": \"Modèle refusé : {issues} (+{more})\",\n \"Template saved.\": \"Modèle enregistré.\",\n \"Templates that use it will show its “value if empty”.\": \"Les modèles qui l’utilisent afficheront sa « valeur si vide ».\",\n \"Test\": \"Tester\",\n \"Test sending is not configured\": \"L’envoi de test n’est pas configuré\",\n \"Text\": \"Texte\",\n \"The builder could not load.\": \"Le constructeur n’a pas pu se charger.\",\n \"The builder could not start.\": \"Le constructeur n’a pas pu démarrer.\",\n \"the document must start with <mjml>\": \"le document doit commencer par <mjml>\",\n \"The draft will be offered again next time.\": \"Le brouillon reste proposé à la prochaine ouverture.\",\n \"The email on screen, to reviewers’ addresses.\": \"Le courriel à l’écran, aux adresses de relecture.\",\n \"The template could not be loaded.\": \"Le modèle n’a pas pu être chargé.\",\n \"Thickness\": \"Épaisseur\",\n \"This key is already used\": \"Cette balise existe déjà\",\n \"To check\": \"À vérifier\",\n \"Too many test sends — try again later\": \"Trop d’envois de test — réessayez plus tard\",\n \"Too many uploads — try again later\": \"Trop de dépôts d’images — réessayez plus tard\",\n \"Top\": \"Haut\",\n \"Typography\": \"Typographie\",\n \"Undo\": \"Annuler\",\n \"Undo (Ctrl+Z)\": \"Annuler (Ctrl+Z)\",\n \"Unknown variables (rendered empty): {tags}\": \"Variables inconnues (affichées vides) : {tags}\",\n \"Unsaved changes\": \"Modifications non enregistrées\",\n \"Unsaved changes will be lost.\": \"Les modifications non enregistrées seront perdues.\",\n \"Unsaved changes will be replaced.\": \"Les modifications non enregistrées seront remplacées.\",\n \"Unsaved template from {when}.\": \"Modèle non enregistré du {when}.\",\n \"Unsubscribe\": \"Se désinscrire\",\n \"Unsubscribe link\": \"Lien de désinscription\",\n \"Upload\": \"Déposer\",\n \"Upload failed\": \"Dépôt impossible\",\n \"URL rejected ({name})\": \"adresse refusée ({name})\",\n \"Value if empty\": \"Valeur si vide\",\n \"Variable “{name}” created and inserted.\": \"Variable « {name} » créée et insérée.\",\n \"Variable “{name}” created.\": \"Variable « {name} » créée.\",\n \"Variable “{name}” saved.\": \"Variable « {name} » enregistrée.\",\n \"Variable deleted.\": \"Variable supprimée.\",\n \"Variable not found\": \"Variable introuvable\",\n \"Variables\": \"Variables\",\n \"Version {n}\": \"Version {n}\",\n \"Version {n} restored — save to keep it.\": \"Version {n} rechargée — enregistrez pour la garder.\",\n \"Version not found\": \"Version introuvable\",\n \"Versions\": \"Versions\",\n \"Vertical alignment\": \"Alignement vertical\",\n \"Vertical space\": \"Vide vertical\",\n \"Weight\": \"Graisse\",\n \"When a recipient has none\": \"Quand un destinataire n’en a pas\",\n \"Width\": \"Largeur\",\n \"Your company\": \"Votre société\",\n \"Your headline\": \"Titre du message\",\n \"Your text.\": \"Votre texte.\",\n \"“{name}” template applied.\": \"Modèle « {name} » appliqué.\",\n \"€49 instead of €70\": \"49 € au lieu de 70 €\",\n};\n","// @ts-check\n/**\n * Translations.\n *\n * Source strings are written in English directly in the code and double as\n * translation keys (`t('Save')`). A locale is a plain object mapping those\n * English strings to translated ones; a missing entry falls back to English.\n * Placeholders use `{name}`: `t('Saved — version {n}.', { n: 3 })`.\n *\n * The same translator serves the browser (builder interface) and the server\n * (validation and API error messages).\n */\n\nimport { fr } from './fr.js';\n\n/** @typedef {Record<string, string>} Messages */\n/** @typedef {(text: string, params?: Record<string, string|number|null|undefined>) => string} Translate */\n\n/** Built-in locales. English is the source language: an empty dictionary. */\nexport const LOCALES = /** @type {Record<string, Messages>} */ ({ en: {}, fr });\n\n/**\n * @param {Messages} [messages]\n * @returns {Translate}\n */\nexport function createTranslator(messages = {}) {\n return (text, params) => {\n const template = Object.prototype.hasOwnProperty.call(messages, text) ? messages[text] : text;\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (match, key) =>\n params[key] === undefined || params[key] === null ? match : String(params[key])\n );\n };\n}\n\n/**\n * Translator for a locale code (`'fr'`, `'fr-FR'`, `'en'`…), optionally\n * extended with application-specific strings.\n *\n * @param {string} [locale]\n * @param {Messages} [overrides]\n * @returns {Translate}\n */\nexport function translatorFor(locale = 'en', overrides = {}) {\n const base = LOCALES[String(locale).toLowerCase()] || LOCALES[String(locale).slice(0, 2).toLowerCase()] || {};\n return createTranslator({ ...base, ...overrides });\n}\n\n/** English, untouched: the default everywhere a translator is optional. */\nexport const identity = createTranslator();\n","// @ts-check\n/**\n * Variables catalog — the merge tags a template can contain.\n *\n * The editor only inserts TAGS (`{{contact.firstname}}`); it never knows a\n * value. Rendering (server side) replaces them:\n *\n * preview, test → sample values (`sample`), the real address for a test;\n * sending → each recipient's values (`applyVariables`), or the merge\n * tags of an email provider (`mapVariables`).\n *\n * A missing value falls back to the tag's inline default\n * (`{{contact.firstname|there}}`), then to the variable's `fallback`, then to\n * an empty string — never to the raw tag.\n *\n * Two kinds of variables:\n * - SYSTEM variables, given by the application (`variables` prop / option),\n * read-only in the interface — this list is the default;\n * - CUSTOM variables, created by users in the builder and kept by the\n * storage (`listVariables` / `saveVariable` / `deleteVariable`), shared by\n * every template.\n *\n * The unsubscribe link is `{{ unsubscribe }}`: the sending pipeline replaces it\n * with the right link (see `applyServiceLinks`).\n *\n * Dependency-free: shared by the browser and the server.\n */\n\nimport { identity } from '../../i18n/index.js';\n\n/**\n * @typedef {{\n * key: string,\n * label: string,\n * group: string,\n * sample: string,\n * fallback?: string,\n * description?: string,\n * token?: string,\n * kind?: 'text'|'link',\n * custom?: boolean,\n * }} EmailVariable\n */\n\n/** @type {EmailVariable[]} */\nexport const DEFAULT_VARIABLES = [\n { key: 'contact.firstname', label: 'First name', group: 'Recipient', sample: 'Jane' },\n { key: 'contact.lastname', label: 'Last name', group: 'Recipient', sample: 'Doe' },\n { key: 'contact.email', label: 'Email address', group: 'Recipient', sample: 'jane.doe@example.com' },\n { key: 'company.name', label: 'Company', group: 'Recipient', sample: 'Example Inc.' },\n { key: 'campaign.name', label: 'Campaign name', group: 'Campaign', sample: 'Newsletter' },\n {\n key: 'unsubscribe',\n label: 'Unsubscribe link',\n group: 'Links',\n sample: '',\n token: '{{ unsubscribe }}',\n kind: 'link',\n },\n];\n\n/** Group of custom variables created without a group. */\nexport const CUSTOM_GROUP = 'Custom';\n\n/** Keys reserved by the sending pipeline. */\nconst RESERVED_KEYS = new Set(['unsubscribe', 'mirror']);\n\nexport const VARIABLE_LIMITS = { key: 60, label: 80, group: 40, sample: 200, fallback: 200, description: 200 };\n\nconst KEY_SHAPE = /^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$/;\n\n/** Tag inserted by the editor. @param {EmailVariable} variable */\nexport function tokenOf(variable) {\n return variable.token || `{{${variable.key}}}`;\n}\n\n/**\n * @param {string} key\n * @param {EmailVariable[]} [variables]\n */\nexport function findVariable(key, variables = DEFAULT_VARIABLES) {\n return variables.find((v) => v.key === key) || null;\n}\n\n/**\n * A key proposed from a label: \"Order number\" → `order_number`,\n * \"Numéro de commande\" → `numero_de_commande`.\n *\n * @param {string} label\n */\nexport function keyFromLabel(label) {\n const key = String(label || '')\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .replace(/^[^a-z]+/, '')\n .slice(0, VARIABLE_LIMITS.key)\n .replace(/_+$/, '');\n return key || 'variable';\n}\n\n/**\n * Checks an application-provided catalog (keys must be unique and safe to\n * embed in a tag).\n *\n * @param {EmailVariable[]} variables\n * @returns {EmailVariable[]}\n */\nexport function defineVariables(variables) {\n const seen = new Set();\n for (const variable of variables) {\n if (!variable || !KEY_SHAPE.test(variable.key || '')) {\n throw new Error(`variables: invalid key \"${variable?.key}\"`);\n }\n if (seen.has(variable.key)) throw new Error(`variables: duplicate key \"${variable.key}\"`);\n seen.add(variable.key);\n }\n return variables;\n}\n\nconst clean = (value, max) => String(value ?? '').replace(/\\s+/g, ' ').trim().slice(0, max);\n\n/**\n * Validates and normalizes a CUSTOM variable typed by a user.\n * Returns `{ variable }` or `{ errors: { field: message } }`.\n *\n * @param {Partial<EmailVariable>} input\n * @param {{\n * existing?: EmailVariable[],\n * editing?: string|null,\n * t?: import('../../i18n/index.js').Translate,\n * }} [options] `editing`: key of the variable being edited (its key cannot change)\n */\nexport function validateCustomVariable(input, { existing = [], editing = null, t = identity } = {}) {\n /** @type {Record<string, string>} */\n const errors = {};\n const label = clean(input?.label, VARIABLE_LIMITS.label);\n const key = editing || String(input?.key || '').trim();\n if (!label) errors.label = t('Name is required');\n if (!editing) {\n if (!key) errors.key = t('Key is required');\n else if (key.length > VARIABLE_LIMITS.key || !KEY_SHAPE.test(key)) {\n errors.key = t('Lowercase letters, digits, \"_\" and \".\" only, starting with a letter');\n } else if (RESERVED_KEYS.has(key) || existing.some((v) => v.key === key)) {\n errors.key = t('This key is already used');\n }\n }\n if (Object.keys(errors).length) return { errors };\n return {\n variable: {\n key,\n label,\n group: clean(input?.group, VARIABLE_LIMITS.group) || CUSTOM_GROUP,\n sample: clean(input?.sample, VARIABLE_LIMITS.sample),\n fallback: clean(input?.fallback, VARIABLE_LIMITS.fallback),\n description: clean(input?.description, VARIABLE_LIMITS.description),\n custom: true,\n },\n };\n}\n\n/**\n * System variables first, then custom ones (a custom variable never replaces\n * a system one).\n *\n * @param {EmailVariable[]} system\n * @param {EmailVariable[]} custom\n */\nexport function mergeVariables(system, custom = []) {\n const keys = new Set(system.map((v) => v.key));\n return [...system, ...custom.filter((v) => !keys.has(v.key)).map((v) => ({ ...v, custom: true }))];\n}\n","// @ts-check\n/**\n * Service links and uploaded images in a rendered email.\n *\n * `{{ unsubscribe }}` → the unsubscribe link. For a preview or a test it is\n * an INERT link: it can be displayed and clicked but\n * unsubscribes nobody — least of all the person\n * proofreading their test;\n * `{{ mirror }}` → \"view online\" link of some providers, `#` here.\n *\n * UPLOADED IMAGES: a template keeps a RELATIVE path (`/email-assets/<key>`),\n * made absolute at render time with the public URL of the environment that\n * sends. An absolute URL frozen at save time would point to whichever\n * environment saved the template.\n *\n * An uploaded image path is any root-relative path ending with an asset key:\n * 32 hex characters of the content hash and a known extension. The prefix is\n * the application's choice (`assetsPath` option of the server handler).\n */\n\nconst UNSUBSCRIBE = /\\{\\{\\s*unsubscribe\\s*\\}\\}|\\[UNSUBSCRIBE\\]/gi;\nconst MIRROR = /\\{\\{\\s*mirror\\s*\\}\\}/gi;\n\n/** Default public path of uploaded images. */\nexport const DEFAULT_ASSETS_PATH = '/email-assets/';\n\nconst ASSET_PATH_PATTERN = /^\\/(?:[A-Za-z0-9._~-]+\\/)*[a-f0-9]{32}\\.(png|jpg|gif)$/;\nconst RELATIVE_ASSET = /(^|[\"'(=\\s])(\\/(?:[A-Za-z0-9._~-]+\\/)*[a-f0-9]{32}\\.(?:png|jpg|gif))(?=[\"')\\s>]|$)/g;\n\n/** Is this the path of an uploaded image? @param {string} value */\nexport function isAssetPath(value) {\n const path = String(value || '').trim();\n return ASSET_PATH_PATTERN.test(path) && !path.split('/').some((part) => part === '..' || part === '.');\n}\n\n/**\n * Makes uploaded image paths absolute. Without a base URL the HTML is\n * returned as is (preview inside the application, same origin).\n *\n * @param {string} html\n * @param {string|null|undefined} baseUrl\n */\nexport function absoluteAssetUrls(html, baseUrl) {\n const root = String(baseUrl || '').replace(/\\/+$/, '');\n if (!root) return String(html || '');\n return String(html || '').replace(RELATIVE_ASSET, (match, before, path) =>\n isAssetPath(path) ? `${before}${root}${path}` : match\n );\n}\n\nfunction escapeAttribute(text) {\n return String(text ?? '')\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n\n/**\n * @param {string} html\n * @param {{unsubscribeUrl: string}} links\n */\nexport function applyServiceLinks(html, { unsubscribeUrl }) {\n return String(html || '')\n .replace(UNSUBSCRIBE, () => escapeAttribute(unsubscribeUrl))\n .replace(MIRROR, '#');\n}\n","// @ts-check\n/**\n * Variables service — replaces merge tags in compiled HTML (or in a subject).\n *\n * Only the tags of the catalog are touched, in their `{{key}}` or\n * `{{key|fallback}}` form (spaces allowed): other tags (an email provider's own\n * merge tags, `{{ mirror }}`) are left intact.\n *\n * Values are escaped in HTML: a first name \"<b>Jo</b>\" is displayed as typed,\n * it makes nothing bold — and cannot inject anything.\n *\n * Missing value → the tag's inline fallback → the variable's `fallback` → ''.\n * A raw tag never reaches a recipient.\n *\n * The unsubscribe link is NOT replaced here: only the sending pipeline knows\n * the right link (see `applyServiceLinks`).\n */\n\nimport { DEFAULT_VARIABLES } from './catalog.js';\n\n/** @typedef {import('./catalog.js').EmailVariable} EmailVariable */\n/** @typedef {Record<string, string|number|null|undefined>} VariableValues */\n\nfunction escapeHtml(text) {\n return String(text ?? '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\n/** Undoes the escaping the editor applied to an inline fallback. */\nfunction decodeHtml(text) {\n return String(text ?? '')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/&/g, '&');\n}\n\nconst escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/** `{{ key }}` or `{{ key | fallback }}`. @param {string} key */\nfunction patternOf(key) {\n return new RegExp(`\\\\{\\\\{\\\\s*${escapeRegExp(key)}\\\\s*(?:\\\\|([^{}]*))?\\\\}\\\\}`, 'g');\n}\n\n/** Any `{{ … }}` tag that looks like a variable. */\nconst ANY_TAG = /\\{\\{\\s*([A-Za-z][\\w.]*)\\s*(?:\\|[^{}]*)?\\}\\}/g;\n\n/** @param {EmailVariable[]} variables */\nconst textVariables = (variables) => variables.filter((v) => v.kind !== 'link');\n\nconst present = (value) => value !== null && value !== undefined && String(value) !== '';\n\n/**\n * @param {string} text\n * @param {VariableValues} values\n * @param {EmailVariable[]} variables\n * @param {boolean} html\n */\nfunction replace(text, values, variables, html) {\n let out = String(text || '');\n for (const variable of textVariables(variables)) {\n out = out.replace(patternOf(variable.key), (_, inline) => {\n const value = values?.[variable.key];\n if (present(value)) return html ? escapeHtml(value) : String(value);\n const fallback = inline !== undefined ? (html ? decodeHtml(inline) : inline).trim() : variable.fallback || '';\n return html ? escapeHtml(fallback) : fallback;\n });\n }\n return out;\n}\n\n/**\n * Replaces the known tags with values (escaped) in HTML.\n *\n * @param {string} html\n * @param {VariableValues} values\n * @param {EmailVariable[]} [variables]\n */\nexport function applyVariables(html, values, variables = DEFAULT_VARIABLES) {\n return replace(html, values, variables, true);\n}\n\n/**\n * Same as `applyVariables` for PLAIN TEXT (subject line, preview text): no\n * HTML escaping.\n *\n * @param {string} text\n * @param {VariableValues} values\n * @param {EmailVariable[]} [variables]\n */\nexport function applyVariablesToText(text, values, variables = DEFAULT_VARIABLES) {\n return replace(text, values, variables, false);\n}\n\n/**\n * Replaces the known tags with RAW replacements — typically the merge tags of\n * an email provider (`{ 'contact.firstname': '{{ contact.FIRSTNAME }}' }`).\n * A tag without replacement is left untouched.\n *\n * @param {string} html\n * @param {Record<string, string>} mapping\n * @param {EmailVariable[]} [variables]\n */\nexport function mapVariables(html, mapping, variables = DEFAULT_VARIABLES) {\n let out = String(html || '');\n for (const variable of textVariables(variables)) {\n if (!Object.prototype.hasOwnProperty.call(mapping, variable.key)) continue;\n out = out.replace(patternOf(variable.key), () => String(mapping[variable.key]));\n }\n return out;\n}\n\n/**\n * Catalog keys present in a text.\n * @param {string} text\n * @param {EmailVariable[]} [variables]\n */\nexport function usedVariables(text, variables = DEFAULT_VARIABLES) {\n const source = String(text || '');\n return variables.filter((v) => patternOf(v.key).test(source)).map((v) => v.key);\n}\n\n/**\n * Tags that look like variables but are not in the catalog (typos such as\n * `{{contact.fistname}}`). `ignore` lists tags handled elsewhere.\n *\n * @param {string} text\n * @param {EmailVariable[]} [variables]\n * @param {string[]} [ignore]\n */\nexport function unknownVariables(text, variables = DEFAULT_VARIABLES, ignore = ['unsubscribe', 'mirror']) {\n const known = new Set([...variables.map((v) => v.key), ...ignore]);\n const found = new Set();\n for (const match of String(text || '').matchAll(ANY_TAG)) {\n if (!known.has(match[1])) found.add(match[1]);\n }\n return [...found];\n}\n\n/**\n * Sample values (preview, test), with optional overrides\n * (e.g. `{ 'contact.email': 'reviewer@example.com' }`).\n *\n * @param {EmailVariable[]} [variables]\n * @param {VariableValues} [overrides]\n */\nexport function sampleValues(variables = DEFAULT_VARIABLES, overrides = {}) {\n /** @type {Record<string, string>} */\n const values = Object.fromEntries(textVariables(variables).map((v) => [v.key, v.sample]));\n for (const [key, value] of Object.entries(overrides)) {\n if (present(value)) values[key] = String(value);\n }\n return values;\n}\n","// @ts-check\n/**\n * Images UPLOADED in the builder — one rule shared by the browser (immediate\n * rejection, storage/client.js) and the server (storage/server/assetFile.js).\n *\n * PNG, JPEG, GIF: the three formats every email client displays. WebP and SVG\n * do not display in Outlook, and an SVG can carry script.\n * The server reads the type from the file BYTES; the browser, from `file.type`.\n */\n\nimport { identity } from '../i18n/index.js';\n\n/** Maximum image size: beyond it, the email becomes slow to open. */\nexport const MAX_IMAGE_BYTES = 2 * 1024 * 1024;\n\n/** Accepted types → extension of the public key. */\nexport const IMAGE_TYPES = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif' };\n\n/** Public key: 32 hex characters of the hash + extension. */\nexport const ASSET_KEY_PATTERN = /^[a-f0-9]{32}\\.(png|jpg|gif)$/;\n\nconst SIGNATURES = [\n { type: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },\n { type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },\n { type: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },\n];\n\n/**\n * Image type detected from the file signature, or `null`.\n * @param {Uint8Array} bytes\n * @returns {{type: string, ext: string}|null}\n */\nexport function detectImage(bytes) {\n if (!bytes || bytes.length < 8) return null;\n const found = SIGNATURES.find((s) => s.bytes.every((b, i) => bytes[i] === b));\n return found ? { type: found.type, ext: IMAGE_TYPES[found.type] } : null;\n}\n\n/**\n * Immediate rejection of a file the server would refuse, or `null`.\n * @param {{type?: string, size?: number}} file\n * @param {{t?: import('../i18n/index.js').Translate}} [options]\n */\nexport function imageUploadError(file, { t = identity } = {}) {\n if (!(String(file?.type || '') in IMAGE_TYPES)) return t('Format rejected: PNG, JPEG or GIF expected');\n if (Number(file?.size || 0) > MAX_IMAGE_BYTES) {\n return t('Image too large: {max} MB maximum', { max: MAX_IMAGE_BYTES / (1024 * 1024) });\n }\n return null;\n}\n","// @ts-check\n/**\n * URLs accepted in an email template.\n *\n * link (`href`) https, http, mailto:, tel:, `#…` anchor,\n * or a `{{ … }}` merge tag (unsubscribe,\n * variable) replaced at render time;\n * image (`src`, `background-url`…) https only: an http image is blocked or\n * flagged by most email clients, and\n * `data:` bloats the message until Gmail\n * clips it. Or an UPLOADED image (asset\n * path, see `isAssetPath`), made absolute\n * at render time (renderer/links.js).\n *\n * Everything else (`javascript:`, `data:`, `vbscript:`, `file:`…) is rejected.\n * The scheme is read AFTER decoding entities and stripping whitespace and\n * control characters: `java	script:` and `javascript:` are classics.\n */\nimport { isAssetPath } from '../renderer/links.js';\n\n/** Attributes carrying a LINK URL. */\nexport const LINK_ATTRIBUTES = new Set(['href', 'action', 'formaction', 'xlink:href']);\n\n/** Attributes carrying an IMAGE URL (MJML and HTML). */\nexport const IMAGE_ATTRIBUTES = new Set([\n 'src',\n 'srcset',\n 'background-url',\n 'background',\n 'poster',\n 'icon-unwrapped-url',\n 'icon-wrapped-url',\n]);\n\nconst NAMED_ENTITIES = {\n colon: ':',\n tab: '\\t',\n newline: '\\n',\n lpar: '(',\n rpar: ')',\n amp: '&',\n quot: '\"',\n apos: \"'\",\n lt: '<',\n gt: '>',\n nbsp: ' ',\n};\n\n/** Decodes the HTML entities of an attribute value. @param {string} value */\nexport function decodeEntities(value) {\n return String(value ?? '').replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);?/gi, (whole, body) => {\n const key = String(body).toLowerCase();\n if (key.startsWith('#x')) return String.fromCodePoint(Number.parseInt(key.slice(2), 16) || 0);\n if (key.startsWith('#')) return String.fromCodePoint(Number.parseInt(key.slice(1), 10) || 0);\n return NAMED_ENTITIES[key] ?? whole;\n });\n}\n\n/** A `{{ … }}` merge tag (variable, unsubscribe, web version). @param {string} value */\nexport function isMergeTag(value) {\n return /^\\{\\{\\s*[a-zA-Z][\\w.]*\\s*\\}\\}$/.test(String(value || '').trim());\n}\n\n/**\n * @param {string} raw\n * @param {'link'|'image'} kind\n * @returns {boolean}\n */\nexport function isSafeUrl(raw, kind) {\n const value = decodeEntities(raw).trim();\n if (!value) return true;\n if (isMergeTag(value)) return true;\n // Scheme read without whitespace or control characters (\"java\\tscript:\").\n // eslint-disable-next-line no-control-regex -- control characters are exactly what is stripped\n const compact = value.replace(/[\\s\\u0000-\\u001f\\u007f]+/g, '').toLowerCase();\n if (kind === 'image') return compact.startsWith('https://') || isAssetPath(value);\n if (compact.startsWith('#')) return true;\n return /^(https?:\\/\\/|mailto:|tel:)/.test(compact);\n}\n"],"mappings":"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","names":[]}
|