@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.
@@ -21,7 +21,7 @@ import { EMAIL_COLOR } from "#/theme.ts";
21
21
 
22
22
  /**
23
23
  * A catalogue item on the shared card shell: visual, name, description, price
24
- * with an optional struck-through « tarif de base », and a call to action.
24
+ * with an optional struck-through base price, and a call to action.
25
25
  */
26
26
  function ProductBlockView({
27
27
  block,
@@ -30,17 +30,17 @@ function ProductBlockView({
30
30
  return (
31
31
  <EmailCardShell image={block.image}>
32
32
  <BlockTextInput
33
- ariaLabel="Nom du produit"
33
+ ariaLabel="Product name"
34
34
  value={block.name}
35
- placeholder="Nom du produit"
35
+ placeholder="Product name"
36
36
  onChange={(name) => onChange({ ...block, name })}
37
37
  className="font-bold text-[17px] leading-[1.3]"
38
38
  style={{ color: EMAIL_COLOR.brand }}
39
39
  />
40
40
  <textarea
41
- aria-label="Description du produit"
41
+ aria-label="Product description"
42
42
  value={block.description}
43
- placeholder="La description du produit."
43
+ placeholder="The product description."
44
44
  rows={2}
45
45
  onChange={(event) =>
46
46
  onChange({ ...block, description: event.target.value })
@@ -75,9 +75,9 @@ function ProductBlockSettings({
75
75
  }: EmailBlockComponentProps<EmailEditorProductBlock>) {
76
76
  return (
77
77
  <>
78
- <BlockOptionSection title="Contenu">
78
+ <BlockOptionSection title="Content">
79
79
  <TextOption
80
- label="Nom"
80
+ label="Name"
81
81
  value={block.name}
82
82
  onChange={(name) => onChange({ ...block, name })}
83
83
  />
@@ -87,12 +87,12 @@ function ProductBlockSettings({
87
87
  onChange={(description) => onChange({ ...block, description })}
88
88
  />
89
89
  <MoneyOption
90
- label="Prix"
90
+ label="Price"
91
91
  value={block.price}
92
92
  onChange={(price) => onChange({ ...block, price })}
93
93
  />
94
94
  <ToggleOption
95
- label="Tarif de base barré"
95
+ label="Struck-through base price"
96
96
  checked={block.compareAtPrice !== null}
97
97
  onChange={(enabled) =>
98
98
  onChange({
@@ -100,11 +100,11 @@ function ProductBlockSettings({
100
100
  compareAtPrice: enabled ? { ...block.price } : null,
101
101
  })
102
102
  }
103
- description="Affiche un prix barré à côté du prix courant."
103
+ description="Shows a struck-through price next to the current price."
104
104
  />
105
105
  {block.compareAtPrice === null ? null : (
106
106
  <MoneyOption
107
- label="Tarif de base"
107
+ label="Base price"
108
108
  value={block.compareAtPrice}
109
109
  onChange={(compareAtPrice) =>
110
110
  onChange({ ...block, compareAtPrice })
@@ -112,9 +112,9 @@ function ProductBlockSettings({
112
112
  />
113
113
  )}
114
114
  </BlockOptionSection>
115
- <BlockOptionSection title="Apparence">
115
+ <BlockOptionSection title="Appearance">
116
116
  <TextOption
117
- label="Adresse de l'image"
117
+ label="Image URL"
118
118
  value={block.image.src}
119
119
  onChange={(src) =>
120
120
  onChange({ ...block, image: { ...block.image, src } })
@@ -122,20 +122,20 @@ function ProductBlockSettings({
122
122
  placeholder="https://"
123
123
  />
124
124
  <TextOption
125
- label="Texte alternatif"
125
+ label="Alt text"
126
126
  value={block.image.alt}
127
127
  onChange={(alt) =>
128
128
  onChange({ ...block, image: { ...block.image, alt } })
129
129
  }
130
130
  />
131
131
  </BlockOptionSection>
132
- <BlockOptionSection title="Lien">
132
+ <BlockOptionSection title="Link">
133
133
  <TextOption
134
- label="Libellé du bouton"
134
+ label="Button label"
135
135
  value={block.buttonLabel}
136
136
  onChange={(buttonLabel) => onChange({ ...block, buttonLabel })}
137
- placeholder="Commander"
138
- description="Laissez vide pour une carte sans bouton."
137
+ placeholder="Order now"
138
+ description="Leave empty for a card without a button."
139
139
  />
140
140
  <LinkOption
141
141
  value={block.href}
@@ -148,7 +148,7 @@ function ProductBlockSettings({
148
148
 
149
149
  export const productBlockDefinition: EmailBlockDefinition<EmailEditorProductBlock> =
150
150
  {
151
- label: "Produit",
151
+ label: "Product",
152
152
  icon: TagIcon,
153
153
  View: ProductBlockView,
154
154
  Settings: ProductBlockSettings,
@@ -23,8 +23,8 @@ const STYLE_OPTIONS: ReadonlyArray<{
23
23
  readonly value: EmailEditorRatingStyle;
24
24
  readonly label: string;
25
25
  }> = [
26
- { value: "filled", label: "Étoiles pleines" },
27
- { value: "outline", label: "Étoiles contour" },
26
+ { value: "filled", label: "Filled stars" },
27
+ { value: "outline", label: "Outlined stars" },
28
28
  ];
29
29
 
30
30
  /**
@@ -42,7 +42,7 @@ function RatingBlockView({
42
42
  spans={block.question}
43
43
  onChange={(question) => onChange({ ...block, question })}
44
44
  ariaLabel="Question"
45
- placeholder="Que pensez-vous de votre dernière mission ?"
45
+ placeholder="How did your last project go?"
46
46
  className="text-center text-[16px] leading-[1.6]"
47
47
  style={{ fontFamily: EMAIL_FONT, color: EMAIL_COLOR.ink }}
48
48
  />
@@ -75,25 +75,25 @@ function RatingBlockSettings({
75
75
  }: EmailBlockComponentProps<EmailEditorRatingBlock>) {
76
76
  return (
77
77
  <>
78
- <BlockOptionSection title="Contenu">
78
+ <BlockOptionSection title="Content">
79
79
  <p className="text-muted-foreground text-xs">
80
- La question se saisit et se met en forme directement sur le bloc,
81
- comme un paragraphe.
80
+ The question is typed and formatted directly on the block, like a
81
+ paragraph.
82
82
  </p>
83
83
  <TextOption
84
- label="Libellé bas de l'échelle"
84
+ label="Low end of the scale"
85
85
  value={block.lowLabel}
86
86
  onChange={(lowLabel) => onChange({ ...block, lowLabel })}
87
- placeholder="Pas du tout"
87
+ placeholder="Not at all"
88
88
  />
89
89
  <TextOption
90
- label="Libellé haut de l'échelle"
90
+ label="High end of the scale"
91
91
  value={block.highLabel}
92
92
  onChange={(highLabel) => onChange({ ...block, highLabel })}
93
- placeholder="Tout à fait"
93
+ placeholder="Absolutely"
94
94
  />
95
95
  </BlockOptionSection>
96
- <BlockOptionSection title="Apparence">
96
+ <BlockOptionSection title="Appearance">
97
97
  <SelectOption
98
98
  label="Style"
99
99
  value={block.style}
@@ -101,11 +101,11 @@ function RatingBlockSettings({
101
101
  onChange={(style) => onChange({ ...block, style })}
102
102
  />
103
103
  </BlockOptionSection>
104
- <BlockOptionSection title="Lien">
104
+ <BlockOptionSection title="Link">
105
105
  <LinkOption
106
106
  value={block.href}
107
107
  onChange={(href) => onChange({ ...block, href })}
108
- description="Chaque étoile pointe vers cette adresse avec « rating=1 » à « rating=5 » ajouté : les cinq notes sont donc comptées séparément dans les statistiques de clic."
108
+ description="Each star points to this address with rating=1 through rating=5 appended: the five scores are therefore counted separately in the click statistics."
109
109
  />
110
110
  </BlockOptionSection>
111
111
  </>
@@ -114,7 +114,7 @@ function RatingBlockSettings({
114
114
 
115
115
  export const ratingBlockDefinition: EmailBlockDefinition<EmailEditorRatingBlock> =
116
116
  {
117
- label: "Notation",
117
+ label: "Rating",
118
118
  icon: StarIcon,
119
119
  View: RatingBlockView,
120
120
  Settings: RatingBlockSettings,
@@ -37,7 +37,7 @@ function StatBlockView({
37
37
  style={{ textAlign, fontFamily: EMAIL_FONT }}
38
38
  >
39
39
  <BlockTextInput
40
- ariaLabel="Valeur"
40
+ ariaLabel="Value"
41
41
  value={block.value}
42
42
  placeholder="128"
43
43
  onChange={(value) => onChange({ ...block, value })}
@@ -45,9 +45,9 @@ function StatBlockView({
45
45
  style={{ color: EMAIL_COLOR.brand, textAlign }}
46
46
  />
47
47
  <BlockTextInput
48
- ariaLabel="Libellé"
48
+ ariaLabel="Label"
49
49
  value={block.label}
50
- placeholder="Missions pourvues"
50
+ placeholder="Projects delivered"
51
51
  onChange={(label) => onChange({ ...block, label })}
52
52
  className="font-semibold text-[12px] uppercase leading-[1.4] tracking-[0.04em]"
53
53
  style={{ color: EMAIL_COLOR.muted, textAlign }}
@@ -59,7 +59,7 @@ function StatBlockView({
59
59
  <BlockTextInput
60
60
  ariaLabel="Description"
61
61
  value={block.description}
62
- placeholder="Description (optionnelle)"
62
+ placeholder="Description (optional)"
63
63
  onChange={(description) => onChange({ ...block, description })}
64
64
  className="text-[14px] leading-[1.5]"
65
65
  style={{ color: EMAIL_COLOR.ink, textAlign }}
@@ -75,18 +75,18 @@ function StatBlockSettings({
75
75
  }: EmailBlockComponentProps<EmailEditorStatBlock>) {
76
76
  return (
77
77
  <>
78
- <BlockOptionSection title="Contenu">
78
+ <BlockOptionSection title="Content">
79
79
  <TextOption
80
- label="Valeur"
80
+ label="Value"
81
81
  value={block.value}
82
82
  onChange={(value) => onChange({ ...block, value })}
83
83
  placeholder="128"
84
84
  />
85
85
  <TextOption
86
- label="Libellé"
86
+ label="Label"
87
87
  value={block.label}
88
88
  onChange={(label) => onChange({ ...block, label })}
89
- placeholder="Missions pourvues"
89
+ placeholder="Projects delivered"
90
90
  />
91
91
  <TextAreaOption
92
92
  label="Description"
@@ -95,7 +95,7 @@ function StatBlockSettings({
95
95
  rows={2}
96
96
  />
97
97
  </BlockOptionSection>
98
- <BlockOptionSection title="Apparence">
98
+ <BlockOptionSection title="Appearance">
99
99
  <AlignmentOption
100
100
  value={block.align}
101
101
  onChange={(align) => onChange({ ...block, align })}
@@ -106,7 +106,7 @@ function StatBlockSettings({
106
106
  }
107
107
 
108
108
  export const statBlockDefinition: EmailBlockDefinition<EmailEditorStatBlock> = {
109
- label: "Chiffre clé",
109
+ label: "Key figure",
110
110
  icon: ChartBarIcon,
111
111
  View: StatBlockView,
112
112
  Settings: StatBlockSettings,
@@ -21,8 +21,8 @@ const ALIGN_OPTIONS: ReadonlyArray<{
21
21
  readonly value: EmailEditorTableColumn["align"];
22
22
  readonly label: string;
23
23
  }> = [
24
- { value: "left", label: "À gauche" },
25
- { value: "right", label: "À droite" },
24
+ { value: "left", label: "Left" },
25
+ { value: "right", label: "Right" },
26
26
  ];
27
27
 
28
28
  /** Mirrors `emailLineItemsTable`'s `flushEdges`: the first and last columns
@@ -84,9 +84,9 @@ function TableBlockView({
84
84
  }}
85
85
  >
86
86
  <BlockTextInput
87
- ariaLabel={`Titre de la colonne ${columnIndex + 1}`}
87
+ ariaLabel={`Column ${columnIndex + 1} title`}
88
88
  value={column.label}
89
- placeholder="Colonne"
89
+ placeholder="Column"
90
90
  onChange={(label) =>
91
91
  onChange({
92
92
  ...block,
@@ -116,7 +116,7 @@ function TableBlockView({
116
116
  style={{ borderBottom: `1px solid ${EMAIL_COLOR.border}` }}
117
117
  >
118
118
  <BlockTextInput
119
- ariaLabel={`Ligne ${rowIndex + 1}, colonne ${columnIndex + 1}`}
119
+ ariaLabel={`Row ${rowIndex + 1}, column ${columnIndex + 1}`}
120
120
  value={cell}
121
121
  onChange={(value) => setCell(rowIndex, columnIndex, value)}
122
122
  className="text-[14px] leading-[1.4]"
@@ -147,7 +147,7 @@ function TableColumnSettings({
147
147
  <div key={index} className="flex items-end gap-2">
148
148
  <div className="flex-1">
149
149
  <SelectOption
150
- label={column.label.trim() || `Colonne ${index + 1}`}
150
+ label={column.label.trim() || `Column ${index + 1}`}
151
151
  value={column.align}
152
152
  options={ALIGN_OPTIONS}
153
153
  onChange={(align) =>
@@ -163,7 +163,7 @@ function TableColumnSettings({
163
163
  <Button
164
164
  variant="ghost"
165
165
  size="icon-sm"
166
- aria-label={`Supprimer la colonne ${index + 1}`}
166
+ aria-label={`Remove column ${index + 1}`}
167
167
  disabled={block.columns.length === 1}
168
168
  onClick={() =>
169
169
  onChange({
@@ -191,7 +191,7 @@ function TableColumnSettings({
191
191
  }
192
192
  >
193
193
  <PlusIcon aria-hidden />
194
- Ajouter une colonne
194
+ Add a column
195
195
  </Button>
196
196
  </>
197
197
  );
@@ -203,16 +203,16 @@ function TableBlockSettings({
203
203
  }: EmailBlockComponentProps<EmailEditorTableBlock>) {
204
204
  return (
205
205
  <>
206
- <BlockOptionSection title="Contenu">
206
+ <BlockOptionSection title="Content">
207
207
  <div className="flex items-center justify-between gap-2 text-sm">
208
208
  <span className="text-muted-foreground">
209
- {block.rows.length} ligne{block.rows.length > 1 ? "s" : ""}
209
+ {block.rows.length} row{block.rows.length > 1 ? "s" : ""}
210
210
  </span>
211
211
  <div className="flex gap-1">
212
212
  <Button
213
213
  variant="ghost"
214
214
  size="icon-sm"
215
- aria-label="Supprimer la dernière ligne"
215
+ aria-label="Remove the last row"
216
216
  disabled={block.rows.length === 1}
217
217
  onClick={() =>
218
218
  onChange({ ...block, rows: block.rows.slice(0, -1) })
@@ -231,14 +231,14 @@ function TableBlockSettings({
231
231
  }
232
232
  >
233
233
  <PlusIcon aria-hidden />
234
- Ligne
234
+ Row
235
235
  </Button>
236
236
  </div>
237
237
  </div>
238
238
  </BlockOptionSection>
239
- <BlockOptionSection title="Apparence">
239
+ <BlockOptionSection title="Appearance">
240
240
  <ToggleOption
241
- label="Ligne d'en-tête"
241
+ label="Header row"
242
242
  checked={block.headerRow}
243
243
  onChange={(headerRow) => onChange({ ...block, headerRow })}
244
244
  />
@@ -250,7 +250,7 @@ function TableBlockSettings({
250
250
 
251
251
  export const tableBlockDefinition: EmailBlockDefinition<EmailEditorTableBlock> =
252
252
  {
253
- label: "Tableau",
253
+ label: "Table",
254
254
  icon: TableIcon,
255
255
  View: TableBlockView,
256
256
  Settings: TableBlockSettings,
@@ -190,7 +190,7 @@ export interface EmailEditorProductBlock {
190
190
  readonly description: string;
191
191
  readonly image: EmailEditorCardImage;
192
192
  readonly price: EmailEditorMoney;
193
- /** The struck-through « tarif de base », mirroring the shop catalogue's
193
+ /** The struck-through base price, mirroring the shop catalogue's
194
194
  * `publicPriceHtCents`. Null when the product is not discounted. */
195
195
  readonly compareAtPrice: EmailEditorMoney | null;
196
196
  readonly href: string;
@@ -205,7 +205,7 @@ export interface EmailEditorOfferBlock {
205
205
  readonly description: string;
206
206
  readonly image: EmailEditorCardImage;
207
207
  readonly price: EmailEditorMoney;
208
- /** « par mois », « par an »; empty for a one-off price. */
208
+ /** "per month", "per year"; empty for a one-off price. */
209
209
  readonly period: string;
210
210
  readonly features: ReadonlyArray<string>;
211
211
  readonly buttonLabel: string;
@@ -23,7 +23,7 @@ import {
23
23
  } from "#/sections/editor-sidebar.tsx";
24
24
  import { PreviewToggle } from "#/sections/preview-toggle.tsx";
25
25
 
26
- /** « Réglages du bloc » as a bottom sheet, for the viewports where a 280px
26
+ /** The block settings panel as a bottom sheet, for the viewports where a 280px
27
27
  * column would put the options a screenful away from their block. */
28
28
  function BlockSettingsSheet({
29
29
  open,
@@ -42,9 +42,9 @@ function BlockSettingsSheet({
42
42
  <Drawer open={open} onOpenChange={onOpenChange}>
43
43
  <DrawerContent>
44
44
  <DrawerHeader>
45
- <DrawerTitle>Réglages du bloc</DrawerTitle>
45
+ <DrawerTitle>Block settings</DrawerTitle>
46
46
  <DrawerDescription className="sr-only">
47
- Options du bloc sélectionné.
47
+ Options for the selected block.
48
48
  </DrawerDescription>
49
49
  </DrawerHeader>
50
50
  <div className="flex flex-col gap-4 overflow-y-auto px-4 pb-8">
@@ -31,7 +31,7 @@ export const useCoarsePointer = (): boolean =>
31
31
 
32
32
  /**
33
33
  * Below Tailwind's `lg`, the 280px settings column no longer fits beside the
34
- * canvas, so « Réglages du bloc » moves into a bottom sheet reachable from the
34
+ * canvas, so the block settings panel moves into a bottom sheet reachable from the
35
35
  * selected block's toolbar.
36
36
  */
37
37
  export const useCompactEditorLayout = (): boolean =>
@@ -14,7 +14,7 @@ import {
14
14
  import type { EmailEditorBlockType } from "#/document/types.ts";
15
15
 
16
16
  /**
17
- * The « Ajouter un bloc » menu, fed by the block registry. The default trigger
17
+ * The "Add a block" menu, fed by the block registry. The default trigger
18
18
  * is the outlined empty-state button; the block toolbar passes its own icon
19
19
  * trigger instead.
20
20
  */
@@ -35,7 +35,7 @@ export function AddBlockMenu({
35
35
  trigger ?? (
36
36
  <Button variant="outline" size="sm">
37
37
  <PlusIcon aria-hidden />
38
- Ajouter un bloc
38
+ Add a block
39
39
  </Button>
40
40
  )
41
41
  }
@@ -13,28 +13,28 @@ const ALIGNMENTS: ReadonlyArray<{
13
13
  }> = [
14
14
  {
15
15
  value: "left",
16
- label: "Aligner à gauche",
16
+ label: "Align left",
17
17
  icon: <TextAlignLeftIcon aria-hidden />,
18
18
  },
19
19
  {
20
20
  value: "center",
21
- label: "Centrer",
21
+ label: "Align center",
22
22
  icon: <TextAlignCenterIcon aria-hidden />,
23
23
  },
24
24
  {
25
25
  value: "right",
26
- label: "Aligner à droite",
26
+ label: "Align right",
27
27
  icon: <TextAlignRightIcon aria-hidden />,
28
28
  },
29
29
  ];
30
30
 
31
31
  /**
32
32
  * The one alignment control. Every block that can be aligned uses this exact
33
- * segmented control, so « Alignement » means the same thing and looks the same
33
+ * segmented control, so "Alignment" means the same thing and looks the same
34
34
  * everywhere (§1.2 of the editor plan).
35
35
  */
36
36
  export function AlignmentOption({
37
- label = "Alignement",
37
+ label = "Alignment",
38
38
  value,
39
39
  onChange,
40
40
  }: {
@@ -7,7 +7,7 @@ import {
7
7
  import type { ReactNode } from "react";
8
8
 
9
9
  /**
10
- * One labelled option in « Réglages du bloc ». Every block setting goes
10
+ * One labelled option in the block settings panel. Every block setting goes
11
11
  * through this row rather than hand-rolling `useId()` + `Label` + control, so
12
12
  * spacing, label typography and description placement cannot drift from block
13
13
  * to block.
@@ -58,8 +58,8 @@ export function BlockOptionRow({
58
58
 
59
59
  /**
60
60
  * A titled group of rows. §1.4 of the editor plan: once a block carries more
61
- * than a handful of options they are split into « Contenu », « Apparence » and
62
- * « Lien », always in that order.
61
+ * than a handful of options they are split into "Content", "Appearance" and
62
+ * "Link", always in that order.
63
63
  */
64
64
  export function BlockOptionSection({
65
65
  title,
@@ -62,7 +62,7 @@ export function MoneyOption({
62
62
  })
63
63
  }
64
64
  currency={value.currency}
65
- currencyLabel="Devise"
65
+ currencyLabel="Currency"
66
66
  />
67
67
  </BlockOptionRow>
68
68
  );
@@ -61,12 +61,12 @@ export function TextAreaOption({
61
61
  }
62
62
 
63
63
  /**
64
- * The one destination-URL control. Always « Lien (URL) », always
64
+ * The one destination-URL control. Always "Link (URL)", always
65
65
  * `type="url"` with the `https://` placeholder, so a link field is
66
66
  * recognisable at a glance in any block (§1.2 of the editor plan).
67
67
  */
68
68
  export function LinkOption({
69
- label = "Lien (URL)",
69
+ label = "Link (URL)",
70
70
  value,
71
71
  onChange,
72
72
  description,
@@ -141,7 +141,7 @@ function StructureControls({
141
141
  <Button
142
142
  variant="ghost"
143
143
  size={coarsePointer ? "icon" : "icon-sm"}
144
- aria-label="Ajouter un bloc"
144
+ aria-label="Add a block"
145
145
  className={toolbarButtonClassName(coarsePointer)}
146
146
  >
147
147
  <PlusIcon aria-hidden />
@@ -151,7 +151,7 @@ function StructureControls({
151
151
  <Button
152
152
  variant="ghost"
153
153
  size={coarsePointer ? "icon" : "icon-sm"}
154
- aria-label="Déplacer le bloc"
154
+ aria-label="Move block"
155
155
  className={cn(
156
156
  toolbarButtonClassName(coarsePointer),
157
157
  "cursor-grab touch-none active:cursor-grabbing",
@@ -164,7 +164,7 @@ function StructureControls({
164
164
  </Button>
165
165
  {onSelectContainer ? (
166
166
  <ToolbarIconButton
167
- label="Sélectionner la ligne de colonnes"
167
+ label="Select the column row"
168
168
  coarsePointer={coarsePointer}
169
169
  onClick={onSelectContainer}
170
170
  >
@@ -190,7 +190,7 @@ function RichTextControls({
190
190
  <>
191
191
  <ToolbarSeparator />
192
192
  <ToolbarIconButton
193
- label="Gras"
193
+ label="Bold"
194
194
  active={active.has("bold")}
195
195
  coarsePointer={coarsePointer}
196
196
  onMouseDown={keepSelection}
@@ -199,7 +199,7 @@ function RichTextControls({
199
199
  <TextBIcon aria-hidden />
200
200
  </ToolbarIconButton>
201
201
  <ToolbarIconButton
202
- label="Italique"
202
+ label="Italic"
203
203
  active={active.has("italic")}
204
204
  coarsePointer={coarsePointer}
205
205
  onMouseDown={keepSelection}
@@ -208,7 +208,7 @@ function RichTextControls({
208
208
  <TextItalicIcon aria-hidden />
209
209
  </ToolbarIconButton>
210
210
  <ToolbarIconButton
211
- label="Souligné"
211
+ label="Underline"
212
212
  active={active.has("underline")}
213
213
  coarsePointer={coarsePointer}
214
214
  onMouseDown={keepSelection}
@@ -253,7 +253,7 @@ function SelectionLinkButton({
253
253
  <Button
254
254
  variant="ghost"
255
255
  size={coarsePointer ? "icon" : "icon-sm"}
256
- aria-label="Insérer un lien"
256
+ aria-label="Insert a link"
257
257
  aria-pressed={active || undefined}
258
258
  className={toolbarButtonClassName(coarsePointer, active)}
259
259
  onMouseDown={keepSelection}
@@ -295,7 +295,7 @@ function SelectionLinkButton({
295
295
  /**
296
296
  * The controls of the selected block: add-below, drag handle, inline
297
297
  * formatting when the block holds rich text, duplicate, delete, plus a
298
- * « Réglages » button when the settings live in a bottom sheet rather than in
298
+ * Settings button when the settings live in a bottom sheet rather than in
299
299
  * the sidebar. App-chrome styling on purpose — the toolbar is editor UI, not
300
300
  * part of the email. Only ever rendered for the selected block, so a single
301
301
  * toolbar is visible at a time.
@@ -303,7 +303,7 @@ function SelectionLinkButton({
303
303
  * At 44px per target the full set is wider than a 390px viewport, so the row
304
304
  * wraps under a touch pointer rather than scrolling: it sits in the flow there
305
305
  * (see the canvas), so a second line costs nothing, while a scrolling row
306
- * would hide « Supprimer » behind an edge with no affordance.
306
+ * would hide Delete behind an edge with no affordance.
307
307
  */
308
308
  export function BlockToolbar({
309
309
  handle,
@@ -353,7 +353,7 @@ export function BlockToolbar({
353
353
  <ToolbarSeparator />
354
354
  {onOpenSettings ? (
355
355
  <ToolbarIconButton
356
- label="Réglages du bloc"
356
+ label="Block settings"
357
357
  coarsePointer={coarsePointer}
358
358
  onClick={onOpenSettings}
359
359
  >
@@ -361,14 +361,14 @@ export function BlockToolbar({
361
361
  </ToolbarIconButton>
362
362
  ) : null}
363
363
  <ToolbarIconButton
364
- label="Dupliquer le bloc"
364
+ label="Duplicate block"
365
365
  coarsePointer={coarsePointer}
366
366
  onClick={onDuplicate}
367
367
  >
368
368
  <CopyIcon aria-hidden />
369
369
  </ToolbarIconButton>
370
370
  <ToolbarIconButton
371
- label="Supprimer le bloc"
371
+ label="Delete block"
372
372
  coarsePointer={coarsePointer}
373
373
  onClick={onRemove}
374
374
  >
@@ -53,7 +53,7 @@ interface CanvasContext {
53
53
  readonly onOpenSettings?: () => void;
54
54
  }
55
55
 
56
- /** The dashed « ajouter » slot of a grid cell. */
56
+ /** The dashed add slot of a grid cell. */
57
57
  function GridAddCell({
58
58
  gridId,
59
59
  context,
@@ -325,7 +325,7 @@ export function EditorCanvas({
325
325
  preview: EmailEditorPreview;
326
326
  onUploadImage?: (file: File) => Promise<string>;
327
327
  /** Present when the settings live in a sheet rather than in the sidebar;
328
- * each selected block's toolbar then offers a « Réglages » button. */
328
+ * each selected block's toolbar then offers a Settings button. */
329
329
  onOpenSettings?: () => void;
330
330
  headerSlot?: ReactNode;
331
331
  footerSlot?: ReactNode;
@@ -387,7 +387,7 @@ export function EditorCanvas({
387
387
  className="text-[14px]"
388
388
  style={{ color: EMAIL_COLOR.muted, fontFamily: EMAIL_FONT }}
389
389
  >
390
- Votre email est vide.
390
+ Your email is empty.
391
391
  </p>
392
392
  <AddBlockMenu
393
393
  onAdd={(type) => dispatch({ type: "add", blockType: type })}