@fonderie/react-courier-admin-screens 0.7.0 → 0.9.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/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/screens/TemplateEditorScreen.tsx
30
+ var import_client = require("@fonderie/client");
30
31
  var import_react_courier_admin = require("@fonderie/react-courier-admin");
31
32
  var import_react = require("react");
32
33
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -39,10 +40,14 @@ function TemplateEditorScreen({
39
40
  onSaved,
40
41
  system = false,
41
42
  onDeleted,
42
- onAddLocale
43
+ onAddLocale,
44
+ onSelectLocale,
45
+ uiLocale
43
46
  }) {
47
+ const t = (0, import_react.useMemo)(() => (0, import_client.createAdminT)(uiLocale), [uiLocale]);
44
48
  const { template, isLoading, error, refresh } = (0, import_react_courier_admin.useTemplate)(client, type, locale);
45
- const { saveTemplate, removeTemplate } = (0, import_react_courier_admin.useTemplates)(client);
49
+ const { templates, saveTemplate, removeTemplate } = (0, import_react_courier_admin.useTemplates)(client);
50
+ const siblings = (0, import_client.groupTemplatesByType)(templates).find((g) => g.type === type)?.entries ?? [];
46
51
  const [isDeleting, setIsDeleting] = (0, import_react.useState)(false);
47
52
  const { revisions, rollback } = (0, import_react_courier_admin.useTemplateRevisions)(client, type, locale);
48
53
  const [isSaving, setIsSaving] = (0, import_react.useState)(false);
@@ -66,13 +71,13 @@ function TemplateEditorScreen({
66
71
  try {
67
72
  const parsed = JSON.parse(sampleJson || "{}");
68
73
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
69
- setSampleError("Sample data must be a JSON object.");
74
+ setSampleError(t("templates.editor.sampleNotObject"));
70
75
  return;
71
76
  }
72
77
  data = parsed;
73
78
  setSampleError(null);
74
79
  } catch {
75
- setSampleError("Sample data is not valid JSON.");
80
+ setSampleError(t("templates.editor.sampleInvalid"));
76
81
  return;
77
82
  }
78
83
  const result = await renderPreview(
@@ -85,7 +90,7 @@ function TemplateEditorScreen({
85
90
  const seeded = { ...data, ...Object.fromEntries(missing.map((v) => [v, v])) };
86
91
  setSampleJson(JSON.stringify(seeded, null, 2));
87
92
  }
88
- }, [renderPreview, type, text, subject, html, locale, sampleJson]);
93
+ }, [renderPreview, type, text, subject, html, locale, sampleJson, t]);
89
94
  const runRef = (0, import_react.useRef)(run);
90
95
  runRef.current = run;
91
96
  const loadedType = template ? `${type}:${locale ?? ""}` : null;
@@ -113,10 +118,13 @@ function TemplateEditorScreen({
113
118
  setIsSaving(false);
114
119
  }
115
120
  };
121
+ const switchTo = (entry) => {
122
+ if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
123
+ onSelectLocale?.(entry);
124
+ };
116
125
  const handleDelete = async () => {
117
- const which = locale ? `the ${locale} version of "${type}"` : `"${type}"`;
118
- const fallback = locale ? " People in that locale will receive the default version." : "";
119
- if (!window.confirm(`Delete ${which}?${fallback}`)) return;
126
+ const message = locale ? t("templates.editor.confirmDeleteLocale", { type, locale }) : t("templates.editor.confirmDelete", { type });
127
+ if (!window.confirm(message)) return;
120
128
  setIsDeleting(true);
121
129
  setSaveError(null);
122
130
  try {
@@ -128,32 +136,54 @@ function TemplateEditorScreen({
128
136
  setIsDeleting(false);
129
137
  }
130
138
  };
131
- if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.status, children: "Loading template\u2026" });
139
+ if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.status, children: t("templates.editor.loading") });
132
140
  if (error)
