@voila.dev/ui-email-block-editor 1.1.9 → 1.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voila.dev/ui-email-block-editor",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "type": "module",
5
5
  "description": "The email template editor that lives in your app, not someone else's SaaS.",
6
6
  "license": "MIT",
@@ -32,18 +32,18 @@ function ArticleBlockView({
32
32
  return (
33
33
  <EmailCardShell image={block.image}>
34
34
  <BlockTextInput
35
- ariaLabel="Titre de l'article"
35
+ ariaLabel="Article title"
36
36
  value={block.title}
37
- placeholder="Titre de l'article"
37
+ placeholder="Article title"
38
38
  onChange={(title) => onChange({ ...block, title })}
39
39
  className="font-bold text-[17px] leading-[1.3]"
40
40
  style={{ color: EMAIL_COLOR.brand }}
41
41
  />
42
42
  {meta === "" ? null : <EmailCardMeta>{meta}</EmailCardMeta>}
43
43
  <textarea
44
- aria-label="Résumé de l'article"
44
+ aria-label="Article summary"
45
45
  value={block.description}
46
- placeholder="Le résumé de l'article."
46
+ placeholder="The article summary."
47
47
  rows={2}
48
48
  onChange={(event) =>
49
49
  onChange({ ...block, description: event.target.value })
@@ -61,33 +61,33 @@ function ArticleBlockSettings({
61
61
  }: EmailBlockComponentProps<EmailEditorArticleBlock>) {
62
62
  return (
63
63
  <>
64
- <BlockOptionSection title="Contenu">
64
+ <BlockOptionSection title="Content">
65
65
  <TextOption
66
- label="Titre"
66
+ label="Title"
67
67
  value={block.title}
68
68
  onChange={(title) => onChange({ ...block, title })}
69
69
  />
70
70
  <TextAreaOption
71
- label="Résumé"
71
+ label="Summary"
72
72
  value={block.description}
73
73
  onChange={(description) => onChange({ ...block, description })}
74
74
  />
75
75
  <TextOption
76
- label="Auteur"
76
+ label="Author"
77
77
  value={block.author}
78
78
  onChange={(author) => onChange({ ...block, author })}
79
79
  />
80
80
  <TextOption
81
- label="Date de publication"
81
+ label="Publication date"
82
82
  value={block.publishDate}
83
83
  onChange={(publishDate) => onChange({ ...block, publishDate })}
84
84
  placeholder="2026-07-20"
85
- description="Format AAAA-MM-JJ ; la date est écrite dans la langue du destinataire à l'envoi."
85
+ description="YYYY-MM-DD format; the date is written in the recipient's language at send time."
86
86
  />
87
87
  </BlockOptionSection>
88
- <BlockOptionSection title="Apparence">
88
+ <BlockOptionSection title="Appearance">
89
89
  <TextOption
90
- label="Adresse de l'image"
90
+ label="Image URL"
91
91
  value={block.image.src}
92
92
  onChange={(src) =>
93
93
  onChange({ ...block, image: { ...block.image, src } })
@@ -95,18 +95,18 @@ function ArticleBlockSettings({
95
95
  placeholder="https://"
96
96
  />
97
97
  <TextOption
98
- label="Texte alternatif"
98
+ label="Alt text"
99
99
  value={block.image.alt}
100
100
  onChange={(alt) =>
101
101
  onChange({ ...block, image: { ...block.image, alt } })
102
102
  }
103
103
  />
104
104
  </BlockOptionSection>
105
- <BlockOptionSection title="Lien">
105
+ <BlockOptionSection title="Link">
106
106
  <LinkOption
107
107
  value={block.href}
108
108
  onChange={(href) => onChange({ ...block, href })}
109
- description="La carte entière renvoie vers cette adresse."
109
+ description="The whole card links to this address."
110
110
  />
111
111
  </BlockOptionSection>
112
112
  </>
@@ -47,7 +47,7 @@ export interface EmailBlockComponentProps<
47
47
  export interface EmailBlockDefinition<
48
48
  B extends EmailEditorBlock = EmailEditorBlock,
49
49
  > {
50
- /** French label shown in the add-block menu. */
50
+ /** Label shown in the add-block menu. */
51
51
  readonly label: string;
52
52
  readonly icon: Icon;
53
53
  /** The WYSIWYG rendering, edited in place on the canvas. */
@@ -21,8 +21,8 @@ const VARIANT_OPTIONS: ReadonlyArray<{
21
21
  readonly value: EmailEditorButtonVariant;
22
22
  readonly label: string;
23
23
  }> = [
24
- { value: "primary", label: "Plein (couleur de marque)" },
25
- { value: "secondary", label: "Contour" },
24
+ { value: "primary", label: "Filled (brand color)" },
25
+ { value: "secondary", label: "Outline" },
26
26
  ];
27
27
 
28
28
  /** Flexbox equivalents of the email's `align` attribute. */
@@ -53,12 +53,12 @@ function ButtonBlockView({
53
53
  }}
54
54
  >
55
55
  <input
56
- aria-label="Libellé du bouton"
56
+ aria-label="Button label"
57
57
  value={block.label}
58
58
  onChange={(event) =>
59
59
  onChange({ ...block, label: event.target.value })
60
60
  }
61
- placeholder="Votre bouton"
61
+ placeholder="Your button"
62
62
  className="min-w-16 max-w-full border-none bg-transparent p-0 text-center font-semibold text-[15px] leading-none outline-none [field-sizing:content] placeholder:opacity-50"
63
63
  style={{
64
64
  fontFamily: EMAIL_FONT,
@@ -76,14 +76,14 @@ function ButtonBlockSettings({
76
76
  }: EmailBlockComponentProps<EmailEditorButtonBlock>) {
77
77
  return (
78
78
  <>
79
- <BlockOptionSection title="Contenu">
79
+ <BlockOptionSection title="Content">
80
80
  <TextOption
81
- label="Libellé"
81
+ label="Label"
82
82
  value={block.label}
83
83
  onChange={(label) => onChange({ ...block, label })}
84
84
  />
85
85
  </BlockOptionSection>
86
- <BlockOptionSection title="Apparence">
86
+ <BlockOptionSection title="Appearance">
87
87
  <AlignmentOption
88
88
  value={block.align}
89
89
  onChange={(align) => onChange({ ...block, align })}
@@ -95,12 +95,12 @@ function ButtonBlockSettings({
95
95
  onChange={(variant) => onChange({ ...block, variant })}
96
96
  description={
97
97
  block.variant === "secondary"
98
- ? "Outlook (moteur Word) ignore les coins arrondis : le contour y sera à angles droits."
98
+ ? "Outlook (Word engine) ignores rounded corners: the outline will have square corners there."
99
99
  : undefined
100
100
  }
101
101
  />
102
102
  </BlockOptionSection>
103
- <BlockOptionSection title="Lien">
103
+ <BlockOptionSection title="Link">
104
104
  <LinkOption
105
105
  value={block.href}
106
106
  onChange={(href) => onChange({ ...block, href })}
@@ -112,7 +112,7 @@ function ButtonBlockSettings({
112
112
 
113
113
  export const buttonBlockDefinition: EmailBlockDefinition<EmailEditorButtonBlock> =
114
114
  {
115
- label: "Bouton",
115
+ label: "Button",
116
116
  icon: CursorClickIcon,
117
117
  View: ButtonBlockView,
118
118
  Settings: ButtonBlockSettings,
@@ -12,7 +12,7 @@ function DividerBlockView() {
12
12
 
13
13
  export const dividerBlockDefinition: EmailBlockDefinition<EmailEditorDividerBlock> =
14
14
  {
15
- label: "Séparateur",
15
+ label: "Divider",
16
16
  icon: MinusIcon,
17
17
  View: DividerBlockView,
18
18
  Settings: null,
@@ -34,7 +34,7 @@ const MOBILE_COLUMN_OPTIONS: ReadonlyArray<{
34
34
 
35
35
  /**
36
36
  * The layout shell of a multi-column row. The cells themselves — the child
37
- * block rows and the « ajouter » slot — are composed by the canvas and slotted
37
+ * block rows and the add slot — are composed by the canvas and slotted
38
38
  * in as `children`, so the grid owns the layout and nothing else.
39
39
  *
40
40
  * The cells mirror the count the reader will actually get in the previewed
@@ -65,22 +65,22 @@ function GridBlockSettings({
65
65
  onChange,
66
66
  }: EmailBlockComponentProps<EmailEditorGridBlock>) {
67
67
  return (
68
- <BlockOptionSection title="Apparence">
68
+ <BlockOptionSection title="Appearance">
69
69
  <SegmentedOption
70
- label="Colonnes (ordinateur)"
70
+ label="Columns (desktop)"
71
71
  value={block.desktopColumns}
72
72
  options={DESKTOP_COLUMN_OPTIONS}
73
73
  onChange={(desktopColumns) => onChange({ ...block, desktopColumns })}
74
74
  />
75
75
  <SegmentedOption
76
- label="Colonnes (mobile)"
76
+ label="Columns (mobile)"
77
77
  value={block.mobileColumns}
78
78
  options={MOBILE_COLUMN_OPTIONS}
79
79
  onChange={(mobileColumns) => onChange({ ...block, mobileColumns })}
80
80
  description={
81
81
  block.mobileColumns === block.desktopColumns
82
82
  ? undefined
83
- : "Un nombre de colonnes différent sur mobile repose sur une media query : l'application Gmail sur compte tiers l'ignore et repasse à une colonne."
83
+ : "A different column count on mobile relies on a media query: the Gmail app on a third-party account ignores it and falls back to one column."
84
84
  }
85
85
  />
86
86
  </BlockOptionSection>
@@ -88,7 +88,7 @@ function GridBlockSettings({
88
88
  }
89
89
 
90
90
  export const gridBlockDefinition: EmailBlockDefinition<EmailEditorGridBlock> = {
91
- label: "Colonnes",
91
+ label: "Columns",
92
92
  icon: ColumnsIcon,
93
93
  View: GridBlockView,
94
94
  Settings: GridBlockSettings,
@@ -19,8 +19,8 @@ export const EMAIL_HEADING_STYLE: {
19
19
  readonly label: string;
20
20
  };
21
21
  } = {
22
- 1: { fontSize: "22px", label: "Titre principal (H1)" },
23
- 2: { fontSize: "17px", label: "Sous-titre (H2)" },
22
+ 1: { fontSize: "22px", label: "Main title (H1)" },
23
+ 2: { fontSize: "17px", label: "Subtitle (H2)" },
24
24
  };
25
25
 
26
26
  const HEADING_LEVEL_OPTIONS: ReadonlyArray<{
@@ -42,10 +42,10 @@ function HeadingBlockView({
42
42
  }: EmailBlockComponentProps<EmailEditorHeadingBlock>) {
43
43
  return (
44
44
  <BlockTextInput
45
- ariaLabel="Titre"
45
+ ariaLabel="Heading"
46
46
  value={block.text}
47
47
  onChange={(text) => onChange({ ...block, text })}
48
- placeholder="Votre titre"
48
+ placeholder="Your heading"
49
49
  className="font-bold leading-[1.3]"
50
50
  style={{
51
51
  fontFamily: EMAIL_FONT,
@@ -63,12 +63,12 @@ function HeadingBlockSettings({
63
63
  return (
64
64
  <>
65
65
  <TextOption
66
- label="Texte"
66
+ label="Text"
67
67
  value={block.text}
68
68
  onChange={(text) => onChange({ ...block, text })}
69
69
  />
70
70
  <SelectOption
71
- label="Niveau"
71
+ label="Level"
72
72
  value={block.level}
73
73
  options={HEADING_LEVEL_OPTIONS}
74
74
  onChange={(level) => onChange({ ...block, level })}
@@ -79,7 +79,7 @@ function HeadingBlockSettings({
79
79
 
80
80
  export const headingBlockDefinition: EmailBlockDefinition<EmailEditorHeadingBlock> =
81
81
  {
82
- label: "Titre",
82
+ label: "Heading",
83
83
  icon: TextHIcon,
84
84
  View: HeadingBlockView,
85
85
  Settings: HeadingBlockSettings,
@@ -34,16 +34,16 @@ const WIDTH_OPTIONS: ReadonlyArray<{
34
34
  readonly value: EmailEditorImageWidth;
35
35
  readonly label: string;
36
36
  }> = [
37
- { value: "full", label: "Pleine largeur" },
38
- { value: "contained", label: "Largeur réduite (centrée)" },
37
+ { value: "full", label: "Full width" },
38
+ { value: "contained", label: "Reduced width (centered)" },
39
39
  ];
40
40
 
41
41
  const OVERLAY_OPTIONS: ReadonlyArray<{
42
42
  readonly value: EmailEditorImageOverlay;
43
43
  readonly label: string;
44
44
  }> = [
45
- { value: "none", label: "Aucune" },
46
- { value: "play", label: "Bouton lecture (vignette vidéo)" },
45
+ { value: "none", label: "None" },
46
+ { value: "play", label: "Play button (video thumbnail)" },
47
47
  ];
48
48
 
49
49
  /** The play badge composited over a video thumbnail. */
@@ -135,7 +135,7 @@ function ImageDropZone({
135
135
  }}
136
136
  >
137
137
  <ImageIcon size={24} aria-hidden />
138
- {uploading ? "Téléversement en cours…" : "Ajouter une image"}
138
+ {uploading ? "Uploading…" : "Add an image"}
139
139
  </button>
140
140
  <input
141
141
  ref={fileInputRef}
@@ -172,7 +172,7 @@ function ImageUploadButton({
172
172
  onClick={() => fileInputRef.current?.click()}
173
173
  >
174
174
  <UploadSimpleIcon aria-hidden />
175
- {block.src === "" ? "Téléverser une image" : "Remplacer l'image"}
175
+ {block.src === "" ? "Upload an image" : "Replace the image"}
176
176
  </Button>
177
177
  <input
178
178
  ref={fileInputRef}
@@ -198,15 +198,15 @@ function ImageBlockSettings({
198
198
  }: EmailBlockComponentProps<EmailEditorImageBlock>) {
199
199
  return (
200
200
  <>
201
- <BlockOptionSection title="Contenu">
201
+ <BlockOptionSection title="Content">
202
202
  <TextOption
203
- label="Texte alternatif"
203
+ label="Alt text"
204
204
  value={block.alt}
205
205
  onChange={(alt) => onChange({ ...block, alt })}
206
- description="Affiché quand l'image est bloquée par la messagerie."
206
+ description="Shown when the email client blocks the image."
207
207
  />
208
208
  <TextOption
209
- label="Adresse de l'image"
209
+ label="Image URL"
210
210
  value={block.src}
211
211
  onChange={(src) => onChange({ ...block, src })}
212
212
  placeholder="https://"
@@ -219,36 +219,36 @@ function ImageBlockSettings({
219
219
  />
220
220
  ) : null}
221
221
  </BlockOptionSection>
222
- <BlockOptionSection title="Apparence">
222
+ <BlockOptionSection title="Appearance">
223
223
  <SelectOption
224
- label="Largeur"
224
+ label="Width"
225
225
  value={block.width}
226
226
  options={WIDTH_OPTIONS}
227
227
  onChange={(width) => onChange({ ...block, width })}
228
228
  />
229
229
  <SelectOption
230
- label="Superposition"
230
+ label="Overlay"
231
231
  value={block.overlay}
232
232
  options={OVERLAY_OPTIONS}
233
233
  onChange={(overlay) => onChange({ ...block, overlay })}
234
234
  description={
235
235
  block.overlay === "play"
236
- ? "Aucune messagerie ne lit une vidéo intégrée : la vignette renvoie vers le lien ci-dessous. Outlook affiche la pastille sous l'image."
236
+ ? "No email client plays an embedded video: the thumbnail links to the URL below. Outlook shows the badge under the image."
237
237
  : undefined
238
238
  }
239
239
  />
240
240
  <ToggleOption
241
- label="Coins arrondis"
241
+ label="Rounded corners"
242
242
  checked={block.rounded}
243
243
  onChange={(rounded) => onChange({ ...block, rounded })}
244
- description="Outlook (moteur Word) affiche toujours des angles droits."
244
+ description="Outlook (Word engine) always renders square corners."
245
245
  />
246
246
  </BlockOptionSection>
247
- <BlockOptionSection title="Lien">
247
+ <BlockOptionSection title="Link">
248
248
  <LinkOption
249
249
  value={block.href}
250
250
  onChange={(href) => onChange({ ...block, href })}
251
- description="Laissez vide pour une image non cliquable."
251
+ description="Leave empty for a non-clickable image."
252
252
  />
253
253
  </BlockOptionSection>
254
254
  </>
@@ -17,9 +17,9 @@ const MARKER_OPTIONS: ReadonlyArray<{
17
17
  readonly value: EmailEditorListMarker;
18
18
  readonly label: string;
19
19
  }> = [
20
- { value: "bullet", label: "Puce" },
21
- { value: "number", label: "Numéro" },
22
- { value: "badge", label: "Pastille numérotée" },
20
+ { value: "bullet", label: "Bullet" },
21
+ { value: "number", label: "Number" },
22
+ { value: "badge", label: "Numbered badge" },
23
23
  ];
24
24
 
25
25
  /** The marker shown before an item, mirroring the domain `emailList`. */
@@ -109,9 +109,9 @@ function ListItemRow({
109
109
  <div className="flex min-w-0 flex-1 flex-col">
110
110
  {showTitle ? (
111
111
  <input
112
- aria-label={`Titre de l'élément ${index + 1}`}
112
+ aria-label={`Item ${index + 1} title`}
113
113
  value={item.title ?? ""}
114
- placeholder="Titre (optionnel)"
114
+ placeholder="Title (optional)"
115
115
  onChange={(event) =>
116
116
  onChange({ ...item, title: event.target.value })
117
117
  }
@@ -122,8 +122,8 @@ function ListItemRow({
122
122
  <RichTextEditable
123
123
  spans={item.spans}
124
124
  onChange={(spans) => onChange({ ...item, spans })}
125
- ariaLabel={`Élément ${index + 1}`}
126
- placeholder="Votre texte"
125
+ ariaLabel={`Item ${index + 1}`}
126
+ placeholder="Your text"
127
127
  className="text-[16px] leading-[1.6]"
128
128
  style={{ fontFamily: EMAIL_FONT, color: EMAIL_COLOR.ink }}
129
129
  />
@@ -139,13 +139,13 @@ function ListBlockSettings({
139
139
  return (
140
140
  <>
141
141
  <SelectOption
142
- label="Marqueur"
142
+ label="Marker"
143
143
  value={block.marker}
144
144
  options={MARKER_OPTIONS}
145
145
  onChange={(marker) => onChange({ ...block, marker })}
146
146
  />
147
147
  <div className="flex flex-col gap-2">
148
- <span className="font-medium text-sm">Éléments</span>
148
+ <span className="font-medium text-sm">Items</span>
149
149
  {block.items.map((item, index) => (
150
150
  <div
151
151
  key={index}
@@ -157,12 +157,12 @@ function ListBlockSettings({
157
157
  .map((span) => span.text)
158
158
  .join("")
159
159
  .trim() ||
160
- `Élément ${index + 1}`}
160
+ `Item ${index + 1}`}
161
161
  </span>
162
162
  <Button
163
163
  variant="ghost"
164
164
  size="icon-sm"
165
- aria-label={`Supprimer l'élément ${index + 1}`}
165
+ aria-label={`Remove item ${index + 1}`}
166
166
  disabled={block.items.length === 1}
167
167
  onClick={() =>
168
168
  onChange({
@@ -183,19 +183,18 @@ function ListBlockSettings({
183
183
  }
184
184
  >
185
185
  <PlusIcon aria-hidden />
186
- Ajouter un élément
186
+ Add an item
187
187
  </Button>
188
188
  </div>
189
189
  <p className="text-muted-foreground text-xs">
190
- Le texte de chaque élément se met en forme depuis la barre d'outils du
191
- bloc, comme un paragraphe.
190
+ Each item's text is formatted from the block toolbar, like a paragraph.
192
191
  </p>
193
192
  </>
194
193
  );
195
194
  }
196
195
 
197
196
  export const listBlockDefinition: EmailBlockDefinition<EmailEditorListBlock> = {
198
- label: "Liste",
197
+ label: "List",
199
198
  icon: ListBulletsIcon,
200
199
  View: ListBlockView,
201
200
  Settings: ListBlockSettings,
@@ -78,9 +78,9 @@ function OfferHeader({
78
78
  </span>
79
79
  )}
80
80
  <BlockTextInput
81
- ariaLabel="Nom de l'offre"
81
+ ariaLabel="Offer name"
82
82
  value={block.name}
83
- placeholder="Nom de l'offre"
83
+ placeholder="Offer name"
84
84
  onChange={(name) => onChange({ ...block, name })}
85
85
  className="font-bold text-[17px] leading-[1.3]"
86
86
  style={{ color: EMAIL_COLOR.brand }}
@@ -100,7 +100,7 @@ function OfferHeader({
100
100
  );
101
101
  }
102
102
 
103
- /** The ticked « points inclus », mirroring the bulleted list the renderer
103
+ /** The ticked included features, mirroring the bulleted list the renderer
104
104
  * emits for them. */
105
105
  function OfferFeatureList({ features }: { features: ReadonlyArray<string> }) {
106
106
  if (features.length === 0) {
@@ -135,11 +135,11 @@ function OfferFeatureSettings({
135
135
  }) {
136
136
  return (
137
137
  <div className="flex flex-col gap-2">
138
- <span className="font-medium text-sm">Points inclus</span>
138
+ <span className="font-medium text-sm">Included features</span>
139
139
  {block.features.map((feature, index) => (
140
140
  <div key={index} className="flex items-center gap-2">
141
141
  <input
142
- aria-label={`Point inclus ${index + 1}`}
142
+ aria-label={`Included feature ${index + 1}`}
143
143
  value={feature}
144
144
  onChange={(event) =>
145
145
  onChange({
@@ -154,7 +154,7 @@ function OfferFeatureSettings({
154
154
  <Button
155
155
  variant="ghost"
156
156
  size="icon-sm"
157
- aria-label={`Supprimer le point ${index + 1}`}
157
+ aria-label={`Remove feature ${index + 1}`}
158
158
  onClick={() =>
159
159
  onChange({
160
160
  ...block,
@@ -174,7 +174,7 @@ function OfferFeatureSettings({
174
174
  }
175
175
  >
176
176
  <PlusIcon aria-hidden />
177
- Ajouter un point
177
+ Add a feature
178
178
  </Button>
179
179
  </div>
180
180
  );
@@ -186,29 +186,29 @@ function OfferBlockSettings({
186
186
  }: EmailBlockComponentProps<EmailEditorOfferBlock>) {
187
187
  return (
188
188
  <>
189
- <BlockOptionSection title="Contenu">
189
+ <BlockOptionSection title="Content">
190
190
  <TextOption
191
- label="Surtitre"
191
+ label="Eyebrow"
192
192
  value={block.eyebrow}
193
193
  onChange={(eyebrow) => onChange({ ...block, eyebrow })}
194
- placeholder="Le plus choisi"
194
+ placeholder="Most popular"
195
195
  />
196
196
  <TextOption
197
- label="Nom"
197
+ label="Name"
198
198
  value={block.name}
199
199
  onChange={(name) => onChange({ ...block, name })}
200
200
  />
201
201
  <MoneyOption
202
- label="Prix"
202
+ label="Price"
203
203
  value={block.price}
204
204
  onChange={(price) => onChange({ ...block, price })}
205
205
  />
206
206
  <TextOption
207
- label="Périodicité"
207
+ label="Billing period"
208
208
  value={block.period}
209
209
  onChange={(period) => onChange({ ...block, period })}
210
- placeholder="par mois"
211
- description="Laissez vide pour un tarif unique."
210
+ placeholder="per month"
211
+ description="Leave empty for a one-off price."
212
212
  />
213
213
  <TextAreaOption
214
214
  label="Description"
@@ -217,37 +217,37 @@ function OfferBlockSettings({
217
217
  />
218
218
  <OfferFeatureSettings block={block} onChange={onChange} />
219
219
  </BlockOptionSection>
220
- <BlockOptionSection title="Apparence">
220
+ <BlockOptionSection title="Appearance">
221
221
  <ToggleOption
222
- label="Mettre en avant"
222
+ label="Highlight"
223
223
  checked={block.highlighted}
224
224
  onChange={(highlighted) => onChange({ ...block, highlighted })}
225
- description="Encadre la carte dans la couleur de marque. Outlook (moteur Word) affiche des angles droits."
225
+ description="Frames the card in the brand color. Outlook (Word engine) renders square corners."
226
226
  />
227
227
  <TextOption
228
- label="Adresse de l'image"
228
+ label="Image URL"
229
229
  value={block.image.src}
230
230
  onChange={(src) =>
231
231
  onChange({ ...block, image: { ...block.image, src } })
232
232
  }
233
233
  placeholder="https://"
234
- description="Laissez vide pour une offre sans visuel."
234
+ description="Leave empty for an offer without a visual."
235
235
  />
236
236
  <TextOption
237
- label="Texte alternatif"
237
+ label="Alt text"
238
238
  value={block.image.alt}
239
239
  onChange={(alt) =>
240
240
  onChange({ ...block, image: { ...block.image, alt } })
241
241
  }
242
242
  />
243
243
  </BlockOptionSection>
244
- <BlockOptionSection title="Lien">
244
+ <BlockOptionSection title="Link">
245
245
  <TextOption
246
- label="Libellé du bouton"
246
+ label="Button label"
247
247
  value={block.buttonLabel}
248
248
  onChange={(buttonLabel) => onChange({ ...block, buttonLabel })}
249
- placeholder="Choisir cette offre"
250
- description="Laissez vide pour une carte sans bouton."
249
+ placeholder="Choose this offer"
250
+ description="Leave empty for a card without a button."
251
251
  />
252
252
  <LinkOption
253
253
  value={block.buttonHref}
@@ -260,7 +260,7 @@ function OfferBlockSettings({
260
260
 
261
261
  export const offerBlockDefinition: EmailBlockDefinition<EmailEditorOfferBlock> =
262
262
  {
263
- label: "Offre",
263
+ label: "Offer",
264
264
  icon: SealPercentIcon,
265
265
  View: OfferBlockView,
266
266
  Settings: OfferBlockSettings,
@@ -20,8 +20,8 @@ function ParagraphBlockView({
20
20
  <RichTextEditable
21
21
  spans={block.spans}
22
22
  onChange={(spans) => onChange({ ...block, spans })}
23
- ariaLabel="Paragraphe"
24
- placeholder="Votre texte. Utilisez {{firstName}} pour personnaliser."
23
+ ariaLabel="Paragraph"
24
+ placeholder="Your text. Use {{firstName}} to personalize."
25
25
  className="text-[16px] leading-[1.6]"
26
26
  style={{ fontFamily: EMAIL_FONT, color: EMAIL_COLOR.ink }}
27
27
  />
@@ -32,7 +32,7 @@ function ParagraphBlockSettings() {
32
32
  return (
33
33
  <p className="text-muted-foreground text-xs">
34
34
  {
35
- "Mettez en forme le texte (gras, italique, souligné, lien) depuis la barre d'outils du bloc. Personnalisez avec {{firstName}}, {{lastName}} ou {{email}} ; la valeur du contact est substituée à l'envoi."
35
+ "Format the text (bold, italic, underline, link) from the block toolbar. Personalize with {{firstName}}, {{lastName}} or {{email}}; the contact's value is substituted at send time."
36
36
  }
37
37
  </p>
38
38
  );
@@ -40,7 +40,7 @@ function ParagraphBlockSettings() {
40
40
 
41
41
  export const paragraphBlockDefinition: EmailBlockDefinition<EmailEditorParagraphBlock> =
42
42
  {
43
- label: "Paragraphe",
43
+ label: "Paragraph",
44
44
  icon: TextAlignLeftIcon,
45
45
  View: ParagraphBlockView,
46
46
  Settings: ParagraphBlockSettings,