best-email-builder 0.3.0 → 0.5.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 +190 -0
- package/README.md +290 -20
- package/dist/ai-server.js +68 -0
- package/dist/ai-server.js.map +1 -0
- package/dist/ai.js +852 -0
- package/dist/ai.js.map +1 -0
- package/dist/builder.js +16 -8
- package/dist/chunk-2RZHGAH5.js +64 -0
- package/dist/chunk-2RZHGAH5.js.map +1 -0
- package/dist/chunk-3LKF4QBL.js +595 -0
- package/dist/chunk-3LKF4QBL.js.map +1 -0
- package/dist/chunk-5SASKCUC.js +251 -0
- package/dist/chunk-5SASKCUC.js.map +1 -0
- package/dist/{chunk-JOC752I3.js → chunk-6D3SLT5W.js} +27 -29
- package/dist/chunk-6D3SLT5W.js.map +1 -0
- package/dist/chunk-7U6BAKM7.js +119 -0
- package/dist/chunk-7U6BAKM7.js.map +1 -0
- package/dist/chunk-DFNHICTF.js +1359 -0
- package/dist/chunk-DFNHICTF.js.map +1 -0
- package/dist/chunk-ECHGJZ2W.js +327 -0
- package/dist/chunk-ECHGJZ2W.js.map +1 -0
- package/dist/chunk-KXJJSVN5.js +98 -0
- package/dist/chunk-KXJJSVN5.js.map +1 -0
- package/dist/{chunk-ZSUHOCCE.js → chunk-LXR4QTGP.js} +123 -579
- package/dist/chunk-LXR4QTGP.js.map +1 -0
- package/dist/{chunk-BVOVAEEJ.js → chunk-LYMJ2TBX.js} +2 -2
- package/dist/chunk-MPX5CWTZ.js +1687 -0
- package/dist/chunk-MPX5CWTZ.js.map +1 -0
- package/dist/chunk-PB7YZ3WP.js +5635 -0
- package/dist/chunk-PB7YZ3WP.js.map +1 -0
- package/dist/chunk-UFKCLU2D.js +255 -0
- package/dist/chunk-UFKCLU2D.js.map +1 -0
- package/dist/index.js +51 -20
- package/dist/index.js.map +1 -1
- package/dist/library.js +12 -7
- package/dist/server.js +513 -214
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +12 -0
- package/dist/types/ai/GenerateButton.d.ts +4 -0
- package/dist/types/ai/RewriteText.d.ts +5 -0
- package/dist/types/ai/SubjectIdeas.d.ts +25 -0
- package/dist/types/ai/chip.d.ts +1 -0
- package/dist/types/ai/claude.d.ts +21 -0
- package/dist/types/ai/createAi.d.ts +66 -0
- package/dist/types/ai/draft.d.ts +163 -0
- package/dist/types/ai/generator.d.ts +123 -0
- package/dist/types/ai/i18n/fr.d.ts +2 -0
- package/dist/types/ai/i18n/index.d.ts +2 -0
- package/dist/types/ai/index.d.ts +9 -0
- package/dist/types/ai/keys.d.ts +46 -0
- package/dist/types/ai/prompt.d.ts +76 -0
- package/dist/types/ai/providers.d.ts +67 -0
- package/dist/types/ai/purposes.d.ts +48 -0
- package/dist/types/ai/rewrite.d.ts +83 -0
- package/dist/types/ai/server.d.ts +9 -0
- package/dist/types/ai/subjects.d.ts +97 -0
- package/dist/types/blocks/shared.d.ts +20 -0
- package/dist/types/core/EmailEditor.d.ts +72 -19
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/core/mjmlDefaults.d.ts +13 -0
- package/dist/types/core/scrollbars.d.ts +1 -1
- package/dist/types/defaultEditor.d.ts +4 -1
- package/dist/types/engine/NativeAdapter.d.ts +28 -0
- package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
- package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
- package/dist/types/engine/canvas/icons.d.ts +14 -0
- package/dist/types/engine/canvas/morph.d.ts +21 -0
- package/dist/types/engine/canvasMjml.d.ts +30 -0
- package/dist/types/engine/mjml.d.ts +31 -0
- package/dist/types/engine/model.d.ts +130 -0
- package/dist/types/engine/placement.d.ts +69 -0
- package/dist/types/engine/richText.d.ts +15 -0
- package/dist/types/engine/unwrapModule.d.ts +29 -0
- package/dist/types/features/brand/BrandButton.d.ts +4 -0
- package/dist/types/features/brand/brand.d.ts +108 -0
- package/dist/types/features/brand/extract.d.ts +61 -0
- package/dist/types/features/brand/index.d.ts +2 -0
- package/dist/types/features/brand/messages.d.ts +13 -0
- package/dist/types/features/brand/useBrand.d.ts +22 -0
- package/dist/types/features/preview/PreviewButton.d.ts +1 -1
- package/dist/types/features/preview/darkMode.d.ts +65 -0
- package/dist/types/features/variables/VariablePicker.d.ts +3 -2
- package/dist/types/i18n/index.d.ts +8 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/server/brand.d.ts +32 -0
- package/dist/types/server/brandImport.d.ts +93 -0
- package/dist/types/server/compileMjml.d.ts +2 -0
- package/dist/types/server/handler.d.ts +26 -0
- package/dist/types/server/index.d.ts +5 -1
- package/dist/types/storage/client.d.ts +57 -3
- package/dist/types/ui/EmailBuilder.d.ts +5 -1
- package/dist/types/ui/EmailStudio.d.ts +6 -1
- package/dist/types/ui/PropertiesPanel.d.ts +2 -1
- package/dist/types/ui/fields.d.ts +2 -1
- package/dist/types/ui/primitives/button.d.ts +1 -1
- package/dist/types/ui/primitives/color-picker.d.ts +2 -1
- package/dist/types/ui/primitives/searchShortcut.d.ts +16 -0
- package/dist/types/ui/useLayoutWidth.d.ts +1 -1
- package/package.json +27 -7
- package/dist/chunk-3M36VVXS.js +0 -665
- package/dist/chunk-3M36VVXS.js.map +0 -1
- package/dist/chunk-57CTQNTK.js +0 -56
- package/dist/chunk-57CTQNTK.js.map +0 -1
- package/dist/chunk-ILRAQTHS.js +0 -3677
- package/dist/chunk-ILRAQTHS.js.map +0 -1
- package/dist/chunk-JOC752I3.js.map +0 -1
- package/dist/chunk-ZSUHOCCE.js.map +0 -1
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -148
- package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
- /package/dist/{chunk-BVOVAEEJ.js.map → chunk-LYMJ2TBX.js.map} +0 -0
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
// src/i18n/fr.js
|
|
2
|
+
var fr = {
|
|
3
|
+
"1. Complete your profile": "1. Compl\xE9tez votre profil",
|
|
4
|
+
"2. Explore": "2. Explorez",
|
|
5
|
+
"3. Ask us anything": "3. Posez-nous vos questions",
|
|
6
|
+
"30% off everything": "-30 % sur tout",
|
|
7
|
+
"30% off this weekend only": "-30 % ce week-end seulement",
|
|
8
|
+
"4 columns": "4 colonnes",
|
|
9
|
+
"4-column section": "Section en 4 colonnes",
|
|
10
|
+
"A few words about it.": "Quelques mots pour le pr\xE9senter.",
|
|
11
|
+
"A new feature": "Une nouveaut\xE9",
|
|
12
|
+
"A sale with a discount code and two products": "Une vente avec un code promo et deux produits",
|
|
13
|
+
"A tip": "Une astuce",
|
|
14
|
+
"Add a block": "Ajouter un bloc",
|
|
15
|
+
"Add to favorites": "Ajouter aux favoris",
|
|
16
|
+
"All blocks": "Tous les blocs",
|
|
17
|
+
"All capitals": "Tout en majuscules",
|
|
18
|
+
"All templates": "Tous les mod\xE8les",
|
|
19
|
+
"All your templates are in the favorites.": "Tous vos mod\xE8les sont dans les favoris.",
|
|
20
|
+
"An empty page to build from scratch": "Une page vide pour partir de z\xE9ro",
|
|
21
|
+
"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.",
|
|
22
|
+
"An event": "Un \xE9v\xE9nement",
|
|
23
|
+
"Apply the link": "Appliquer le lien",
|
|
24
|
+
"Article {n}": "Article {n}",
|
|
25
|
+
"As typed": "Comme saisi",
|
|
26
|
+
"Background image": "Image de fond",
|
|
27
|
+
"Background image with a title and a button": "Image de fond avec un titre et un bouton",
|
|
28
|
+
"Background width": "Largeur du fond",
|
|
29
|
+
"Base": "Base",
|
|
30
|
+
"Base \u2014 not editable: start from it to create your template.": "Base non modifiable \u2014 partez-en pour cr\xE9er votre mod\xE8le.",
|
|
31
|
+
"Bases": "Bases",
|
|
32
|
+
"Blank": "Vierge",
|
|
33
|
+
"Blog": "Blog",
|
|
34
|
+
"Bold (Ctrl+B)": "Gras (Ctrl+B)",
|
|
35
|
+
"Book my seat": "Je r\xE9serve ma place",
|
|
36
|
+
"Builder": "Constructeur",
|
|
37
|
+
"Buy": "Acheter",
|
|
38
|
+
"by {name}": "par {name}",
|
|
39
|
+
"Capitals": "Majuscules",
|
|
40
|
+
"Choose a template": "Choisissez un mod\xE8le",
|
|
41
|
+
"Choose an image in the panel": "Choisissez une image dans le panneau",
|
|
42
|
+
"Clear formatting": "Effacer la mise en forme",
|
|
43
|
+
"Clear the search": "Effacer la recherche",
|
|
44
|
+
"Contact": "Contact",
|
|
45
|
+
"Copy HTML": "Copier le HTML",
|
|
46
|
+
"Could not load the templates.": "Impossible de charger les mod\xE8les.",
|
|
47
|
+
"Created": "Cr\xE9\xE9",
|
|
48
|
+
"Date, place and a button to book a seat": "Date, lieu et un bouton pour r\xE9server",
|
|
49
|
+
"Default ({value})": "Par d\xE9faut ({value})",
|
|
50
|
+
"Delete \u201C{name}\u201D?": "Supprimer \xAB {name} \xBB ?",
|
|
51
|
+
"Direction": "Disposition",
|
|
52
|
+
"Download": "T\xE9l\xE9charger",
|
|
53
|
+
"Drag to move": "Glisser pour d\xE9placer",
|
|
54
|
+
"Drop a block here": "D\xE9posez un bloc ici",
|
|
55
|
+
"Edit: {element}": "Modifier : {element}",
|
|
56
|
+
"Email canvas": "Aper\xE7u modifiable de l\u2019e-mail",
|
|
57
|
+
"Email settings": "R\xE9glages de l'e-mail",
|
|
58
|
+
"Email template library": "Biblioth\xE8que de mod\xE8les e-mail",
|
|
59
|
+
"Equal": "\xC9gales",
|
|
60
|
+
"Facebook": "Facebook",
|
|
61
|
+
"Favorites": "Favoris",
|
|
62
|
+
"Fill the section": "Remplir la section",
|
|
63
|
+
"First letters": "Premi\xE8res lettres",
|
|
64
|
+
"Get started": "Commencer",
|
|
65
|
+
"GitHub": "GitHub",
|
|
66
|
+
"Greets a new customer and lists the first steps": "Accueille un nouveau client et liste les premi\xE8res \xE9tapes",
|
|
67
|
+
"Heading": "Titre",
|
|
68
|
+
"Hero banner": "Banni\xE8re",
|
|
69
|
+
"Horizontal": "Horizontale",
|
|
70
|
+
"HTML copied.": "HTML copi\xE9.",
|
|
71
|
+
"Icon background": "Fond de l'ic\xF4ne",
|
|
72
|
+
"Icon size": "Taille des ic\xF4nes",
|
|
73
|
+
"Icons linking to your profiles": "Ic\xF4nes vers vos profils",
|
|
74
|
+
"Image + text": "Image + texte",
|
|
75
|
+
"Image beside a title, text and button": "Image \xE0 c\xF4t\xE9 d'un titre, d'un texte et d'un bouton",
|
|
76
|
+
"Image position": "Position de l'image",
|
|
77
|
+
"Image size": "Taille de l'image",
|
|
78
|
+
"In brief": "En bref",
|
|
79
|
+
"Instagram": "Instagram",
|
|
80
|
+
"Italic (Ctrl+I)": "Italique (Ctrl+I)",
|
|
81
|
+
"Keep its width": "Garder sa largeur",
|
|
82
|
+
"Label": "Texte affich\xE9",
|
|
83
|
+
"Large bold title": "Grand titre en gras",
|
|
84
|
+
"LinkedIn": "LinkedIn",
|
|
85
|
+
"Logo, headline, text and a button": "Logo, titre, texte et un bouton",
|
|
86
|
+
"Menu": "Menu",
|
|
87
|
+
"Menu link": "Lien du menu",
|
|
88
|
+
"Monthly news: banner, two articles, highlights": "Les nouvelles du mois : banni\xE8re, deux articles, br\xE8ves",
|
|
89
|
+
"Network": "R\xE9seau",
|
|
90
|
+
"No preview: this storage cannot render emails.": "Pas d'aper\xE7u : ce stockage ne sait pas compiler les e-mails.",
|
|
91
|
+
"No template yet. Start from a base, or create a new one.": "Aucun mod\xE8le pour l\u2019instant. Partez d\u2019une base, ou cr\xE9ez-en un.",
|
|
92
|
+
"Nothing matches \u201C{query}\u201D.": "Aucun r\xE9sultat pour \xAB {query} \xBB.",
|
|
93
|
+
"Offer valid until Sunday midnight, while stocks last.": "Offre valable jusqu'\xE0 dimanche minuit, dans la limite des stocks.",
|
|
94
|
+
"On mobile": "Sur mobile",
|
|
95
|
+
"One sentence about it.": "Une phrase pour en parler.",
|
|
96
|
+
"One sentence that makes people want to read on.": "Une phrase qui donne envie de lire la suite.",
|
|
97
|
+
"Origin": "Origine",
|
|
98
|
+
"Original size": "Taille d'origine",
|
|
99
|
+
"Our offices, 1 Main Street": "Nos bureaux, 1 rue Principale",
|
|
100
|
+
"Picking a template replaces the email being edited.": "Choisir un mod\xE8le remplace l'e-mail en cours de modification.",
|
|
101
|
+
"Product one": "Produit un",
|
|
102
|
+
"Product two": "Produit deux",
|
|
103
|
+
"Profile URL": "Adresse du profil",
|
|
104
|
+
"Promotion": "Promotion",
|
|
105
|
+
"Read more": "Lire la suite",
|
|
106
|
+
"Ready-made templates to copy into your campaigns.": "Mod\xE8les pr\xEAts \xE0 copier dans vos campagnes.",
|
|
107
|
+
"Remove from favorites": "Retirer des favoris",
|
|
108
|
+
"Remove the link": "Retirer le lien",
|
|
109
|
+
"Replace": "Remplacer",
|
|
110
|
+
"Replace the current email?": "Remplacer l'e-mail actuel ?",
|
|
111
|
+
"Reply to this email: a person reads it.": "R\xE9pondez \xE0 cet e-mail : une personne le lit.",
|
|
112
|
+
"Row of links to your site": "Rang\xE9e de liens vers votre site",
|
|
113
|
+
"Save the date \u2014 seats are limited.": "Notez la date \u2014 les places sont limit\xE9es.",
|
|
114
|
+
"Search templates": "Rechercher un mod\xE8le",
|
|
115
|
+
"Seats are limited: book yours now, it takes ten seconds.": "Les places sont limit\xE9es : r\xE9servez la v\xF4tre, cela prend dix secondes.",
|
|
116
|
+
"Select a template on the left.": "S\xE9lectionnez un mod\xE8le \xE0 gauche.",
|
|
117
|
+
"Select the parent": "S\xE9lectionner le bloc parent",
|
|
118
|
+
"Shop": "Boutique",
|
|
119
|
+
"Shop the sale": "Voir les offres",
|
|
120
|
+
"Simple": "Simple",
|
|
121
|
+
"So we can tailor what we send you.": "Pour adapter ce que nous vous envoyons.",
|
|
122
|
+
"Social link": "R\xE9seau social",
|
|
123
|
+
"Social links": "R\xE9seaux sociaux",
|
|
124
|
+
"Space around each icon": "Espace autour de chaque ic\xF4ne",
|
|
125
|
+
"Start from a template": "Partir d'un mod\xE8le",
|
|
126
|
+
"Start from this base": "Partir de cette base",
|
|
127
|
+
"Strikethrough": "Barr\xE9",
|
|
128
|
+
"Take a tour of what you can do.": "Faites le tour de ce que vous pouvez faire.",
|
|
129
|
+
"Template deleted.": "Mod\xE8le supprim\xE9.",
|
|
130
|
+
"Template duplicated.": "Mod\xE8le dupliqu\xE9.",
|
|
131
|
+
"Templates": "Mod\xE8les",
|
|
132
|
+
"Text formatting": "Mise en forme du texte",
|
|
133
|
+
"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.",
|
|
134
|
+
"The browser refused to copy.": "Le navigateur a refus\xE9 la copie.",
|
|
135
|
+
"The news of the month": "Les nouvelles du mois",
|
|
136
|
+
"The template and its versions will be deleted.": "Le mod\xE8le et ses versions seront supprim\xE9s.",
|
|
137
|
+
"The template preview appears here.": "L'aper\xE7u du mod\xE8le s'affiche ici.",
|
|
138
|
+
"This template was saved in a format this builder cannot open.": "Ce mod\xE8le a \xE9t\xE9 enregistr\xE9 dans un format que ce constructeur ne sait pas ouvrir.",
|
|
139
|
+
"This weekend only, with the code SPRING30.": "Ce week-end seulement, avec le code SPRING30.",
|
|
140
|
+
"Three steps to get started.": "Trois \xE9tapes pour bien commencer.",
|
|
141
|
+
"Thursday, 7 pm": "Jeudi, 19 h",
|
|
142
|
+
"Title": "Titre",
|
|
143
|
+
"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.",
|
|
144
|
+
"Underline (Ctrl+U)": "Soulign\xE9 (Ctrl+U)",
|
|
145
|
+
"Updated": "Modifi\xE9",
|
|
146
|
+
"Vertical": "Verticale",
|
|
147
|
+
"Welcome aboard!": "Bienvenue \xE0 bord !",
|
|
148
|
+
"Welcome, {name}!": "Bienvenue, {name} !",
|
|
149
|
+
"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.",
|
|
150
|
+
"When": "Quand",
|
|
151
|
+
"Where": "O\xF9",
|
|
152
|
+
"Whole image": "Image enti\xE8re",
|
|
153
|
+
"Whole screen": "Tout l\u2019\xE9cran",
|
|
154
|
+
"With the code SPRING30, until Sunday midnight.": "Avec le code SPRING30, jusqu'\xE0 dimanche minuit.",
|
|
155
|
+
"X": "X",
|
|
156
|
+
"You're invited": "On vous invite !",
|
|
157
|
+
"Your big announcement": "Votre grande annonce",
|
|
158
|
+
"YouTube": "YouTube",
|
|
159
|
+
"{count} sections selected. Press Delete to remove them (Ctrl+Z brings them back).": "{count} sections s\xE9lectionn\xE9es. Appuyez sur Suppr pour les supprimer (Ctrl+Z les r\xE9tablit).",
|
|
160
|
+
"{label} \u2014 {side}": "{label} \u2014 {side}",
|
|
161
|
+
"{max} test addresses at most": "{max} adresses de test au maximum",
|
|
162
|
+
"{message} Your version can be saved as a copy.": "{message} Votre version peut \xEAtre enregistr\xE9e comme copie.",
|
|
163
|
+
"{name} (copy)": "{name} (copie)",
|
|
164
|
+
"{sent} sent, {failed} failed.": "{sent} parti(s), {failed} en \xE9chec.",
|
|
165
|
+
"{sent} test(s) sent \u2014 check your inbox.": "{sent} test(s) parti(s) \u2014 ouvrez votre messagerie avant de continuer.",
|
|
166
|
+
"1 column": "1 colonne",
|
|
167
|
+
"2 columns": "2 colonnes",
|
|
168
|
+
"2-column section": "Section en 2 colonnes",
|
|
169
|
+
"25%": "25 %",
|
|
170
|
+
"3 columns": "3 colonnes",
|
|
171
|
+
"3-column section": "Section en 3 colonnes",
|
|
172
|
+
"50%": "50 %",
|
|
173
|
+
"600 px (standard)": "600 px (standard)",
|
|
174
|
+
"640 px": "640 px",
|
|
175
|
+
"75%": "75 %",
|
|
176
|
+
"Address not allowed": "Adresse refus\xE9e",
|
|
177
|
+
"Addresses": "Adresses",
|
|
178
|
+
"Alignment": "Alignement",
|
|
179
|
+
"Alt text": "Texte alternatif",
|
|
180
|
+
"An https:// address is required": "Une adresse https:// est requise",
|
|
181
|
+
"attribute {name} not allowed": "attribut {name} interdit",
|
|
182
|
+
"Authentication required": "Authentification requise",
|
|
183
|
+
"Autosave": "Sauvegarde automatique",
|
|
184
|
+
"Autosave paused": "Sauvegarde automatique suspendue",
|
|
185
|
+
"Autosaved at {time}": "Enregistr\xE9 automatiquement \xE0 {time}",
|
|
186
|
+
"Autosaving\u2026": "Sauvegarde automatique\u2026",
|
|
187
|
+
"Available in every template.": "Disponible dans tous les mod\xE8les.",
|
|
188
|
+
"Back": "Retour",
|
|
189
|
+
"Background": "Fond",
|
|
190
|
+
"base tag not allowed": "balise base interdite",
|
|
191
|
+
"Blocks": "Blocs",
|
|
192
|
+
"Bold": "Gras",
|
|
193
|
+
"Bottom": "Bas",
|
|
194
|
+
"Button": "Bouton",
|
|
195
|
+
"Button background": "Fond du bouton",
|
|
196
|
+
"Button padding": "Marges du bouton",
|
|
197
|
+
"Button without link ({count})": "Bouton sans lien ({count})",
|
|
198
|
+
"Call to action": "Appel \xE0 l\u2019action",
|
|
199
|
+
"Campaign": "Campagne",
|
|
200
|
+
"Campaign name": "Nom de la campagne",
|
|
201
|
+
"Cancel": "Annuler",
|
|
202
|
+
"CDATA section not allowed": "section CDATA interdite",
|
|
203
|
+
"Center": "Centre",
|
|
204
|
+
"Close": "Fermer",
|
|
205
|
+
"Colors": "Couleurs",
|
|
206
|
+
"Column": "Colonne",
|
|
207
|
+
"Company": "Soci\xE9t\xE9",
|
|
208
|
+
"component script not allowed": "script de composant interdit",
|
|
209
|
+
"conditional comment not allowed": "commentaire conditionnel interdit",
|
|
210
|
+
"Confirm": "Confirmer",
|
|
211
|
+
"Content": "Contenu",
|
|
212
|
+
"Content width": "Largeur du contenu",
|
|
213
|
+
"Copy saved: \u201C{name}\u201D.": "Copie enregistr\xE9e : \xAB {name} \xBB.",
|
|
214
|
+
"Corner radius": "Arrondi",
|
|
215
|
+
"Create": "Cr\xE9er",
|
|
216
|
+
"Create \u201C{name}\u201D": "Cr\xE9er \xAB {name} \xBB",
|
|
217
|
+
"current": "actuelle",
|
|
218
|
+
"Custom": "Personnalis\xE9es",
|
|
219
|
+
"Custom variables are not configured": "Les variables personnalis\xE9es ne sont pas configur\xE9es",
|
|
220
|
+
"Dashed": "Tirets",
|
|
221
|
+
"Default": "Par d\xE9faut",
|
|
222
|
+
"Delete": "Supprimer",
|
|
223
|
+
"Delete (Del)": "Supprimer (Suppr)",
|
|
224
|
+
"Delete the variable \u201C{name}\u201D?": "Supprimer la variable \xAB {name} \xBB ?",
|
|
225
|
+
"Describes the image if it is blocked": "D\xE9crit l\u2019image si elle est bloqu\xE9e",
|
|
226
|
+
"Deselect": "D\xE9s\xE9lectionner",
|
|
227
|
+
"Desktop": "Bureau",
|
|
228
|
+
"Device": "Appareil",
|
|
229
|
+
"Dismiss": "Masquer",
|
|
230
|
+
"Divider": "S\xE9parateur",
|
|
231
|
+
"Dotted": "Pointill\xE9s",
|
|
232
|
+
"Double-click the text to edit it.": "Double-cliquez sur le texte pour le modifier.",
|
|
233
|
+
"Duplicate": "Dupliquer",
|
|
234
|
+
"Edit": "Modifier",
|
|
235
|
+
"Edit \u201C{name}\u201D": "Modifier \xAB {name} \xBB",
|
|
236
|
+
"Edit variable": "Modifier la variable",
|
|
237
|
+
"Element actions": "Actions sur l\u2019\xE9l\xE9ment",
|
|
238
|
+
"Email": "Courriel",
|
|
239
|
+
"Email address": "Adresse e-mail",
|
|
240
|
+
"Email preview": "Aper\xE7u du courriel",
|
|
241
|
+
"embed not allowed": "embed interdit",
|
|
242
|
+
"Empty file": "Fichier vide",
|
|
243
|
+
"Empty MJML": "MJML vide",
|
|
244
|
+
"Empty MJML compilation": "Compilation MJML vide",
|
|
245
|
+
"Empty subject": "Objet vide",
|
|
246
|
+
"executable content not allowed": "contenu ex\xE9cutable interdit",
|
|
247
|
+
"external resource not allowed": "ressource externe interdite",
|
|
248
|
+
"File missing": "Fichier manquant",
|
|
249
|
+
"Final rendering, variables filled with samples.": "Rendu final, variables remplies d\u2019exemples.",
|
|
250
|
+
"First name": "Pr\xE9nom",
|
|
251
|
+
"Fit to text": "Ajust\xE9e au texte",
|
|
252
|
+
"Font": "Police",
|
|
253
|
+
"forbidden element in project: {tag}": "\xE9l\xE9ment interdit dans le projet : {tag}",
|
|
254
|
+
"form field not allowed": "champ de formulaire interdit",
|
|
255
|
+
"form not allowed": "formulaire interdit",
|
|
256
|
+
"Format rejected: PNG, JPEG or GIF expected": "Format refus\xE9 : PNG, JPEG ou GIF attendu",
|
|
257
|
+
"frame not allowed": "frame interdite",
|
|
258
|
+
"Full length": "Toute la largeur",
|
|
259
|
+
"Full width": "Pleine largeur",
|
|
260
|
+
"Full-width section": "Section pleine largeur",
|
|
261
|
+
"Give the template a name before saving it.": "Donnez un libell\xE9 au mod\xE8le avant de l\u2019enregistrer.",
|
|
262
|
+
"Group": "Groupe",
|
|
263
|
+
"Height": "Hauteur",
|
|
264
|
+
"Hello {name},": "Bonjour {name},",
|
|
265
|
+
"Hex code": "Code hexad\xE9cimal",
|
|
266
|
+
"Horizontal rule": "Filet horizontal",
|
|
267
|
+
"HTML button not allowed (use the Button block)": "bouton HTML interdit (utiliser le bloc Bouton)",
|
|
268
|
+
"iframe not allowed": "iframe interdite",
|
|
269
|
+
"Ignored (invalid): {email}": "Ignor\xE9e (invalide) : {email}",
|
|
270
|
+
"Image": "Image",
|
|
271
|
+
"image rejected ({name})": "image refus\xE9e ({name})",
|
|
272
|
+
"image rejected ({name}): https only": "image refus\xE9e ({name}) : https seulement",
|
|
273
|
+
"Image too large: {max} MB maximum": "Image trop lourde : {max} Mo au maximum",
|
|
274
|
+
"Image without alt text ({count})": "Texte alternatif manquant ({count} image(s))",
|
|
275
|
+
"Image without file ({count})": "Image sans fichier ({count})",
|
|
276
|
+
"Inbox": "Bo\xEEte de r\xE9ception",
|
|
277
|
+
"Indicative preview \u2014 check it with \u201CSend test\u201D.": "Aper\xE7u indicatif \u2014 validez avec \xAB Envoyer un test \xBB.",
|
|
278
|
+
"inline style rejected": "style en ligne refus\xE9",
|
|
279
|
+
"Insecure http link ({count})": "Lien non s\xE9curis\xE9 en http ({count})",
|
|
280
|
+
"Insert a variable in the preview text": "Ins\xE9rer une variable dans le texte d\u2019aper\xE7u",
|
|
281
|
+
"Insert a variable in the subject": "Ins\xE9rer une variable dans l\u2019objet",
|
|
282
|
+
"Internal error": "Erreur interne",
|
|
283
|
+
"Invalid JSON": "JSON invalide",
|
|
284
|
+
"Justify": "Justifi\xE9",
|
|
285
|
+
"Key is required": "Balise requise",
|
|
286
|
+
"Last name": "Nom",
|
|
287
|
+
"Layout": "Mise en page",
|
|
288
|
+
"Learn more": "D\xE9couvrir",
|
|
289
|
+
"Leave": "Quitter",
|
|
290
|
+
"Leave the builder?": "Quitter le constructeur ?",
|
|
291
|
+
"Left": "Gauche",
|
|
292
|
+
"Length": "Longueur",
|
|
293
|
+
"Line color": "Couleur du filet",
|
|
294
|
+
"Line height": "Interligne",
|
|
295
|
+
"Line style": "Trait",
|
|
296
|
+
"Link": "Lien",
|
|
297
|
+
"Link on click": "Lien au clic",
|
|
298
|
+
"Link URL": "Adresse du lien",
|
|
299
|
+
"Links": "Liens",
|
|
300
|
+
"Loading the builder": "Chargement du constructeur",
|
|
301
|
+
"Loading\u2026": "Chargement\u2026",
|
|
302
|
+
'Lowercase letters, digits, "_" and "." only, starting with a letter': "Minuscules, chiffres, \xAB _ \xBB et \xAB . \xBB seulement, en commen\xE7ant par une lettre",
|
|
303
|
+
"MathML not allowed": "MathML interdit",
|
|
304
|
+
"Message is {size} KB: Gmail clips messages over {limit} KB": "Message de {size} Ko : Gmail le coupe au-del\xE0 de {limit} Ko",
|
|
305
|
+
"meta tag not allowed": "balise meta interdite",
|
|
306
|
+
"Middle": "Milieu",
|
|
307
|
+
"missing <mj-body>": "<mj-body> manquant",
|
|
308
|
+
"Missing unsubscribe link": "Lien de d\xE9sinscription absent",
|
|
309
|
+
"mj-font (external font) is outside the builder": "mj-font (police externe) est hors du constructeur",
|
|
310
|
+
"mj-include would read a file from the server": "mj-include lirait un fichier du serveur",
|
|
311
|
+
"mj-raw (free HTML) is not allowed": "mj-raw (HTML libre) est refus\xE9",
|
|
312
|
+
"mj-style (free CSS) is outside the builder": "mj-style (CSS libre) est hors du constructeur",
|
|
313
|
+
"MJML too large ({size} KB, maximum {max} KB)": "MJML trop volumineux ({size} ko, maximum {max} ko)",
|
|
314
|
+
"Mobile": "Mobile",
|
|
315
|
+
"Move down": "Descendre",
|
|
316
|
+
"Move left": "D\xE9placer \xE0 gauche",
|
|
317
|
+
"Move right": "D\xE9placer \xE0 droite",
|
|
318
|
+
"Move up": "Monter",
|
|
319
|
+
"Multipart form expected": "Formulaire multipart attendu",
|
|
320
|
+
"Name": "Libell\xE9",
|
|
321
|
+
"Name is required": "Libell\xE9 requis",
|
|
322
|
+
"Name required": "Libell\xE9 requis",
|
|
323
|
+
"New template": "Nouveau mod\xE8le",
|
|
324
|
+
"New variable": "Nouvelle variable",
|
|
325
|
+
"No changes": "Aucune modification",
|
|
326
|
+
"No preview text": "Aucun texte d\u2019aper\xE7u",
|
|
327
|
+
"No subject": "Sans objet",
|
|
328
|
+
"No usable test address": "Aucune adresse de test exploitable",
|
|
329
|
+
"No variable found.": "Aucune variable trouv\xE9e.",
|
|
330
|
+
"No versions yet.": "Aucune version.",
|
|
331
|
+
"Normal": "Normale",
|
|
332
|
+
"Nothing to fix.": "Aucun point \xE0 corriger.",
|
|
333
|
+
"Nothing to show yet.": "Rien \xE0 afficher pour l\u2019instant.",
|
|
334
|
+
"object not allowed": "object interdit",
|
|
335
|
+
"Order number": "Num\xE9ro de commande",
|
|
336
|
+
"Padding": "Marges",
|
|
337
|
+
"Page background": "Fond de page",
|
|
338
|
+
"Paragraph or heading": "Paragraphe ou titre",
|
|
339
|
+
"Photo, visual or logo": "Photo, visuel ou logo",
|
|
340
|
+
"Pick a colour": "Choisir une couleur",
|
|
341
|
+
"Points to check": "Points \xE0 v\xE9rifier",
|
|
342
|
+
"Preview": "Aper\xE7u",
|
|
343
|
+
"Preview and tests": "Aper\xE7u et tests",
|
|
344
|
+
"Preview text": "Texte d\u2019aper\xE7u",
|
|
345
|
+
"Project rejected: {issues}": "Projet refus\xE9 : {issues}",
|
|
346
|
+
"project too deep": "projet trop profond",
|
|
347
|
+
"project too large ({size} KB, maximum {max} KB)": "projet trop volumineux ({size} ko, maximum {max} ko)",
|
|
348
|
+
"Properties": "Propri\xE9t\xE9s",
|
|
349
|
+
"Recipient": "Destinataire",
|
|
350
|
+
"Redo": "R\xE9tablir",
|
|
351
|
+
"Redo (Ctrl+Y)": "R\xE9tablir (Ctrl+Y)",
|
|
352
|
+
"Refresh": "Rafra\xEEchir",
|
|
353
|
+
"Rendering\u2026": "Rendu en cours\u2026",
|
|
354
|
+
"Request too large": "Requ\xEAte trop volumineuse",
|
|
355
|
+
"Restore": "Recharger",
|
|
356
|
+
"Restore version {n}": "Recharger la version {n}",
|
|
357
|
+
"Restore version {n}?": "Recharger la version {n} ?",
|
|
358
|
+
"Results": "R\xE9sultats",
|
|
359
|
+
"Resume": "Reprendre",
|
|
360
|
+
"Resume the draft?": "Reprendre le brouillon ?",
|
|
361
|
+
"Retry": "R\xE9essayer",
|
|
362
|
+
"Right": "Droite",
|
|
363
|
+
"Route not found": "Route introuvable",
|
|
364
|
+
"Sample value": "Valeur d\u2019exemple",
|
|
365
|
+
"Save": "Enregistrer",
|
|
366
|
+
"Save a copy": "Enregistrer une copie",
|
|
367
|
+
"Save the template first": "Enregistrez d\u2019abord le mod\xE8le",
|
|
368
|
+
"Saved": "Enregistr\xE9",
|
|
369
|
+
"Saved \u2014 version {n}.": "Enregistr\xE9 \u2014 version {n}.",
|
|
370
|
+
"Saved at {time}": "Enregistr\xE9 \xE0 {time}",
|
|
371
|
+
"Saved, with {count} MJML warning(s): {message}": "Enregistr\xE9, avec {count} avertissement(s) MJML : {message}",
|
|
372
|
+
"Saving\u2026": "Enregistrement\u2026",
|
|
373
|
+
"script not allowed": "script interdit",
|
|
374
|
+
"Search variables\u2026": "Rechercher une variable\u2026",
|
|
375
|
+
"Section": "Section",
|
|
376
|
+
"Select a text or a button first.": "S\xE9lectionnez d\u2019abord un texte ou un bouton.",
|
|
377
|
+
"Send": "Envoyer",
|
|
378
|
+
"Send a test": "Envoyer un test",
|
|
379
|
+
"Shown after the subject in the inbox": "Affich\xE9 apr\xE8s l\u2019objet dans la bo\xEEte de r\xE9ception",
|
|
380
|
+
"Size": "Taille",
|
|
381
|
+
"Solid": "Plein",
|
|
382
|
+
"Spacer": "Espace",
|
|
383
|
+
"Spacing": "Espacement",
|
|
384
|
+
"Start over": "Partir de z\xE9ro",
|
|
385
|
+
"style sheet not allowed": "feuille de style interdite",
|
|
386
|
+
"Subject": "Objet",
|
|
387
|
+
"SVG not allowed": "SVG interdit",
|
|
388
|
+
"Tag": "Balise",
|
|
389
|
+
"tag not allowed: <{tag}>": "balise non admise : <{tag}>",
|
|
390
|
+
"Template": "Mod\xE8le",
|
|
391
|
+
"Template changed in the meantime": "Mod\xE8le modifi\xE9 entre-temps",
|
|
392
|
+
"Template changed in the meantime \u2014 reload it before saving": "Mod\xE8le modifi\xE9 entre-temps \u2014 rechargez-le avant d\u2019enregistrer",
|
|
393
|
+
"Template changed in the meantime by {user} \u2014 reload it before saving": "Mod\xE8le modifi\xE9 entre-temps par {user} \u2014 rechargez-le avant d\u2019enregistrer",
|
|
394
|
+
"Template not found": "Mod\xE8le introuvable",
|
|
395
|
+
"Template rejected: {issues}": "Mod\xE8le refus\xE9 : {issues}",
|
|
396
|
+
"Template rejected: {issues} (+{more})": "Mod\xE8le refus\xE9 : {issues} (+{more})",
|
|
397
|
+
"Template saved.": "Mod\xE8le enregistr\xE9.",
|
|
398
|
+
"Templates that use it will show its \u201Cvalue if empty\u201D.": "Les mod\xE8les qui l\u2019utilisent afficheront sa \xAB valeur si vide \xBB.",
|
|
399
|
+
"Test": "Tester",
|
|
400
|
+
"Test sending is not configured": "L\u2019envoi de test n\u2019est pas configur\xE9",
|
|
401
|
+
"Text": "Texte",
|
|
402
|
+
"The builder could not load.": "Le constructeur n\u2019a pas pu se charger.",
|
|
403
|
+
"The builder could not start.": "Le constructeur n\u2019a pas pu d\xE9marrer.",
|
|
404
|
+
"the document must start with <mjml>": "le document doit commencer par <mjml>",
|
|
405
|
+
"The draft will be offered again next time.": "Le brouillon reste propos\xE9 \xE0 la prochaine ouverture.",
|
|
406
|
+
"The email on screen, to reviewers\u2019 addresses.": "Le courriel \xE0 l\u2019\xE9cran, aux adresses de relecture.",
|
|
407
|
+
"The template could not be loaded.": "Le mod\xE8le n\u2019a pas pu \xEAtre charg\xE9.",
|
|
408
|
+
"Thickness": "\xC9paisseur",
|
|
409
|
+
"This key is already used": "Cette balise existe d\xE9j\xE0",
|
|
410
|
+
"To check": "\xC0 v\xE9rifier",
|
|
411
|
+
"Too many test sends \u2014 try again later": "Trop d\u2019envois de test \u2014 r\xE9essayez plus tard",
|
|
412
|
+
"Too many uploads \u2014 try again later": "Trop de d\xE9p\xF4ts d\u2019images \u2014 r\xE9essayez plus tard",
|
|
413
|
+
"Top": "Haut",
|
|
414
|
+
"Typography": "Typographie",
|
|
415
|
+
"Undo": "Annuler",
|
|
416
|
+
"Undo (Ctrl+Z)": "Annuler (Ctrl+Z)",
|
|
417
|
+
"Unknown variables (rendered empty): {tags}": "Variables inconnues (affich\xE9es vides) : {tags}",
|
|
418
|
+
"Unsaved changes": "Modifications non enregistr\xE9es",
|
|
419
|
+
"Unsaved changes will be lost.": "Les modifications non enregistr\xE9es seront perdues.",
|
|
420
|
+
"Unsaved changes will be replaced.": "Les modifications non enregistr\xE9es seront remplac\xE9es.",
|
|
421
|
+
"Unsaved template from {when}.": "Mod\xE8le non enregistr\xE9 du {when}.",
|
|
422
|
+
"Unsubscribe": "Se d\xE9sinscrire",
|
|
423
|
+
"Unsubscribe link": "Lien de d\xE9sinscription",
|
|
424
|
+
"Upload": "D\xE9poser",
|
|
425
|
+
"Upload failed": "D\xE9p\xF4t impossible",
|
|
426
|
+
"URL rejected ({name})": "adresse refus\xE9e ({name})",
|
|
427
|
+
"Value if empty": "Valeur si vide",
|
|
428
|
+
"Variable \u201C{name}\u201D created and inserted.": "Variable \xAB {name} \xBB cr\xE9\xE9e et ins\xE9r\xE9e.",
|
|
429
|
+
"Variable \u201C{name}\u201D created.": "Variable \xAB {name} \xBB cr\xE9\xE9e.",
|
|
430
|
+
"Variable \u201C{name}\u201D saved.": "Variable \xAB {name} \xBB enregistr\xE9e.",
|
|
431
|
+
"Variable deleted.": "Variable supprim\xE9e.",
|
|
432
|
+
"Variable not found": "Variable introuvable",
|
|
433
|
+
"Variables": "Variables",
|
|
434
|
+
"Version {n}": "Version {n}",
|
|
435
|
+
"Version {n} restored \u2014 save to keep it.": "Version {n} recharg\xE9e \u2014 enregistrez pour la garder.",
|
|
436
|
+
"Version not found": "Version introuvable",
|
|
437
|
+
"Versions": "Versions",
|
|
438
|
+
"Vertical alignment": "Alignement vertical",
|
|
439
|
+
"Vertical space": "Vide vertical",
|
|
440
|
+
"Weight": "Graisse",
|
|
441
|
+
"When a recipient has none": "Quand un destinataire n\u2019en a pas",
|
|
442
|
+
"Width": "Largeur",
|
|
443
|
+
"Your company": "Votre soci\xE9t\xE9",
|
|
444
|
+
"Your headline": "Titre du message",
|
|
445
|
+
"Your text.": "Votre texte.",
|
|
446
|
+
"\u201C{name}\u201D template applied.": "Mod\xE8le \xAB {name} \xBB appliqu\xE9.",
|
|
447
|
+
"\u20AC49 instead of \u20AC70": "49 \u20AC au lieu de 70 \u20AC",
|
|
448
|
+
"Cancelled": "Annul\xE9",
|
|
449
|
+
"Email generation is not configured": "La g\xE9n\xE9ration d'e-mails n'est pas configur\xE9e",
|
|
450
|
+
"Too many generated emails \u2014 try again later": "Trop d'e-mails g\xE9n\xE9r\xE9s \u2014 r\xE9essayez plus tard",
|
|
451
|
+
"The AI service did not answer \u2014 try again in a moment.": "Le service d'IA n'a pas r\xE9pondu \u2014 r\xE9essayez dans un instant.",
|
|
452
|
+
"Color scheme": "Th\xE8me de couleurs",
|
|
453
|
+
"Light mode": "Mode clair",
|
|
454
|
+
"Dark mode": "Mode sombre",
|
|
455
|
+
"Dark mode as Gmail and Outlook apps show it: light backgrounds turn dark, dark text turns light, images stay as they are.": "Mode sombre tel que l'affichent les applis Gmail et Outlook : les fonds clairs deviennent sombres, le texte fonc\xE9 devient clair, les images restent telles quelles.",
|
|
456
|
+
"Subject ideas are not configured": "Les id\xE9es d'objet ne sont pas configur\xE9es",
|
|
457
|
+
"Too many subject ideas \u2014 try again later": "Trop de demandes d'id\xE9es d'objet \u2014 r\xE9essayez plus tard",
|
|
458
|
+
"Text rewriting is not configured": "La r\xE9\xE9criture de texte n'est pas configur\xE9e",
|
|
459
|
+
"Too many rewrites \u2014 try again later": "Trop de r\xE9\xE9critures \u2014 r\xE9essayez plus tard",
|
|
460
|
+
"Brand": "Marque",
|
|
461
|
+
"Your brand": "Votre marque",
|
|
462
|
+
"Set it once: new emails, templates and blocks take your logo, colors and font.": "\xC0 d\xE9finir une fois : les nouveaux e-mails, mod\xE8les et blocs prennent votre logo, vos couleurs et votre police.",
|
|
463
|
+
"Your website": "Votre site web",
|
|
464
|
+
"Get my brand": "R\xE9cup\xE9rer ma marque",
|
|
465
|
+
"Reading the website\u2026": "Lecture du site\u2026",
|
|
466
|
+
"The logo, colors and font are read from it. You can change them before saving.": "Le logo, les couleurs et la police y sont lus. Vous pourrez les modifier avant d'enregistrer.",
|
|
467
|
+
"The website could not be read.": "Le site n'a pas pu \xEAtre lu.",
|
|
468
|
+
"Logo": "Logo",
|
|
469
|
+
"Remove": "Retirer",
|
|
470
|
+
"or an image address: https://\u2026": "ou l'adresse d'une image : https://\u2026",
|
|
471
|
+
"Company name": "Nom de l'entreprise",
|
|
472
|
+
"Main color": "Couleur principale",
|
|
473
|
+
"Buttons, banners, links": "Boutons, bandeaux, liens",
|
|
474
|
+
"Headings color": "Couleur des titres",
|
|
475
|
+
"Also apply to this email": "Appliquer aussi \xE0 cet e-mail",
|
|
476
|
+
"Brand applied. Ctrl+Z brings back the previous look.": "Marque appliqu\xE9e. Ctrl+Z r\xE9tablit l'apparence pr\xE9c\xE9dente.",
|
|
477
|
+
"Brand saved. New emails and blocks use it.": "Marque enregistr\xE9e. Les nouveaux e-mails et blocs l'utilisent.",
|
|
478
|
+
"The brand could not be saved.": "La marque n'a pas pu \xEAtre enregistr\xE9e.",
|
|
479
|
+
"Big news": "Grande nouvelle",
|
|
480
|
+
"A short line under the title.": "Une courte ligne sous le titre.",
|
|
481
|
+
"A headline": "Un titre",
|
|
482
|
+
"Some text, with": "Un peu de texte, avec",
|
|
483
|
+
"a link": "un lien",
|
|
484
|
+
"The brand is not configured": "La marque n'est pas configur\xE9e",
|
|
485
|
+
"Reading a brand from a website is not configured": "La lecture d'une marque depuis un site n'est pas configur\xE9e",
|
|
486
|
+
"Too many websites read \u2014 try again later": "Trop de sites lus \u2014 r\xE9essayez plus tard",
|
|
487
|
+
"Enter the address of a website, like acme.com": "Saisissez l'adresse d'un site, par exemple acme.com",
|
|
488
|
+
"This address is not a public website": "Cette adresse n'est pas un site public",
|
|
489
|
+
"The website could not be reached. Check the address.": "Le site est injoignable. V\xE9rifiez l'adresse.",
|
|
490
|
+
"The website took too long to answer": "Le site a mis trop de temps \xE0 r\xE9pondre",
|
|
491
|
+
"The website does not let the builder read it. Fill in your brand below.": "Le site ne se laisse pas lire par l'\xE9diteur. Renseignez votre marque ci-dessous.",
|
|
492
|
+
"This address is not a web page": "Cette adresse n'est pas une page web",
|
|
493
|
+
"The website redirects too many times": "Le site redirige trop de fois",
|
|
494
|
+
"The website sent too much data": "Le site a envoy\xE9 trop de donn\xE9es",
|
|
495
|
+
"No logo was found on the website. Upload it below.": "Aucun logo trouv\xE9 sur le site. Importez-le ci-dessous.",
|
|
496
|
+
"The logo on the website is an SVG or WebP image, which most email apps do not show. Upload a PNG or JPEG version.": "Le logo du site est une image SVG ou WebP, que la plupart des messageries n'affichent pas. Importez une version PNG ou JPEG.",
|
|
497
|
+
"The website icon is used as the logo. Upload your full logo if you have one.": "L'ic\xF4ne du site sert de logo. Importez votre logo complet si vous en avez un.",
|
|
498
|
+
"No main color was found on the website. Pick one below.": "Aucune couleur principale trouv\xE9e sur le site. Choisissez-en une ci-dessous."
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/i18n/index.js
|
|
502
|
+
var LOCALES = (
|
|
503
|
+
/** @type {Record<string, Messages>} */
|
|
504
|
+
{ en: {}, fr }
|
|
505
|
+
);
|
|
506
|
+
function createTranslator(messages = {}) {
|
|
507
|
+
return (text, params) => {
|
|
508
|
+
const template = Object.prototype.hasOwnProperty.call(messages, text) ? messages[text] : text;
|
|
509
|
+
if (!params) return template;
|
|
510
|
+
return template.replace(
|
|
511
|
+
/\{(\w+)\}/g,
|
|
512
|
+
(match, key) => params[key] === void 0 || params[key] === null ? match : String(params[key])
|
|
513
|
+
);
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function translatorFor(locale = "en", overrides = {}) {
|
|
517
|
+
return createTranslator({ ...messagesOf(LOCALES, locale), ...overrides });
|
|
518
|
+
}
|
|
519
|
+
function messagesOf(dictionaries, locale = "en") {
|
|
520
|
+
if (!dictionaries) return {};
|
|
521
|
+
const code = String(locale).toLowerCase();
|
|
522
|
+
const own = (key) => Object.prototype.hasOwnProperty.call(dictionaries, key) ? dictionaries[key] : null;
|
|
523
|
+
return own(code) || own(code.slice(0, 2)) || {};
|
|
524
|
+
}
|
|
525
|
+
var identity = createTranslator();
|
|
526
|
+
|
|
527
|
+
// src/engine/model.js
|
|
528
|
+
var ENDING_TAGS = /* @__PURE__ */ new Set([
|
|
529
|
+
"mj-text",
|
|
530
|
+
"mj-button",
|
|
531
|
+
"mj-navbar-link",
|
|
532
|
+
"mj-social-element",
|
|
533
|
+
"mj-table",
|
|
534
|
+
"mj-raw",
|
|
535
|
+
"mj-title",
|
|
536
|
+
"mj-preview",
|
|
537
|
+
"mj-style",
|
|
538
|
+
"mj-accordion-title",
|
|
539
|
+
"mj-accordion-text"
|
|
540
|
+
]);
|
|
541
|
+
var SECTION_TAGS = /* @__PURE__ */ new Set(["mj-section", "mj-wrapper", "mj-hero"]);
|
|
542
|
+
var LOCKED_TAGS = /* @__PURE__ */ new Set(["mjml", "mj-head", "mj-body"]);
|
|
543
|
+
var counter = 0;
|
|
544
|
+
function createId() {
|
|
545
|
+
counter = (counter + 1) % 1679616;
|
|
546
|
+
return `n${Date.now().toString(36).slice(-5)}${counter.toString(36).padStart(4, "0")}${Math.floor(Math.random() * 1296).toString(36).padStart(2, "0")}`;
|
|
547
|
+
}
|
|
548
|
+
function createNode(tag, { attributes = {}, children = [], content = null, id = createId() } = {}) {
|
|
549
|
+
return { id, tag, attributes: { ...attributes }, children: [...children], content: ENDING_TAGS.has(tag) ? String(content ?? "") : null };
|
|
550
|
+
}
|
|
551
|
+
function cloneWithNewIds(node) {
|
|
552
|
+
return {
|
|
553
|
+
id: createId(),
|
|
554
|
+
tag: node.tag,
|
|
555
|
+
attributes: { ...node.attributes },
|
|
556
|
+
children: node.children.map(cloneWithNewIds),
|
|
557
|
+
content: node.content
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function pathTo(root, id) {
|
|
561
|
+
if (root.id === id) return [root];
|
|
562
|
+
for (const child of root.children) {
|
|
563
|
+
const path = pathTo(child, id);
|
|
564
|
+
if (path) return [root, ...path];
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
function findNode(root, id) {
|
|
569
|
+
if (!id) return null;
|
|
570
|
+
const path = pathTo(root, id);
|
|
571
|
+
return path ? path[path.length - 1] : null;
|
|
572
|
+
}
|
|
573
|
+
function parentOf(root, id) {
|
|
574
|
+
const path = pathTo(root, id);
|
|
575
|
+
return path && path.length > 1 ? path[path.length - 2] : null;
|
|
576
|
+
}
|
|
577
|
+
function closestOfTags(root, id, tags) {
|
|
578
|
+
const set = tags instanceof Set ? tags : new Set(tags);
|
|
579
|
+
const path = pathTo(root, id);
|
|
580
|
+
if (!path) return null;
|
|
581
|
+
for (let i = path.length - 1; i >= 0; i -= 1) if (set.has(path[i].tag)) return path[i];
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
function findFirst(root, tag) {
|
|
585
|
+
if (root.tag === tag) return root;
|
|
586
|
+
for (const child of root.children) {
|
|
587
|
+
const found = findFirst(child, tag);
|
|
588
|
+
if (found) return found;
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
function updateNode(root, id, update) {
|
|
593
|
+
if (root.id === id) return update(root);
|
|
594
|
+
let changed = false;
|
|
595
|
+
const children = root.children.map((child) => {
|
|
596
|
+
const next = updateNode(child, id, update);
|
|
597
|
+
if (next !== child) changed = true;
|
|
598
|
+
return next;
|
|
599
|
+
});
|
|
600
|
+
return changed ? { ...root, children } : root;
|
|
601
|
+
}
|
|
602
|
+
function setAttributes(root, id, attributes) {
|
|
603
|
+
return updateNode(root, id, (node) => {
|
|
604
|
+
const next = { ...node.attributes };
|
|
605
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
606
|
+
if (value === null || value === void 0 || value === "") delete next[key];
|
|
607
|
+
else next[key] = String(value);
|
|
608
|
+
}
|
|
609
|
+
return { ...node, attributes: next };
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function setContent(root, id, content) {
|
|
613
|
+
return updateNode(root, id, (node) => node.content === null ? node : { ...node, content: String(content ?? "") });
|
|
614
|
+
}
|
|
615
|
+
function insertNodes(root, parentId, index, nodes) {
|
|
616
|
+
return updateNode(root, parentId, (parent) => {
|
|
617
|
+
const at = Math.max(0, Math.min(index, parent.children.length));
|
|
618
|
+
const children = [...parent.children];
|
|
619
|
+
children.splice(at, 0, ...nodes);
|
|
620
|
+
return { ...parent, children };
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function removeNode(root, id) {
|
|
624
|
+
const parent = parentOf(root, id);
|
|
625
|
+
if (!parent) return root;
|
|
626
|
+
return updateNode(root, parent.id, (p) => ({ ...p, children: p.children.filter((c) => c.id !== id) }));
|
|
627
|
+
}
|
|
628
|
+
function moveNode(root, id, parentId, index) {
|
|
629
|
+
const node = findNode(root, id);
|
|
630
|
+
const from = parentOf(root, id);
|
|
631
|
+
if (!node || !from) return root;
|
|
632
|
+
if (pathTo(node, parentId)) return root;
|
|
633
|
+
let at = index;
|
|
634
|
+
if (from.id === parentId) {
|
|
635
|
+
const current = from.children.findIndex((c) => c.id === id);
|
|
636
|
+
if (current < at) at -= 1;
|
|
637
|
+
if (current === at) return root;
|
|
638
|
+
}
|
|
639
|
+
return insertNodes(removeNode(root, id), parentId, at, [node]);
|
|
640
|
+
}
|
|
641
|
+
function positionOf(root, id) {
|
|
642
|
+
const parent = parentOf(root, id);
|
|
643
|
+
if (!parent) return { parent: null, index: -1, count: 0 };
|
|
644
|
+
return { parent, index: parent.children.findIndex((c) => c.id === id), count: parent.children.length };
|
|
645
|
+
}
|
|
646
|
+
var PROJECT_FORMAT = 1;
|
|
647
|
+
function toProject(root) {
|
|
648
|
+
return { format: PROJECT_FORMAT, root: strip(root) };
|
|
649
|
+
}
|
|
650
|
+
function strip(node) {
|
|
651
|
+
const out = { id: node.id, tag: node.tag };
|
|
652
|
+
if (Object.keys(node.attributes).length) out.attributes = { ...node.attributes };
|
|
653
|
+
if (node.content !== null) out.content = node.content;
|
|
654
|
+
if (node.children.length) out.children = node.children.map(strip);
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
function fromProject(project) {
|
|
658
|
+
const value = (
|
|
659
|
+
/** @type {any} */
|
|
660
|
+
project
|
|
661
|
+
);
|
|
662
|
+
if (!value || typeof value !== "object" || value.format !== PROJECT_FORMAT || !value.root) {
|
|
663
|
+
throw new Error("Not a project of this builder");
|
|
664
|
+
}
|
|
665
|
+
const seen = /* @__PURE__ */ new Set();
|
|
666
|
+
const read = (raw) => {
|
|
667
|
+
if (!raw || typeof raw !== "object" || typeof raw.tag !== "string" || !/^(mjml|mj-[a-z-]+)$/.test(raw.tag)) {
|
|
668
|
+
throw new Error("Invalid element in the project");
|
|
669
|
+
}
|
|
670
|
+
let id = typeof raw.id === "string" && /^[a-z0-9]{1,40}$/.test(raw.id) ? raw.id : createId();
|
|
671
|
+
if (seen.has(id)) id = createId();
|
|
672
|
+
seen.add(id);
|
|
673
|
+
const attributes = {};
|
|
674
|
+
for (const [k, v] of Object.entries(raw.attributes || {})) {
|
|
675
|
+
if (/^[a-z][a-z0-9-]*$/i.test(k) && v !== null && v !== void 0) attributes[k] = String(v);
|
|
676
|
+
}
|
|
677
|
+
return createNode(raw.tag, {
|
|
678
|
+
id,
|
|
679
|
+
attributes,
|
|
680
|
+
content: ENDING_TAGS.has(raw.tag) ? String(raw.content ?? "") : null,
|
|
681
|
+
children: ENDING_TAGS.has(raw.tag) ? [] : (Array.isArray(raw.children) ? raw.children : []).map(read)
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
const root = read(value.root);
|
|
685
|
+
if (root.tag !== "mjml" || !findFirst(root, "mj-body")) throw new Error("The project has no email body");
|
|
686
|
+
return root;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/engine/mjml.js
|
|
690
|
+
var NAMED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\xA0" };
|
|
691
|
+
function decodeAttribute(value) {
|
|
692
|
+
return value.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (match, entity) => {
|
|
693
|
+
if (entity[0] === "#") {
|
|
694
|
+
const code = entity[1] === "x" || entity[1] === "X" ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
|
|
695
|
+
return Number.isFinite(code) && code > 0 && code < 1114112 ? String.fromCodePoint(code) : match;
|
|
696
|
+
}
|
|
697
|
+
const named = (
|
|
698
|
+
/** @type {Record<string, string>} */
|
|
699
|
+
NAMED_ENTITIES[entity.toLowerCase()]
|
|
700
|
+
);
|
|
701
|
+
return named ?? match;
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function encodeAttribute(value) {
|
|
705
|
+
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
706
|
+
}
|
|
707
|
+
function readAttributes(source, start) {
|
|
708
|
+
const attributes = {};
|
|
709
|
+
let i = start;
|
|
710
|
+
const n = source.length;
|
|
711
|
+
while (i < n) {
|
|
712
|
+
while (i < n && /\s/.test(source[i])) i += 1;
|
|
713
|
+
if (source[i] === ">") return { attributes, end: i + 1, selfClosing: false };
|
|
714
|
+
if (source[i] === "/" && source[i + 1] === ">") return { attributes, end: i + 2, selfClosing: true };
|
|
715
|
+
if (i >= n) break;
|
|
716
|
+
const nameMatch = /^[^\s=/>"']+/.exec(source.slice(i, i + 200));
|
|
717
|
+
if (!nameMatch) {
|
|
718
|
+
i += 1;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const name = nameMatch[0].toLowerCase();
|
|
722
|
+
i += nameMatch[0].length;
|
|
723
|
+
while (i < n && /\s/.test(source[i])) i += 1;
|
|
724
|
+
let value = "";
|
|
725
|
+
if (source[i] === "=") {
|
|
726
|
+
i += 1;
|
|
727
|
+
while (i < n && /\s/.test(source[i])) i += 1;
|
|
728
|
+
const quote = source[i];
|
|
729
|
+
if (quote === '"' || quote === "'") {
|
|
730
|
+
const close = source.indexOf(quote, i + 1);
|
|
731
|
+
const stop = close === -1 ? n : close;
|
|
732
|
+
value = source.slice(i + 1, stop);
|
|
733
|
+
i = stop + 1;
|
|
734
|
+
} else {
|
|
735
|
+
let bare = /^[^\s>]*/.exec(source.slice(i))?.[0] || "";
|
|
736
|
+
if (bare.endsWith("/") && source[i + bare.length] === ">") bare = bare.slice(0, -1);
|
|
737
|
+
value = bare;
|
|
738
|
+
i += bare.length;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
attributes[name] = decodeAttribute(value);
|
|
742
|
+
}
|
|
743
|
+
return { attributes, end: n, selfClosing: false };
|
|
744
|
+
}
|
|
745
|
+
function parseMjmlFragment(source) {
|
|
746
|
+
const text = String(source || "").replace(/^\uFEFF/, "");
|
|
747
|
+
const top = createNode("mj-fragment");
|
|
748
|
+
const stack = [top];
|
|
749
|
+
let i = 0;
|
|
750
|
+
const n = text.length;
|
|
751
|
+
while (i < n) {
|
|
752
|
+
const lt = text.indexOf("<", i);
|
|
753
|
+
if (lt === -1) break;
|
|
754
|
+
i = lt;
|
|
755
|
+
if (text.startsWith("<!--", i)) {
|
|
756
|
+
const close = text.indexOf("-->", i + 4);
|
|
757
|
+
i = close === -1 ? n : close + 3;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (text.startsWith("<?", i) || text.startsWith("<!", i)) {
|
|
761
|
+
const close = text.indexOf(">", i);
|
|
762
|
+
i = close === -1 ? n : close + 1;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
const closing = text[i + 1] === "/";
|
|
766
|
+
const nameMatch = /^[a-z][a-z0-9-]*/i.exec(text.slice(i + (closing ? 2 : 1), i + 60));
|
|
767
|
+
if (!nameMatch) {
|
|
768
|
+
i += 1;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const tag = nameMatch[0].toLowerCase();
|
|
772
|
+
const afterName = i + (closing ? 2 : 1) + nameMatch[0].length;
|
|
773
|
+
const isMjml = tag === "mjml" || tag.startsWith("mj-");
|
|
774
|
+
if (closing) {
|
|
775
|
+
const close = text.indexOf(">", afterName);
|
|
776
|
+
i = close === -1 ? n : close + 1;
|
|
777
|
+
if (!isMjml) continue;
|
|
778
|
+
for (let depth = stack.length - 1; depth > 0; depth -= 1) {
|
|
779
|
+
if (stack[depth].tag === tag) {
|
|
780
|
+
stack.length = depth;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const { attributes, end, selfClosing } = readAttributes(text, afterName);
|
|
787
|
+
i = end;
|
|
788
|
+
if (!isMjml) continue;
|
|
789
|
+
const parent = stack[stack.length - 1];
|
|
790
|
+
if (ENDING_TAGS.has(tag)) {
|
|
791
|
+
let content = "";
|
|
792
|
+
if (!selfClosing) {
|
|
793
|
+
const closeRe = new RegExp(`</${tag}\\s*>`, "ig");
|
|
794
|
+
closeRe.lastIndex = i;
|
|
795
|
+
const close = closeRe.exec(text);
|
|
796
|
+
const stop = close ? close.index : n;
|
|
797
|
+
content = text.slice(i, stop).trim();
|
|
798
|
+
i = close ? close.index + close[0].length : n;
|
|
799
|
+
}
|
|
800
|
+
parent.children.push(createNode(tag, { attributes, content }));
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const node = createNode(tag, { attributes });
|
|
804
|
+
parent.children.push(node);
|
|
805
|
+
if (!selfClosing) stack.push(node);
|
|
806
|
+
}
|
|
807
|
+
return top.children;
|
|
808
|
+
}
|
|
809
|
+
function parseMjml(source) {
|
|
810
|
+
const nodes = parseMjmlFragment(source);
|
|
811
|
+
let root = nodes.find((node) => node.tag === "mjml") || null;
|
|
812
|
+
if (!root) {
|
|
813
|
+
const body = nodes.find((node) => node.tag === "mj-body");
|
|
814
|
+
const head = nodes.find((node) => node.tag === "mj-head");
|
|
815
|
+
root = createNode("mjml", {
|
|
816
|
+
children: [
|
|
817
|
+
...head ? [head] : [],
|
|
818
|
+
body || createNode("mj-body", { children: nodes.filter((node) => node.tag !== "mj-head") })
|
|
819
|
+
]
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (!findFirst(root, "mj-body")) root.children.push(createNode("mj-body"));
|
|
823
|
+
return root;
|
|
824
|
+
}
|
|
825
|
+
function serializeMjml(node, options = {}) {
|
|
826
|
+
const { pretty = true } = options;
|
|
827
|
+
const write = (current, depth) => {
|
|
828
|
+
const indent = pretty ? " ".repeat(depth) : "";
|
|
829
|
+
const attributes = options.attributes ? options.attributes(current) : current.attributes;
|
|
830
|
+
const attrs = Object.entries(attributes).map(([key, value]) => ` ${key}="${encodeAttribute(value)}"`).join("");
|
|
831
|
+
if (current.content !== null) {
|
|
832
|
+
return `${indent}<${current.tag}${attrs}>${current.content}</${current.tag}>`;
|
|
833
|
+
}
|
|
834
|
+
const inner = current.children.map((child) => write(child, depth + 1));
|
|
835
|
+
const extra = options.extraChildren ? options.extraChildren(current) : "";
|
|
836
|
+
if (extra) inner.push(pretty ? `${" ".repeat(depth + 1)}${extra}` : extra);
|
|
837
|
+
if (!inner.length) return `${indent}<${current.tag}${attrs} />`;
|
|
838
|
+
return pretty ? `${indent}<${current.tag}${attrs}>
|
|
839
|
+
${inner.join("\n")}
|
|
840
|
+
${indent}</${current.tag}>` : `<${current.tag}${attrs}>${inner.join("")}</${current.tag}>`;
|
|
841
|
+
};
|
|
842
|
+
return write(node, 0);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/blocks/shared.js
|
|
846
|
+
function escapeXml(text) {
|
|
847
|
+
return String(text ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
848
|
+
}
|
|
849
|
+
var PALETTE = {
|
|
850
|
+
ink: "#2b3038",
|
|
851
|
+
inkSoft: "#41506b",
|
|
852
|
+
navy: "#212e5a",
|
|
853
|
+
brand: "#2977bc",
|
|
854
|
+
border: "#dfe6f0",
|
|
855
|
+
canvas: "#eceff4",
|
|
856
|
+
surface: "#ffffff",
|
|
857
|
+
white: "#ffffff"
|
|
858
|
+
};
|
|
859
|
+
var DEFAULT_BUTTON_URL = "https://example.com";
|
|
860
|
+
var FONT_STACK = "Arial, Helvetica, sans-serif";
|
|
861
|
+
var FONT_OPTIONS = [
|
|
862
|
+
{ value: "Arial, Helvetica, sans-serif", label: "Arial" },
|
|
863
|
+
{ value: "Verdana, Geneva, sans-serif", label: "Verdana" },
|
|
864
|
+
{ value: "Tahoma, Geneva, sans-serif", label: "Tahoma" },
|
|
865
|
+
{ value: "'Trebuchet MS', Helvetica, sans-serif", label: "Trebuchet" },
|
|
866
|
+
{ value: "Georgia, Times, serif", label: "Georgia" },
|
|
867
|
+
{ value: "'Times New Roman', Times, serif", label: "Times" },
|
|
868
|
+
{ value: "'Courier New', Courier, monospace", label: "Courier" }
|
|
869
|
+
];
|
|
870
|
+
var ALIGN_OPTIONS = [
|
|
871
|
+
{ value: "left", label: "Left" },
|
|
872
|
+
{ value: "center", label: "Center" },
|
|
873
|
+
{ value: "right", label: "Right" }
|
|
874
|
+
];
|
|
875
|
+
var GROUPS = {
|
|
876
|
+
content: "Content",
|
|
877
|
+
link: "Link",
|
|
878
|
+
typography: "Typography",
|
|
879
|
+
colors: "Colors",
|
|
880
|
+
layout: "Layout",
|
|
881
|
+
spacing: "Spacing"
|
|
882
|
+
};
|
|
883
|
+
var prop = {
|
|
884
|
+
/** @returns {BlockProperty} */
|
|
885
|
+
fontFamily: () => ({ key: "font-family", label: "Font", type: "select", group: GROUPS.typography, options: FONT_OPTIONS }),
|
|
886
|
+
/** @returns {BlockProperty} */
|
|
887
|
+
fontSize: (min = 10, max = 48) => ({ key: "font-size", label: "Size", type: "size", group: GROUPS.typography, min, max, unit: "px" }),
|
|
888
|
+
/** @returns {BlockProperty} */
|
|
889
|
+
lineHeight: () => ({ key: "line-height", label: "Line height", type: "size", group: GROUPS.typography, min: 10, max: 80, unit: "px" }),
|
|
890
|
+
/** @returns {BlockProperty} */
|
|
891
|
+
color: (label = "Text") => ({ key: "color", label, type: "color", group: GROUPS.colors }),
|
|
892
|
+
/** @returns {BlockProperty} */
|
|
893
|
+
background: (key = "background-color", label = "Background") => ({ key, label, type: "color", group: GROUPS.colors }),
|
|
894
|
+
/** @returns {BlockProperty} */
|
|
895
|
+
align: (options = ALIGN_OPTIONS) => ({ key: "align", label: "Alignment", type: "align", group: GROUPS.layout, options }),
|
|
896
|
+
/** @returns {BlockProperty} */
|
|
897
|
+
padding: (key = "padding", label = "Padding") => ({ key, label, type: "spacing", group: GROUPS.spacing }),
|
|
898
|
+
/** @returns {BlockProperty} */
|
|
899
|
+
href: (label = "Link URL") => ({ key: "href", label, type: "url", group: GROUPS.link, placeholder: "https://" }),
|
|
900
|
+
/** @returns {BlockProperty} */
|
|
901
|
+
radius: (max = 40) => ({ key: "border-radius", label: "Corner radius", type: "size", group: GROUPS.layout, min: 0, max, unit: "px" })
|
|
902
|
+
};
|
|
903
|
+
function inWhiteSection(inner) {
|
|
904
|
+
return `<mj-section background-color="${PALETTE.surface}" padding-top="0px" padding-bottom="0px"><mj-column>${inner}</mj-column></mj-section>`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/features/brand/brand.js
|
|
908
|
+
var HEX = /^#[0-9a-f]{6}$/;
|
|
909
|
+
var FONTS = new Set(FONT_OPTIONS.map((option) => option.value));
|
|
910
|
+
var MAX_LOGO_WIDTH = 240;
|
|
911
|
+
function parseColor(value) {
|
|
912
|
+
const source = String(value ?? "").trim().toLowerCase();
|
|
913
|
+
if (source === "white") return { r: 255, g: 255, b: 255, a: 1 };
|
|
914
|
+
if (source === "black") return { r: 0, g: 0, b: 0, a: 1 };
|
|
915
|
+
let match = source.match(/^#([0-9a-f]{3,8})$/);
|
|
916
|
+
if (match) {
|
|
917
|
+
const hex = match[1];
|
|
918
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
919
|
+
const [r, g, b, a = "ff"] = hex.split("").map((c) => c + c);
|
|
920
|
+
return { r: parseInt(r, 16), g: parseInt(g, 16), b: parseInt(b, 16), a: parseInt(a, 16) / 255 };
|
|
921
|
+
}
|
|
922
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
923
|
+
return {
|
|
924
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
925
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
926
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
927
|
+
a: hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
match = source.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
|
|
933
|
+
if (!match) return null;
|
|
934
|
+
const parts = match[2].split(/[\s,/]+/).filter(Boolean);
|
|
935
|
+
if (parts.length < 3) return null;
|
|
936
|
+
const alpha = parts[3] === void 0 ? 1 : parts[3].endsWith("%") ? parseFloat(parts[3]) / 100 : parseFloat(parts[3]);
|
|
937
|
+
if (match[1].startsWith("rgb")) {
|
|
938
|
+
const channel = (part) => part.endsWith("%") ? parseFloat(part) * 255 / 100 : parseFloat(part);
|
|
939
|
+
const [r, g, b] = parts.slice(0, 3).map(channel);
|
|
940
|
+
if ([r, g, b, alpha].some((n) => Number.isNaN(n))) return null;
|
|
941
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
942
|
+
return { r: clamp(r), g: clamp(g), b: clamp(b), a: alpha };
|
|
943
|
+
}
|
|
944
|
+
const h = parseFloat(parts[0]);
|
|
945
|
+
const s = parseFloat(parts[1]) / 100;
|
|
946
|
+
const l = parseFloat(parts[2]) / 100;
|
|
947
|
+
if ([h, s, l, alpha].some((n) => Number.isNaN(n))) return null;
|
|
948
|
+
return { ...fromHsl((h % 360 + 360) % 360, Math.max(0, Math.min(1, s)), Math.max(0, Math.min(1, l))), a: alpha };
|
|
949
|
+
}
|
|
950
|
+
function fromHsl(h, s, l) {
|
|
951
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
952
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
953
|
+
const m = l - c / 2;
|
|
954
|
+
const [r, g, b] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
|
|
955
|
+
return { r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255) };
|
|
956
|
+
}
|
|
957
|
+
var toHex = ({ r, g, b }) => `#${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
|
|
958
|
+
function toHsl({ r, g, b }) {
|
|
959
|
+
const [R, G, B] = [r / 255, g / 255, b / 255];
|
|
960
|
+
const max = Math.max(R, G, B);
|
|
961
|
+
const min = Math.min(R, G, B);
|
|
962
|
+
const l = (max + min) / 2;
|
|
963
|
+
if (max === min) return { h: 0, s: 0, l };
|
|
964
|
+
const d = max - min;
|
|
965
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
966
|
+
const h = max === R ? ((G - B) / d + (G < B ? 6 : 0)) * 60 : max === G ? ((B - R) / d + 2) * 60 : ((R - G) / d + 4) * 60;
|
|
967
|
+
return { h, s, l };
|
|
968
|
+
}
|
|
969
|
+
function luminance({ r, g, b }) {
|
|
970
|
+
const linear = (n) => {
|
|
971
|
+
const c = n / 255;
|
|
972
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
973
|
+
};
|
|
974
|
+
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
|
975
|
+
}
|
|
976
|
+
function contrastRatio(a, b) {
|
|
977
|
+
const ca = parseColor(a);
|
|
978
|
+
const cb = parseColor(b);
|
|
979
|
+
if (!ca || !cb) return 1;
|
|
980
|
+
const [hi, lo] = [luminance(ca), luminance(cb)].sort((x, y) => y - x);
|
|
981
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
982
|
+
}
|
|
983
|
+
function readableOn(background) {
|
|
984
|
+
return contrastRatio(background, PALETTE.white) >= contrastRatio(background, PALETTE.ink) ? PALETTE.white : PALETTE.ink;
|
|
985
|
+
}
|
|
986
|
+
function isColored(value) {
|
|
987
|
+
const color = parseColor(value);
|
|
988
|
+
if (!color || color.a < 0.5) return false;
|
|
989
|
+
const { s, l } = toHsl(color);
|
|
990
|
+
return s >= 0.2 && l >= 0.08 && l <= 0.88;
|
|
991
|
+
}
|
|
992
|
+
function isDark(value) {
|
|
993
|
+
const color = parseColor(value);
|
|
994
|
+
return Boolean(color && color.a >= 0.5 && luminance(color) < 0.12);
|
|
995
|
+
}
|
|
996
|
+
function isMuted(value) {
|
|
997
|
+
const color = parseColor(value);
|
|
998
|
+
if (!color) return false;
|
|
999
|
+
const { s, l } = toHsl(color);
|
|
1000
|
+
return s < 0.2 && l > 0.4;
|
|
1001
|
+
}
|
|
1002
|
+
var cleanColor = (value) => {
|
|
1003
|
+
const color = parseColor(value);
|
|
1004
|
+
return color && color.a >= 0.5 ? toHex(color) : "";
|
|
1005
|
+
};
|
|
1006
|
+
function cleanBrand(input) {
|
|
1007
|
+
const source = (
|
|
1008
|
+
/** @type {Record<string, unknown>} */
|
|
1009
|
+
input && typeof input === "object" ? input : {}
|
|
1010
|
+
);
|
|
1011
|
+
const brand = {};
|
|
1012
|
+
const name = typeof source.name === "string" ? source.name.replace(/\s+/g, " ").trim().slice(0, 80) : "";
|
|
1013
|
+
if (name) brand.name = name;
|
|
1014
|
+
const logo = typeof source.logoUrl === "string" ? source.logoUrl.trim() : "";
|
|
1015
|
+
if (logo && logo.length <= 2e3 && (/^https:\/\/[^\s"'<>]+$/i.test(logo) || /^\/(?!\/)[^\s"'<>]*$/.test(logo))) {
|
|
1016
|
+
brand.logoUrl = logo;
|
|
1017
|
+
const width = Math.round(Number(source.logoWidth));
|
|
1018
|
+
if (width >= 24 && width <= MAX_LOGO_WIDTH) brand.logoWidth = width;
|
|
1019
|
+
}
|
|
1020
|
+
const primary = cleanColor(source.primary);
|
|
1021
|
+
if (primary && HEX.test(primary)) brand.primary = primary;
|
|
1022
|
+
const heading = cleanColor(source.heading);
|
|
1023
|
+
if (heading && HEX.test(heading)) brand.heading = heading;
|
|
1024
|
+
if (typeof source.font === "string" && FONTS.has(source.font)) brand.font = source.font;
|
|
1025
|
+
const website = typeof source.website === "string" ? source.website.trim() : "";
|
|
1026
|
+
if (/^https?:\/\/[^\s"'<>]+$/i.test(website) && website.length <= 500) brand.website = website;
|
|
1027
|
+
return brand;
|
|
1028
|
+
}
|
|
1029
|
+
var hasBrand = (brand) => Boolean(brand && Object.keys(cleanBrand(brand)).some((key) => key !== "website"));
|
|
1030
|
+
function brandedSender(sender, brand) {
|
|
1031
|
+
const base = { ...sender || {} };
|
|
1032
|
+
if (!brand) return base;
|
|
1033
|
+
if (brand.name) base.name = brand.name;
|
|
1034
|
+
if (brand.logoUrl) {
|
|
1035
|
+
base.logoUrl = brand.logoUrl;
|
|
1036
|
+
base.logoWidth = brand.logoWidth || base.logoWidth;
|
|
1037
|
+
}
|
|
1038
|
+
return base;
|
|
1039
|
+
}
|
|
1040
|
+
function paletteOf(brand) {
|
|
1041
|
+
const primary = brand.primary || null;
|
|
1042
|
+
return {
|
|
1043
|
+
primary,
|
|
1044
|
+
onPrimary: primary ? readableOn(primary) : null,
|
|
1045
|
+
heading: brand.heading || null,
|
|
1046
|
+
// A link must read on white: a too-light main color gives way.
|
|
1047
|
+
link: primary ? contrastRatio(primary, PALETTE.white) >= 3 ? primary : brand.heading || PALETTE.ink : null,
|
|
1048
|
+
font: brand.font || null
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
var SECTION_LIKE = /* @__PURE__ */ new Set(["mj-section", "mj-wrapper", "mj-hero", "mj-column", "mj-group"]);
|
|
1052
|
+
var FONT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-navbar-link", "mj-social-element", "mj-table"]);
|
|
1053
|
+
var px = (size) => {
|
|
1054
|
+
const n = parseFloat(String(size ?? ""));
|
|
1055
|
+
return Number.isNaN(n) ? 0 : n;
|
|
1056
|
+
};
|
|
1057
|
+
var isBold = (weight) => /^(bold|bolder|[6-9]00)$/.test(String(weight ?? "").trim());
|
|
1058
|
+
function recolorLinks(html, color) {
|
|
1059
|
+
return html.replace(
|
|
1060
|
+
/<a\b[^>]*>/gi,
|
|
1061
|
+
(tag) => tag.replace(
|
|
1062
|
+
/(style\s*=\s*")([^"]*)(")/i,
|
|
1063
|
+
(_, open, style, close) => `${open}${style.replace(
|
|
1064
|
+
/(^|;)(\s*color\s*:\s*)([^;]+)/i,
|
|
1065
|
+
(whole, sep, prop2, value) => isColored(value) ? `${sep}${prop2}${color}` : whole
|
|
1066
|
+
)}${close}`
|
|
1067
|
+
)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
function paint(node, palette, onColor) {
|
|
1071
|
+
const attributes = { ...node.attributes };
|
|
1072
|
+
let content = node.content;
|
|
1073
|
+
let childrenOnColor = onColor;
|
|
1074
|
+
const set = (key, value) => {
|
|
1075
|
+
if (value) attributes[key] = value;
|
|
1076
|
+
};
|
|
1077
|
+
if (SECTION_LIKE.has(node.tag)) {
|
|
1078
|
+
const background = attributes["background-color"];
|
|
1079
|
+
if (background && palette.primary && (isColored(background) || isDark(background) && node.tag !== "mj-column")) {
|
|
1080
|
+
attributes["background-color"] = palette.primary;
|
|
1081
|
+
childrenOnColor = true;
|
|
1082
|
+
} else if (background && parseColor(background)) {
|
|
1083
|
+
childrenOnColor = false;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (FONT_TAGS.has(node.tag) && palette.font) set("font-family", palette.font);
|
|
1087
|
+
if (node.tag === "mj-text" && content !== null) {
|
|
1088
|
+
if (onColor) {
|
|
1089
|
+
set("color", palette.onPrimary);
|
|
1090
|
+
} else {
|
|
1091
|
+
const heading = isBold(attributes["font-weight"]) && px(attributes["font-size"]) >= 18;
|
|
1092
|
+
if (heading && palette.heading && !isMuted(attributes.color)) attributes.color = palette.heading;
|
|
1093
|
+
if (palette.link) content = recolorLinks(content, palette.link);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (node.tag === "mj-button") {
|
|
1097
|
+
const background = attributes["background-color"];
|
|
1098
|
+
const filled = !background || isColored(background) || isDark(background);
|
|
1099
|
+
if (palette.primary && filled) {
|
|
1100
|
+
set("background-color", onColor ? palette.onPrimary : palette.primary);
|
|
1101
|
+
set("color", onColor ? palette.primary : palette.onPrimary);
|
|
1102
|
+
} else if (palette.primary && onColor) {
|
|
1103
|
+
set("color", readableOn(background) === PALETTE.ink && contrastRatio(palette.primary, background) < 3 ? PALETTE.ink : palette.primary);
|
|
1104
|
+
} else if (palette.link && isColored(attributes.color)) {
|
|
1105
|
+
attributes.color = palette.link;
|
|
1106
|
+
if (isColored(attributes["border-color"])) attributes["border-color"] = palette.link;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (node.tag === "mj-navbar-link" && isColored(attributes.color)) {
|
|
1110
|
+
set("color", onColor ? palette.onPrimary : palette.link);
|
|
1111
|
+
}
|
|
1112
|
+
const children = node.children.map((child) => paint(child, palette, childrenOnColor));
|
|
1113
|
+
return { ...node, attributes, content, children };
|
|
1114
|
+
}
|
|
1115
|
+
var plainText = (html) => html.replace(/<[^>]*>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/\s+/g, " ").trim();
|
|
1116
|
+
var sectionsOf = (body) => body.children.flatMap((child) => child.tag === "mj-wrapper" ? child.children.filter((c) => c.tag === "mj-section") : [child]);
|
|
1117
|
+
function rebrandHeader(root, brand, previous) {
|
|
1118
|
+
const body = root.children.find((child) => child.tag === "mj-body");
|
|
1119
|
+
if (!body) return root;
|
|
1120
|
+
const names = previous.names.map((name) => String(name || "").trim()).filter((name) => name.length >= 2);
|
|
1121
|
+
const known = new Set(names.map((name) => name.toLowerCase()));
|
|
1122
|
+
const sections = sectionsOf(body);
|
|
1123
|
+
const replaced = /* @__PURE__ */ new Map();
|
|
1124
|
+
const top = sections[0];
|
|
1125
|
+
const column = top?.children.find((child) => child.tag === "mj-column");
|
|
1126
|
+
const first = column?.children[0];
|
|
1127
|
+
if (first?.tag === "mj-image") {
|
|
1128
|
+
const small = px(first.attributes.width) > 0 && px(first.attributes.width) <= MAX_LOGO_WIDTH;
|
|
1129
|
+
const wasLogo = small || previous.logos.includes(first.attributes.src) || known.has(String(first.attributes.alt || "").toLowerCase());
|
|
1130
|
+
if (wasLogo && brand.logoUrl) {
|
|
1131
|
+
replaced.set(first.id, {
|
|
1132
|
+
...first,
|
|
1133
|
+
attributes: {
|
|
1134
|
+
...first.attributes,
|
|
1135
|
+
src: brand.logoUrl,
|
|
1136
|
+
alt: brand.name || first.attributes.alt || "",
|
|
1137
|
+
width: `${brand.logoWidth || Math.min(px(first.attributes.width) || 140, MAX_LOGO_WIDTH)}px`
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
} else if (first?.tag === "mj-text" && first.content !== null && known.has(plainText(first.content).toLowerCase())) {
|
|
1142
|
+
if (brand.logoUrl) {
|
|
1143
|
+
const keep = Object.fromEntries(Object.entries(first.attributes).filter(([key]) => key.startsWith("padding") || key === "align"));
|
|
1144
|
+
replaced.set(
|
|
1145
|
+
first.id,
|
|
1146
|
+
createNode("mj-image", {
|
|
1147
|
+
id: first.id,
|
|
1148
|
+
attributes: { src: brand.logoUrl, alt: brand.name || "", width: `${brand.logoWidth || 140}px`, align: "left", ...keep }
|
|
1149
|
+
})
|
|
1150
|
+
);
|
|
1151
|
+
} else if (brand.name) {
|
|
1152
|
+
replaced.set(first.id, { ...first, content: escapeXml(brand.name) });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const bottom = sections.length > 1 ? sections[sections.length - 1] : null;
|
|
1156
|
+
const newName = brand.name;
|
|
1157
|
+
if (bottom && newName) {
|
|
1158
|
+
const swap = (node) => {
|
|
1159
|
+
if (node.tag === "mj-text" && node.content !== null) {
|
|
1160
|
+
let html = node.content;
|
|
1161
|
+
for (const name of names) {
|
|
1162
|
+
if (name !== newName) html = html.split(escapeXml(name)).join(escapeXml(newName));
|
|
1163
|
+
}
|
|
1164
|
+
if (html !== node.content) replaced.set(node.id, { ...node, content: html });
|
|
1165
|
+
}
|
|
1166
|
+
node.children.forEach(swap);
|
|
1167
|
+
};
|
|
1168
|
+
swap(bottom);
|
|
1169
|
+
}
|
|
1170
|
+
if (!replaced.size) return root;
|
|
1171
|
+
const rebuild = (node) => replaced.get(node.id) || { ...node, children: node.children.map(rebuild) };
|
|
1172
|
+
return rebuild(root);
|
|
1173
|
+
}
|
|
1174
|
+
function applyBrand(mjml, brand, { header = false, previous = {} } = {}) {
|
|
1175
|
+
const clean2 = cleanBrand(brand);
|
|
1176
|
+
if (!hasBrand(clean2)) return mjml;
|
|
1177
|
+
let root = parseMjml(mjml);
|
|
1178
|
+
root = paint(root, paletteOf(clean2), false);
|
|
1179
|
+
if (header) {
|
|
1180
|
+
root = rebrandHeader(root, clean2, {
|
|
1181
|
+
names: (
|
|
1182
|
+
/** @type {string[]} */
|
|
1183
|
+
(previous.names || []).filter(Boolean)
|
|
1184
|
+
),
|
|
1185
|
+
logos: (
|
|
1186
|
+
/** @type {string[]} */
|
|
1187
|
+
(previous.logos || []).filter(Boolean)
|
|
1188
|
+
)
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return serializeMjml(root);
|
|
1192
|
+
}
|
|
1193
|
+
function applyBrandToFragment(mjml, brand) {
|
|
1194
|
+
const clean2 = cleanBrand(brand);
|
|
1195
|
+
if (!mjml || !hasBrand(clean2)) return mjml;
|
|
1196
|
+
const palette = paletteOf(clean2);
|
|
1197
|
+
return parseMjmlFragment(mjml).map((node) => serializeMjml(paint(node, palette, false), { pretty: false })).join("");
|
|
1198
|
+
}
|
|
1199
|
+
function brandTemplate(mjml, brand, { sender = null, t = (key) => key } = {}) {
|
|
1200
|
+
return applyBrand(mjml, brand, {
|
|
1201
|
+
header: true,
|
|
1202
|
+
previous: { names: [sender?.name, t("Your company")], logos: [sender?.logoUrl] }
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/features/variables/catalog.js
|
|
1207
|
+
var DEFAULT_VARIABLES = [
|
|
1208
|
+
{ key: "contact.firstname", label: "First name", group: "Recipient", sample: "Jane" },
|
|
1209
|
+
{ key: "contact.lastname", label: "Last name", group: "Recipient", sample: "Doe" },
|
|
1210
|
+
{ key: "contact.email", label: "Email address", group: "Recipient", sample: "jane.doe@example.com" },
|
|
1211
|
+
{ key: "company.name", label: "Company", group: "Recipient", sample: "Example Inc." },
|
|
1212
|
+
{ key: "campaign.name", label: "Campaign name", group: "Campaign", sample: "Newsletter" },
|
|
1213
|
+
{
|
|
1214
|
+
key: "unsubscribe",
|
|
1215
|
+
label: "Unsubscribe link",
|
|
1216
|
+
group: "Links",
|
|
1217
|
+
sample: "",
|
|
1218
|
+
token: "{{ unsubscribe }}",
|
|
1219
|
+
kind: "link"
|
|
1220
|
+
}
|
|
1221
|
+
];
|
|
1222
|
+
var CUSTOM_GROUP = "Custom";
|
|
1223
|
+
var RESERVED_KEYS = /* @__PURE__ */ new Set(["unsubscribe", "mirror"]);
|
|
1224
|
+
var VARIABLE_LIMITS = { key: 60, label: 80, group: 40, sample: 200, fallback: 200, description: 200 };
|
|
1225
|
+
var KEY_SHAPE = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+)*$/;
|
|
1226
|
+
function tokenOf(variable) {
|
|
1227
|
+
return variable.token || `{{${variable.key}}}`;
|
|
1228
|
+
}
|
|
1229
|
+
function keyFromLabel(label) {
|
|
1230
|
+
const key = String(label || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/^[^a-z]+/, "").slice(0, VARIABLE_LIMITS.key).replace(/_+$/, "");
|
|
1231
|
+
return key || "variable";
|
|
1232
|
+
}
|
|
1233
|
+
function defineVariables(variables) {
|
|
1234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1235
|
+
for (const variable of variables) {
|
|
1236
|
+
if (!variable || !KEY_SHAPE.test(variable.key || "")) {
|
|
1237
|
+
throw new Error(`variables: invalid key "${variable?.key}"`);
|
|
1238
|
+
}
|
|
1239
|
+
if (seen.has(variable.key)) throw new Error(`variables: duplicate key "${variable.key}"`);
|
|
1240
|
+
seen.add(variable.key);
|
|
1241
|
+
}
|
|
1242
|
+
return variables;
|
|
1243
|
+
}
|
|
1244
|
+
var clean = (value, max) => String(value ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
|
1245
|
+
function validateCustomVariable(input, { existing = [], editing = null, t = identity } = {}) {
|
|
1246
|
+
const errors = {};
|
|
1247
|
+
const label = clean(input?.label, VARIABLE_LIMITS.label);
|
|
1248
|
+
const key = editing || String(input?.key || "").trim();
|
|
1249
|
+
if (!label) errors.label = t("Name is required");
|
|
1250
|
+
if (!editing) {
|
|
1251
|
+
if (!key) errors.key = t("Key is required");
|
|
1252
|
+
else if (key.length > VARIABLE_LIMITS.key || !KEY_SHAPE.test(key)) {
|
|
1253
|
+
errors.key = t('Lowercase letters, digits, "_" and "." only, starting with a letter');
|
|
1254
|
+
} else if (RESERVED_KEYS.has(key) || existing.some((v) => v.key === key)) {
|
|
1255
|
+
errors.key = t("This key is already used");
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (Object.keys(errors).length) return { errors };
|
|
1259
|
+
return {
|
|
1260
|
+
variable: {
|
|
1261
|
+
key,
|
|
1262
|
+
label,
|
|
1263
|
+
group: clean(input?.group, VARIABLE_LIMITS.group) || CUSTOM_GROUP,
|
|
1264
|
+
sample: clean(input?.sample, VARIABLE_LIMITS.sample),
|
|
1265
|
+
fallback: clean(input?.fallback, VARIABLE_LIMITS.fallback),
|
|
1266
|
+
description: clean(input?.description, VARIABLE_LIMITS.description),
|
|
1267
|
+
custom: true
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
function mergeVariables(system, custom = []) {
|
|
1272
|
+
const keys = new Set(system.map((v) => v.key));
|
|
1273
|
+
return [...system, ...custom.filter((v) => !keys.has(v.key)).map((v) => ({ ...v, custom: true }))];
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/renderer/links.js
|
|
1277
|
+
var UNSUBSCRIBE = /\{\{\s*unsubscribe\s*\}\}|\[UNSUBSCRIBE\]/gi;
|
|
1278
|
+
var MIRROR = /\{\{\s*mirror\s*\}\}/gi;
|
|
1279
|
+
var DEFAULT_ASSETS_PATH = "/email-assets/";
|
|
1280
|
+
var ASSET_PATH_PATTERN = /^\/(?:[A-Za-z0-9._~-]+\/)*[a-f0-9]{32}\.(png|jpg|gif)$/;
|
|
1281
|
+
var RELATIVE_ASSET = /(^|["'(=\s])(\/(?:[A-Za-z0-9._~-]+\/)*[a-f0-9]{32}\.(?:png|jpg|gif))(?=["')\s>]|$)/g;
|
|
1282
|
+
function isAssetPath(value) {
|
|
1283
|
+
const path = String(value || "").trim();
|
|
1284
|
+
return ASSET_PATH_PATTERN.test(path) && !path.split("/").some((part) => part === ".." || part === ".");
|
|
1285
|
+
}
|
|
1286
|
+
function absoluteAssetUrls(html, baseUrl) {
|
|
1287
|
+
const root = String(baseUrl || "").replace(/\/+$/, "");
|
|
1288
|
+
if (!root) return String(html || "");
|
|
1289
|
+
return String(html || "").replace(
|
|
1290
|
+
RELATIVE_ASSET,
|
|
1291
|
+
(match, before, path) => isAssetPath(path) ? `${before}${root}${path}` : match
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
function escapeAttribute(text) {
|
|
1295
|
+
return String(text ?? "").replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1296
|
+
}
|
|
1297
|
+
function applyServiceLinks(html, { unsubscribeUrl }) {
|
|
1298
|
+
return String(html || "").replace(UNSUBSCRIBE, () => escapeAttribute(unsubscribeUrl)).replace(MIRROR, "#");
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
export {
|
|
1302
|
+
LOCALES,
|
|
1303
|
+
createTranslator,
|
|
1304
|
+
translatorFor,
|
|
1305
|
+
messagesOf,
|
|
1306
|
+
identity,
|
|
1307
|
+
escapeXml,
|
|
1308
|
+
PALETTE,
|
|
1309
|
+
DEFAULT_BUTTON_URL,
|
|
1310
|
+
FONT_STACK,
|
|
1311
|
+
FONT_OPTIONS,
|
|
1312
|
+
GROUPS,
|
|
1313
|
+
prop,
|
|
1314
|
+
inWhiteSection,
|
|
1315
|
+
SECTION_TAGS,
|
|
1316
|
+
LOCKED_TAGS,
|
|
1317
|
+
createNode,
|
|
1318
|
+
cloneWithNewIds,
|
|
1319
|
+
pathTo,
|
|
1320
|
+
findNode,
|
|
1321
|
+
parentOf,
|
|
1322
|
+
closestOfTags,
|
|
1323
|
+
findFirst,
|
|
1324
|
+
setAttributes,
|
|
1325
|
+
setContent,
|
|
1326
|
+
insertNodes,
|
|
1327
|
+
removeNode,
|
|
1328
|
+
moveNode,
|
|
1329
|
+
positionOf,
|
|
1330
|
+
toProject,
|
|
1331
|
+
fromProject,
|
|
1332
|
+
parseMjmlFragment,
|
|
1333
|
+
parseMjml,
|
|
1334
|
+
serializeMjml,
|
|
1335
|
+
parseColor,
|
|
1336
|
+
toHex,
|
|
1337
|
+
toHsl,
|
|
1338
|
+
contrastRatio,
|
|
1339
|
+
readableOn,
|
|
1340
|
+
cleanBrand,
|
|
1341
|
+
hasBrand,
|
|
1342
|
+
brandedSender,
|
|
1343
|
+
applyBrand,
|
|
1344
|
+
applyBrandToFragment,
|
|
1345
|
+
brandTemplate,
|
|
1346
|
+
DEFAULT_VARIABLES,
|
|
1347
|
+
CUSTOM_GROUP,
|
|
1348
|
+
VARIABLE_LIMITS,
|
|
1349
|
+
tokenOf,
|
|
1350
|
+
keyFromLabel,
|
|
1351
|
+
defineVariables,
|
|
1352
|
+
validateCustomVariable,
|
|
1353
|
+
mergeVariables,
|
|
1354
|
+
DEFAULT_ASSETS_PATH,
|
|
1355
|
+
isAssetPath,
|
|
1356
|
+
absoluteAssetUrls,
|
|
1357
|
+
applyServiceLinks
|
|
1358
|
+
};
|
|
1359
|
+
//# sourceMappingURL=chunk-DFNHICTF.js.map
|