133
141
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, role: "alert", children: error.explanation });
134
142
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.container, children: [
135
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
136
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: styles.title, children: type }),
137
- onAddLocale && !locale ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: styles.title, children: type }),
144
+ onSelectLocale || onAddLocale ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.tabBar, children: [
145
+ onSelectLocale ? (
146
+ // Each tab opens another stored row, so this is navigation, not an
147
+ // ARIA tablist: plain buttons, the open one marked aria-current.
148
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { "aria-label": t("templates.editor.locales"), style: styles.tabs, children: siblings.map((entry) => {
149
+ const selected = entry.locale === (locale ?? null);
150
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
151
+ "button",
152
+ {
153
+ type: "button",
154
+ "aria-current": selected ? "true" : void 0,
155
+ onClick: () => selected ? void 0 : switchTo(entry),
156
+ style: selected ? styles.tabSelected : entry.active ? styles.tab : styles.tabInactive,
157
+ title: entry.active ? void 0 : t("common.status.inactive"),
158
+ children: entry.locale ?? t("templates.defaultChip")
159
+ },
160
+ entry.locale ?? ""
161
+ );
162
+ }) })
163
+ ) : null,
164
+ onAddLocale ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
138
165
  "button",
139
166
  {
140
167
  type: "button",
141
168
  style: { ...styles.rollbackButton, marginLeft: "auto" },
142
- onClick: () => onAddLocale(type),
143
- children: "+ Add locale"
169
+ onClick: () => {
170
+ if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
171
+ onAddLocale(type, { locales: (0, import_client.missingTemplateLocales)(templates, type) });
172
+ },
173
+ children: t("templates.editor.addLocale")
144
174
  }
145
175
  ) : null
146
- ] }),
176
+ ] }) : null,
147
177
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.meta, children: [
148
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: locale ?? "default locale" }),
178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: locale ?? t("templates.defaultLocale") }),
149
179
  " \xB7 v",
150
180
  template?.version ?? 1,
151
- system ? " \xB7 built-in email: edit or roll back, it cannot be deleted" : ""
181
+ system ? ` \xB7 ${t("templates.editor.builtInNote")}` : ""
152
182
  ] }),
153
183
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.split, children: [
154
184
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.column, children: [
155
185
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { style: styles.form, onSubmit: handleSubmit, children: [
156
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-subject", children: "Subject" }),
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-subject", children: t("templates.subject") }),
157
187
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
158
188
  "input",
159
189
  {
@@ -163,7 +193,7 @@ function TemplateEditorScreen({
163
193
  onChange: (event) => setSubject(event.target.value)
164
194
  }
165
195
  ),
166
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-html", children: "HTML body" }),
196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-html", children: t("templates.htmlBody") }),
167
197
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
168
198
  "textarea",
169
199
  {
@@ -174,7 +204,7 @@ function TemplateEditorScreen({
174
204
  rows: 8
175
205
  }
176
206
  ),
177
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-text", children: "Plain-text body" }),
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-text", children: t("templates.textBody") }),
178
208
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
179
209
  "textarea",
180
210
  {
@@ -195,10 +225,10 @@ function TemplateEditorScreen({
195
225
  onChange: (event) => setActive(event.target.checked)
196
226
  }
197
227
  ),
198
- "Active"
228
+ t("templates.editor.active")
199
229
  ] }),
200
230
  saveError?.reason === "VERSION_CONFLICT" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.error, role: "alert", children: [
201
- "Someone changed this template since you opened it. Reload to see their change, then edit again.",
231
+ t("templates.editor.conflict"),
202
232
  " ",
203
233
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
204
234
  "button",
@@ -209,7 +239,7 @@ function TemplateEditorScreen({
209
239
  setSaveError(null);
210
240
  void refresh();
211
241
  },
212
- children: "Reload"
242
+ children: t("common.reload")
213
243
  }
214
244
  )
215
245
  ] }) : saveError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, role: "alert", children: saveError.explanation }) : null,
@@ -220,10 +250,10 @@ function TemplateEditorScreen({
220
250
  type: "submit",
221
251
  disabled: isSaving || !dirty,
222
252
  style: dirty ? styles.button : styles.buttonDisabled,
223
- children: isSaving ? "Saving\u2026" : "Save"
253
+ children: isSaving ? t("common.saving") : t("common.save")
224
254
  }
225
255
  ),
226
- !dirty && !isSaving && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.meta, children: "No changes to save" }),
256
+ !dirty && !isSaving && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.meta, children: t("templates.editor.noChanges") }),
227
257
  !system ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
228
258
  "button",
229
259
  {
@@ -237,12 +267,12 @@ function TemplateEditorScreen({
237
267
  color: "var(--fonderie-danger,#e00)",
238
268
  borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 40%, transparent)"
239
269
  },
240
- children: isDeleting ? "Deleting\u2026" : "Delete"
270
+ children: isDeleting ? t("common.deleting") : t("common.delete")
241
271
  }
242
272
  ) : null
243
273
  ] })
244
274
  ] }),
245
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-sample", children: "Sample data" }),
275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, htmlFor: "template-sample", children: t("templates.editor.sampleData") }),
246
276
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
247
277
  "textarea",
248
278
  {
@@ -258,8 +288,8 @@ function TemplateEditorScreen({
258
288
  ] }),
259
289
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.previewColumn, children: [
260
290
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.previewHeader, children: [
261
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.label, children: "Preview" }),
262
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.meta, children: isPreviewing ? "Rendering\u2026" : "Live" })
291
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.label, children: t("templates.editor.preview") }),
292
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.meta, children: isPreviewing ? t("templates.editor.rendering") : t("templates.editor.live") })
263
293
  ] }),
264
294
  previewError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, role: "alert", children: previewError.explanation }),
265
295
  preview?.subject && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.previewSubject, children: preview.subject }),
@@ -270,26 +300,26 @@ function TemplateEditorScreen({
270
300
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
271
301
  "iframe",
272
302
  {
273
- title: "Template preview",
303
+ title: t("templates.editor.previewTitle"),
274
304
  style: styles.previewFrame,
275
305
  sandbox: "",
276
306
  srcDoc: preview.html
277
307
  }
278
308
  )
279
- ) : preview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { style: styles.previewText, children: preview.text }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.meta, children: "Nothing rendered yet." })
309
+ ) : preview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { style: styles.previewText, children: preview.text }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.meta, children: t("templates.editor.nothingRendered") })
280
310
  ] })
281
311
  ] }),
282
312
  revisions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.revisions, children: [
283
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: styles.subtitle, children: "History" }),
313
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: styles.subtitle, children: t("templates.editor.history") }),
284
314
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.list, children: revisions.map((rev) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: styles.row, children: [
285
315
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
286
316
  "v",
287
317
  rev.version,
288
318
  " \u2014 ",
289
- rev.actor ?? "unknown",
319
+ rev.actor ?? t("templates.editor.unknownActor"),
290
320
  " \u2014",
291
321
  " ",
292
- new Date(rev.createdAt).toLocaleString()
322
+ (0, import_client.formatAdminDate)(rev.createdAt, uiLocale)
293
323
  ] }),
294
324
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
295
325
  "button",
@@ -297,15 +327,45 @@ function TemplateEditorScreen({
297
327
  type: "button",
298
328
  onClick: () => rollback(rev.version),
299
329
  style: styles.rollbackButton,
300
- children: "Roll back"
330
+ children: t("templates.editor.rollBack")
301
331
  }
302
332
  )
303
333
  ] }, rev.version)) })
304
334
  ] })
305
335
  ] });
306
336
  }
337
+ var tab = {
338
+ height: 32,
339
+ padding: "0 10px",
340
+ background: "none",
341
+ border: "none",
342
+ borderBottom: "2px solid transparent",
343
+ color: "var(--fonderie-text-muted,#5c5c5c)",
344
+ fontSize: 13,
345
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
346
+ cursor: "pointer"
347
+ };
307
348
  var styles = {
308
349
  container: { padding: "8px 40px 64px", maxWidth: 1400, boxSizing: "border-box" },
350
+ // One tab per locale of this email, underlined like a document's tabs.
351
+ tabBar: {
352
+ display: "flex",
353
+ alignItems: "flex-end",
354
+ gap: 12,
355
+ borderBottom: "1px solid var(--fonderie-border,#e0e0e0)",
356
+ margin: "4px 0 12px",
357
+ paddingBottom: 6
358
+ },
359
+ tabs: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: -7 },
360
+ tab,
361
+ tabSelected: {
362
+ ...tab,
363
+ color: "var(--fonderie-text,#171717)",
364
+ fontWeight: 600,
365
+ borderBottomColor: "var(--fonderie-text,#171717)",
366
+ cursor: "default"
367
+ },
368
+ tabInactive: { ...tab, opacity: 0.55, textDecoration: "line-through" },
309
369
  // Two columns, always: the editor on the left, the rendered result on the
310
370
  // right. Stacking the preview under a long form hid it below the fold.
311
371
  split: {
@@ -473,55 +533,75 @@ var styles = {
473
533
  };
474
534
 
475
535
  // src/screens/TemplateListScreen.tsx
536
+ var import_client2 = require("@fonderie/client");
476
537
  var import_react_courier_admin2 = require("@fonderie/react-courier-admin");
477
538
  var import_jsx_runtime2 = require("react/jsx-runtime");
478
539
  function TemplateListScreen({
479
540
  client,
480
541
  onSelectTemplate,
481
- onCreateTemplate
542
+ onCreateTemplate,
543
+ locale
482
544
  }) {
545
+ const t = (0, import_client2.createAdminT)(locale);
483
546
  const { templates, isLoading, error } = (0, import_react_courier_admin2.useTemplates)(client);
547
+ const groups = (0, import_client2.groupTemplatesByType)(templates);
484
548
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
485
549
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
486
550
  "div",
487
551
  {
488
552
  style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16 },
489
553
  children: [
490
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { style: styles2.title, children: "Templates" }),
554
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { style: styles2.title, children: t("templates.list.title") }),
491
555
  onCreateTemplate ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
492
556
  "button",
493
557
  {
494
558
  type: "button",
495
559
  onClick: () => onCreateTemplate({
496
560
  locales: [
497
- ...new Set(templates.map((t) => t.locale).filter((l) => !!l))
561
+ ...new Set(templates.map((row) => row.locale).filter((l) => !!l))
498
562
  ]
499
563
  }),
500
564
  style: styles2.newButton,
501
- children: "New template"
565
+ children: t("templates.list.newTemplate")
502
566
  }
503
567
  ) : null
504
568
  ]
505
569
  }
506
570
  ),
507
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.hint, children: "Every email the app sends. Open one to edit its copy and preview it live." }),
508
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.status, children: "Loading templates\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: styles2.list, children: templates.map((template) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { style: styles2.row, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
509
- "button",
510
- {
511
- type: "button",
512
- onClick: () => onSelectTemplate?.(template),
513
- style: styles2.rowButton,
514
- children: [
515
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.type, children: template.type }),
516
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.locale, children: template.locale ?? "default locale" }),
517
- template.system ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.builtIn, children: "built-in" }) : null,
518
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: template.active ? styles2.active : styles2.inactive, children: [
519
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.dot }),
520
- template.active ? "active" : "inactive"
521
- ] })
522
- ]
523
- }
524
- ) }, `${template.type}:${template.locale ?? "base"}`)) })
571
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.hint, children: t("templates.list.hint") }),
572
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.status, children: t("templates.list.loading") }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: styles2.list, children: groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { style: styles2.row, children: [
573
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
574
+ "button",
575
+ {
576
+ type: "button",
577
+ onClick: () => onSelectTemplate?.(group.primary),
578
+ style: styles2.rowButton,
579
+ children: [
580
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.type, children: group.type }),
581
+ group.system ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.builtIn, children: t("templates.builtInBadge") }) : null,
582
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: group.primary.active ? styles2.active : styles2.inactive, children: [
583
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.dot }),
584
+ group.primary.active ? t("common.status.active") : t("common.status.inactive")
585
+ ] })
586
+ ]
587
+ }
588
+ ),
589
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.chips, children: group.entries.map((entry) => {
590
+ const name = entry.locale ?? t("templates.defaultChip");
591
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
592
+ "button",
593
+ {
594
+ type: "button",
595
+ onClick: () => onSelectTemplate?.(entry),
596
+ style: entry.active ? styles2.chip : styles2.chipInactive,
597
+ "aria-label": t("templates.list.openLocale", { type: group.type, locale: name }),
598
+ title: entry.active ? void 0 : t("common.status.inactive"),
599
+ children: name
600
+ },
601
+ entry.locale ?? ""
602
+ );
603
+ }) })
604
+ ] }, group.type)) })
525
605
  ] });
526
606
  }
527
607
  var pill = {
@@ -535,6 +615,18 @@ var pill = {
535
615
  lineHeight: 1.5,
536
616
  minWidth: 64
537
617
  };
618
+ var chip = {
619
+ height: 24,
620
+ boxSizing: "border-box",
621
+ padding: "0 8px",
622
+ borderRadius: 6,
623
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
624
+ background: "var(--fonderie-surface,#fff)",
625
+ color: "var(--fonderie-text,#171717)",
626
+ fontSize: 12,
627
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
628
+ cursor: "pointer"
629
+ };
538
630
  var styles2 = {
539
631
  container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
540
632
  title: {
@@ -557,9 +649,16 @@ var styles2 = {
557
649
  overflow: "hidden",
558
650
  boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
559
651
  },
560
- row: { borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)" },
652
+ row: {
653
+ display: "flex",
654
+ alignItems: "center",
655
+ gap: 8,
656
+ paddingRight: 16,
657
+ borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)"
658
+ },
561
659
  rowButton: {
562
- width: "100%",
660
+ flex: 1,
661
+ minWidth: 0,
563
662
  display: "flex",
564
663
  justifyContent: "space-between",
565
664
  alignItems: "center",
@@ -578,7 +677,9 @@ var styles2 = {
578
677
  flex: 1,
579
678
  fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)'
580
679
  },
581
- locale: { fontSize: 12.5, color: "var(--fonderie-text-muted,#5c5c5c)" },
680
+ chips: { display: "flex", flexWrap: "wrap", justifyContent: "flex-end", gap: 6 },
681
+ chip,
682
+ chipInactive: { ...chip, opacity: 0.5, textDecoration: "line-through" },
582
683
  dot: { width: 6, height: 6, borderRadius: 999, background: "currentColor" },
583
684
  builtIn: {
584
685
  fontSize: 11,
@@ -616,7 +717,7 @@ var styles2 = {
616
717
  };
617
718
 
618
719
  // src/screens/TemplateCreateScreen.tsx
619
- var import_client = require("@fonderie/client");
720
+ var import_client3 = require("@fonderie/client");
620
721
  var import_react2 = require("react");
621
722
  var import_jsx_runtime3 = require("react/jsx-runtime");
622
723
  var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
@@ -625,11 +726,13 @@ function TemplateCreateScreen({
625
726
  client,
626
727
  type,
627
728
  locales = [],
628
- onCreated
729
+ onCreated,
730
+ locale
629
731
  }) {
732
+ const tr = (0, import_client3.createAdminT)(locale);
630
733
  const addingLocale = type !== void 0;
631
734
  const [newType, setNewType] = (0, import_react2.useState)("");
632
- const [locale, setLocale] = (0, import_react2.useState)("");
735
+ const [targetLocale, setTargetLocale] = (0, import_react2.useState)("");
633
736
  const [subject, setSubject] = (0, import_react2.useState)("");
634
737
  const [html, setHtml] = (0, import_react2.useState)("");
635
738
  const [text, setText] = (0, import_react2.useState)("");
@@ -647,21 +750,21 @@ function TemplateCreateScreen({
647
750
  e.preventDefault();
648
751
  setError(null);
649
752
  const t = addingLocale ? type : newType.trim();
650
- const l = locale.trim() || null;
651
- if (!TYPE_RE.test(t))
652
- return setError("Type: lowercase letters, digits, - and _ (e.g. weekly-digest).");
653
- if (addingLocale && !l) return setError("Choose the locale to add, e.g. fr or fr-CA.");
654
- if (l && !LOCALE_RE.test(l)) return setError("Locale: a language tag such as fr, es or fr-CA.");
655
- if (!text.trim())
656
- return setError("The plain-text body is required \u2014 it is what every email client can show.");
753
+ const l = targetLocale.trim() || null;
754
+ if (!TYPE_RE.test(t)) return setError(tr("templates.create.errorType"));
755
+ if (addingLocale && !l) return setError(tr("templates.create.errorLocaleRequired"));
756
+ if (l && !LOCALE_RE.test(l)) return setError(tr("templates.create.errorLocale"));
757
+ if (!text.trim()) return setError(tr("templates.create.errorText"));
657
758
  setBusy(true);
658
759
  try {
659
760
  const exists = await client.getTemplate(t, l).then(
660
761
  () => true,
661
- (err) => !(err instanceof import_client.FonderieApiError && err.status === 404)
762
+ (err) => !(err instanceof import_client3.FonderieApiError && err.status === 404)
662
763
  );
663
764
  if (exists) {
664
- setError(`"${t}"${l ? ` (${l})` : ""} already exists \u2014 open it from the list to edit.`);
765
+ setError(
766
+ l ? tr("templates.create.existsLocale", { type: t, locale: l }) : tr("templates.create.exists", { type: t })
767
+ );
665
768
  return;
666
769
  }
667
770
  const input = { text, active: true };
@@ -670,17 +773,17 @@ function TemplateCreateScreen({
670
773
  await client.setTemplate(t, input, l);
671
774
  onCreated?.({ type: t, locale: l });
672
775
  } catch (err) {
673
- setError(err instanceof import_client.FonderieApiError ? err.explanation : String(err));
776
+ setError(err instanceof import_client3.FonderieApiError ? err.explanation : String(err));
674
777
  } finally {
675
778
  setBusy(false);
676
779
  }
677
780
  };
678
781
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.container, children: [
679
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: styles3.title, children: addingLocale ? `Add a locale to ${type}` : "New template" }),
680
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.meta, children: addingLocale ? "Starts from the default copy. People whose locale matches receive this version; everyone else keeps the default." : "For an email your app sends that has no stored copy yet. The type must match what the app sends." }),
782
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: styles3.title, children: addingLocale ? tr("templates.create.titleLocale", { type }) : tr("templates.create.titleNew") }),
783
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.meta, children: addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew") }),
681
784
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { style: styles3.form, onSubmit: (e) => void submit(e), children: [
682
785
  !addingLocale ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
683
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-type", children: "Type" }),
786
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-type", children: tr("templates.create.type") }),
684
787
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
685
788
  "input",
686
789
  {
@@ -694,25 +797,22 @@ function TemplateCreateScreen({
694
797
  }
695
798
  )
696
799
  ] }) : null,
697
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: styles3.label, htmlFor: "new-template-locale", children: [
698
- "Locale ",
699
- addingLocale ? "" : "(optional \u2014 empty is the default)"
700
- ] }),
800
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-locale", children: addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional") }),
701
801
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
702
802
  "input",
703
803
  {
704
804
  id: "new-template-locale",
705
805
  style: styles3.input,
706
- value: locale,
806
+ value: targetLocale,
707
807
  placeholder: "fr",
708
808
  list: "new-template-locales",
709
809
  autoComplete: "off",
710
810
  spellCheck: false,
711
- onChange: (e) => setLocale(e.target.value)
811
+ onChange: (e) => setTargetLocale(e.target.value)
712
812
  }
713
813
  ),
714
814
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("datalist", { id: "new-template-locales", children: [...new Set(locales)].map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: l }, l)) }),
715
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-subject", children: "Subject" }),
815
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-subject", children: tr("templates.subject") }),
716
816
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
717
817
  "input",
718
818
  {
@@ -722,7 +822,7 @@ function TemplateCreateScreen({
722
822
  onChange: (e) => setSubject(e.target.value)
723
823
  }
724
824
  ),
725
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-html", children: "HTML body" }),
825
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-html", children: tr("templates.htmlBody") }),
726
826
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
727
827
  "textarea",
728
828
  {
@@ -732,7 +832,7 @@ function TemplateCreateScreen({
732
832
  onChange: (e) => setHtml(e.target.value)
733
833
  }
734
834
  ),
735
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-text", children: "Plain-text body" }),
835
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-text", children: tr("templates.textBody") }),
736
836
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
737
837
  "textarea",
738
838
  {
@@ -743,7 +843,7 @@ function TemplateCreateScreen({
743
843
  }
744
844
  ),
745
845
  error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.error, role: "alert", children: error }) : null,
746
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", disabled: busy, style: styles3.button, children: busy ? "Creating\u2026" : addingLocale ? "Add locale" : "Create template" })
846
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", disabled: busy, style: styles3.button, children: busy ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew") })
747
847
  ] })
748
848
  ] });
749
849
  }