@fonderie/vue-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
@@ -280,7 +280,9 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
280
280
  */
281
281
  type: { type: String, default: void 0 },
282
282
  /** Locales already in use, offered as suggestions. */
283
- locales: { type: Array, default: () => [] }
283
+ locales: { type: Array, default: () => [] },
284
+ /** The console language; defaults to English. */
285
+ locale: { type: String, default: void 0 }
284
286
  },
285
287
  emits: {
286
288
  /** What was created, so the caller can open it in the editor. */
@@ -289,12 +291,13 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
289
291
  setup(props, { emit }) {
290
292
  const addingLocale = props.type !== void 0;
291
293
  const newType = (0, import_vue.ref)("");
292
- const locale = (0, import_vue.ref)("");
294
+ const targetLocale = (0, import_vue.ref)("");
293
295
  const subject = (0, import_vue.ref)("");
294
296
  const html = (0, import_vue.ref)("");
295
297
  const text = (0, import_vue.ref)("");
296
298
  const error = (0, import_vue.ref)(null);
297
299
  const busy = (0, import_vue.ref)(false);
300
+ const tr = (key, params) => (0, import_client.createAdminT)(props.locale)(key, params);
298
301
  (0, import_vue.onMounted)(() => {
299
302
  if (!props.type) return;
300
303
  props.client.getTemplate(props.type, null).then(({ result }) => {
@@ -307,21 +310,21 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
307
310
  e.preventDefault();
308
311
  error.value = null;
309
312
  const t = addingLocale ? props.type : newType.value.trim();
310
- const l = locale.value.trim() || null;
313
+ const l = targetLocale.value.trim() || null;
311
314
  if (!TYPE_RE.test(t)) {
312
- error.value = "Type: lowercase letters, digits, - and _ (e.g. weekly-digest).";
315
+ error.value = tr("templates.create.errorType");
313
316
  return;
314
317
  }
315
318
  if (addingLocale && !l) {
316
- error.value = "Choose the locale to add, e.g. fr or fr-CA.";
319
+ error.value = tr("templates.create.errorLocaleRequired");
317
320
  return;
318
321
  }
319
322
  if (l && !LOCALE_RE.test(l)) {
320
- error.value = "Locale: a language tag such as fr, es or fr-CA.";
323
+ error.value = tr("templates.create.errorLocale");
321
324
  return;
322
325
  }
323
326
  if (!text.value.trim()) {
324
- error.value = "The plain-text body is required \u2014 it is what every email client can show.";
327
+ error.value = tr("templates.create.errorText");
325
328
  return;
326
329
  }
327
330
  busy.value = true;
@@ -331,7 +334,7 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
331
334
  (err) => !(err instanceof import_client.FonderieApiError && err.status === 404)
332
335
  );
333
336
  if (exists) {
334
- error.value = `"${t}"${l ? ` (${l})` : ""} already exists \u2014 open it from the list to edit.`;
337
+ error.value = l ? tr("templates.create.existsLocale", { type: t, locale: l }) : tr("templates.create.exists", { type: t });
335
338
  return;
336
339
  }
337
340
  const input = { text: text.value, active: true };
@@ -364,19 +367,19 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
364
367
  (0, import_vue.h)(
365
368
  "h1",
366
369
  { style: styles.title },
367
- addingLocale ? `Add a locale to ${props.type}` : "New template"
370
+ addingLocale ? tr("templates.create.titleLocale", { type: props.type }) : tr("templates.create.titleNew")
368
371
  ),
369
372
  (0, import_vue.h)(
370
373
  "p",
371
374
  { style: { ...styles.meta, lineHeight: 1.5 } },
372
- 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."
375
+ addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew")
373
376
  ),
374
377
  (0, import_vue.h)("form", { style: styles.form, onSubmit: (ev) => void submit(ev) }, [
375
- ...addingLocale ? [] : field("new-template-type", "Type", newType, { placeholder: "weekly-digest" }),
378
+ ...addingLocale ? [] : field("new-template-type", tr("templates.create.type"), newType, { placeholder: "weekly-digest" }),
376
379
  ...field(
377
380
  "new-template-locale",
378
- addingLocale ? "Locale" : "Locale (optional \u2014 empty is the default)",
379
- locale,
381
+ addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional"),
382
+ targetLocale,
380
383
  { placeholder: "fr", list: "new-template-locales" }
381
384
  ),
382
385
  (0, import_vue.h)(
@@ -384,9 +387,9 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
384
387
  { id: "new-template-locales" },
385
388
  [...new Set(props.locales)].map((l) => (0, import_vue.h)("option", { key: l, value: l }))
386
389
  ),
387
- ...field("new-template-subject", "Subject", subject),
388
- ...field("new-template-html", "HTML body", html, { textarea: true, minHeight: "160px" }),
389
- ...field("new-template-text", "Plain-text body", text, {
390
+ ...field("new-template-subject", tr("templates.subject"), subject),
391
+ ...field("new-template-html", tr("templates.htmlBody"), html, { textarea: true, minHeight: "160px" }),
392
+ ...field("new-template-text", tr("templates.textBody"), text, {
390
393
  textarea: true,
391
394
  minHeight: "100px"
392
395
  }),
@@ -398,7 +401,7 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
398
401
  disabled: busy.value,
399
402
  style: { ...styles.button, marginTop: "16px" }
400
403
  },
401
- busy.value ? "Creating\u2026" : addingLocale ? "Add locale" : "Create template"
404
+ busy.value ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew")
402
405
  )
403
406
  ])
404
407
  ]);
@@ -406,10 +409,39 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
406
409
  });
407
410
 
408
411
  // src/screens/TemplateEditorScreen.ts
412
+ var import_client2 = require("@fonderie/client");
409
413
  var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
410
414
  var import_vue2 = require("vue");
411
415
  var PREVIEW_DEBOUNCE_MS = 500;
412
416
  var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
417
+ var tab = {
418
+ height: "32px",
419
+ padding: "0 10px",
420
+ background: "none",
421
+ border: "none",
422
+ borderBottom: "2px solid transparent",
423
+ color: "var(--fonderie-text-muted,#5c5c5c)",
424
+ fontSize: "13px",
425
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
426
+ cursor: "pointer"
427
+ };
428
+ var tabSelected = {
429
+ ...tab,
430
+ color: "var(--fonderie-text,#171717)",
431
+ fontWeight: 600,
432
+ borderBottomColor: "var(--fonderie-text,#171717)",
433
+ cursor: "default"
434
+ };
435
+ var tabInactive = { ...tab, opacity: 0.55, textDecoration: "line-through" };
436
+ var tabBar = {
437
+ display: "flex",
438
+ alignItems: "flex-end",
439
+ gap: "12px",
440
+ borderBottom: "1px solid var(--fonderie-border,#e0e0e0)",
441
+ margin: "4px 0 12px",
442
+ paddingBottom: "6px"
443
+ };
444
+ var tabs = { display: "flex", flexWrap: "wrap", gap: "4px", marginBottom: "-7px" };
413
445
  var TemplateEditorScreen = (0, import_vue2.defineComponent)({
414
446
  name: "FonderieTemplateEditorScreen",
415
447
  props: {
@@ -421,23 +453,39 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
421
453
  * be edited and rolled back, never deleted — Delete is not offered.
422
454
  */
423
455
  system: { type: Boolean, default: false },
424
- /** Shows "+ Add locale" on the default locale (emits add-locale). */
425
- allowAddLocale: { type: Boolean, default: false }
456
+ /** Shows "+ Add locale" (emits add-locale). */
457
+ allowAddLocale: { type: Boolean, default: false },
458
+ /**
459
+ * Shows a tab per locale this email exists in (emits select-locale).
460
+ * Unsaved edits are confirmed before switching away.
461
+ */
462
+ localeTabs: { type: Boolean, default: false },
463
+ /** The console language (`locale` is the template's); defaults to English. */
464
+ uiLocale: { type: String, default: void 0 }
426
465
  },
427
466
  emits: {
428
467
  saved: () => true,
429
468
  /** The template was deleted and no longer exists. */
430
469
  deleted: () => true,
431
- /** Create a translation of this template. */
432
- "add-locale": (_type) => true
470
+ /**
471
+ * Create a translation of this template. `locales` are the ones the app
472
+ * uses elsewhere that this email lacks — suggest them first.
473
+ */
474
+ "add-locale": (_type, _context) => true,
475
+ /** Open another locale of this email. */
476
+ "select-locale": (_template) => true
433
477
  },
434
478
  setup(props, { emit }) {
479
+ const t = (key, params) => (0, import_client2.createAdminT)(props.uiLocale)(key, params);
435
480
  const { template, isLoading, error, refresh } = (0, import_vue_courier_admin.useTemplate)(
436
481
  props.client,
437
482
  props.type,
438
483
  props.locale
439
484
  );
440
- const { saveTemplate, removeTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
485
+ const { templates, saveTemplate, removeTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
486
+ const siblings = (0, import_vue2.computed)(
487
+ () => (0, import_client2.groupTemplatesByType)(templates.value).find((g) => g.type === props.type)?.entries ?? []
488
+ );
441
489
  const isDeleting = (0, import_vue2.ref)(false);
442
490
  const { revisions, rollback } = (0, import_vue_courier_admin.useTemplateRevisions)(props.client, props.type, props.locale);
443
491
  const isSaving = (0, import_vue2.ref)(false);
@@ -466,13 +514,13 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
466
514
  try {
467
515
  const parsed = JSON.parse(sampleJson.value || "{}");
468
516
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
469
- sampleError.value = "Sample data must be a JSON object.";
517
+ sampleError.value = "templates.editor.sampleNotObject";
470
518
  return;
471
519
  }
472
520
  data = parsed;
473
521
  sampleError.value = null;
474
522
  } catch {
475
- sampleError.value = "Sample data is not valid JSON.";
523
+ sampleError.value = "templates.editor.sampleInvalid";
476
524
  return;
477
525
  }
478
526
  const result = await renderPreview(
@@ -522,10 +570,54 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
522
570
  isSaving.value = false;
523
571
  }
524
572
  }
573
+ const leave = () => !dirty.value || window.confirm(t("templates.editor.discardChanges"));
574
+ function renderTabBar() {
575
+ if (!props.localeTabs && !props.allowAddLocale) return null;
576
+ return (0, import_vue2.h)("div", { style: tabBar }, [
577
+ props.localeTabs ? (
578
+ // Each tab opens another stored row, so this is navigation, not an
579
+ // ARIA tablist: plain buttons, the open one marked aria-current.
580
+ (0, import_vue2.h)(
581
+ "nav",
582
+ { "aria-label": t("templates.editor.locales"), style: tabs },
583
+ siblings.value.map((entry) => {
584
+ const selected = entry.locale === (props.locale ?? null);
585
+ return (0, import_vue2.h)(
586
+ "button",
587
+ {
588
+ key: entry.locale ?? "",
589
+ type: "button",
590
+ "aria-current": selected ? "true" : void 0,
591
+ style: selected ? tabSelected : entry.active ? tab : tabInactive,
592
+ title: entry.active ? void 0 : t("common.status.inactive"),
593
+ onClick: () => {
594
+ if (!selected && leave()) emit("select-locale", entry);
595
+ }
596
+ },
597
+ entry.locale ?? t("templates.defaultChip")
598
+ );
599
+ })
600
+ )
601
+ ) : null,
602
+ props.allowAddLocale ? (0, import_vue2.h)(
603
+ "button",
604
+ {
605
+ type: "button",
606
+ style: { ...styles.rollbackButton, marginLeft: "auto" },
607
+ onClick: () => {
608
+ if (leave())
609
+ emit("add-locale", props.type, {
610
+ locales: (0, import_client2.missingTemplateLocales)(templates.value, props.type)
611
+ });
612
+ }
613
+ },
614
+ t("templates.editor.addLocale")
615
+ ) : null
616
+ ]);
617
+ }
525
618
  async function handleDelete() {
526
- const which = props.locale ? `the ${props.locale} version of "${props.type}"` : `"${props.type}"`;
527
- const fallback = props.locale ? " People in that locale will receive the default version." : "";
528
- if (!window.confirm(`Delete ${which}?${fallback}`)) return;
619
+ const message = props.locale ? t("templates.editor.confirmDeleteLocale", { locale: props.locale, type: props.type }) : t("templates.editor.confirmDelete", { type: props.type });
620
+ if (!window.confirm(message)) return;
529
621
  isDeleting.value = true;
530
622
  saveError.value = null;
531
623
  try {
@@ -541,45 +633,31 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
541
633
  return (0, import_vue2.h)("li", { key: rev.version, style: styles.revisionRow }, [
542
634
  (0, import_vue2.h)(
543
635
  "span",
544
- `v${rev.version} \u2014 ${rev.actor ?? "unknown"} \u2014 ${new Date(rev.createdAt).toLocaleString()}`
636
+ `v${rev.version} \u2014 ${rev.actor ?? t("templates.editor.unknownActor")} \u2014 ${(0, import_client2.formatAdminDate)(rev.createdAt, props.uiLocale)}`
545
637
  ),
546
638
  (0, import_vue2.h)(
547
639
  "button",
548
640
  { type: "button", style: styles.rollbackButton, onClick: () => rollback(rev.version) },
549
- "Roll back"
641
+ t("templates.editor.rollBack")
550
642
  )
551
643
  ]);
552
644
  }
553
645
  return () => {
554
- if (isLoading.value) return (0, import_vue2.h)("p", { style: styles.status }, "Loading template\u2026");
646
+ if (isLoading.value) return (0, import_vue2.h)("p", { style: styles.status }, t("templates.editor.loading"));
555
647
  if (error.value)
556
648
  return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
557
649
  return (0, import_vue2.h)("div", { style: styles.container }, [
558
- (0, import_vue2.h)(
559
- "div",
560
- { style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" } },
561
- [
562
- (0, import_vue2.h)("h1", { style: styles.title }, props.type),
563
- props.allowAddLocale && !props.locale ? (0, import_vue2.h)(
564
- "button",
565
- {
566
- type: "button",
567
- style: { ...styles.rollbackButton, marginLeft: "auto" },
568
- onClick: () => emit("add-locale", props.type)
569
- },
570
- "+ Add locale"
571
- ) : null
572
- ]
573
- ),
650
+ (0, import_vue2.h)("h1", { style: styles.title }, props.type),
651
+ renderTabBar(),
574
652
  (0, import_vue2.h)("p", { style: styles.meta }, [
575
- (0, import_vue2.h)("strong", props.locale ?? "default locale"),
653
+ (0, import_vue2.h)("strong", props.locale ?? t("templates.defaultLocale")),
576
654
  ` \xB7 v${template.value?.version ?? 1}`,
577
- props.system ? " \xB7 built-in email: edit or roll back, it cannot be deleted" : ""
655
+ props.system ? ` \xB7 ${t("templates.editor.builtInNote")}` : ""
578
656
  ]),
579
657
  (0, import_vue2.h)("div", { style: styles.split }, [
580
658
  (0, import_vue2.h)("div", { style: styles.column }, [
581
659
  (0, import_vue2.h)("form", { style: styles.form, onSubmit: handleSubmit }, [
582
- (0, import_vue2.h)("label", { style: styles.label, for: "template-subject" }, "Subject"),
660
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-subject" }, t("templates.subject")),
583
661
  (0, import_vue2.h)("input", {
584
662
  id: "template-subject",
585
663
  style: styles.input,
@@ -588,7 +666,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
588
666
  subject.value = e.target.value;
589
667
  }
590
668
  }),
591
- (0, import_vue2.h)("label", { style: styles.label, for: "template-html" }, "HTML body"),
669
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-html" }, t("templates.htmlBody")),
592
670
  (0, import_vue2.h)("textarea", {
593
671
  id: "template-html",
594
672
  style: styles.textarea,
@@ -598,7 +676,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
598
676
  html.value = e.target.value;
599
677
  }
600
678
  }),
601
- (0, import_vue2.h)("label", { style: styles.label, for: "template-text" }, "Plain-text body"),
679
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-text" }, t("templates.textBody")),
602
680
  (0, import_vue2.h)("textarea", {
603
681
  id: "template-text",
604
682
  style: styles.textarea,
@@ -617,10 +695,10 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
617
695
  active.value = e.target.checked;
618
696
  }
619
697
  }),
620
- "Active"
698
+ t("templates.editor.active")
621
699
  ]),
622
700
  saveError.value?.reason === "VERSION_CONFLICT" ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, [
623
- "Someone changed this template since you opened it. Reload to see their change, then edit again. ",
701
+ `${t("templates.editor.conflict")} `,
624
702
  (0, import_vue2.h)(
625
703
  "button",
626
704
  {
@@ -631,7 +709,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
631
709
  void refresh();
632
710
  }
633
711
  },
634
- "Reload"
712
+ t("common.reload")
635
713
  )
636
714
  ]) : saveError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, saveError.value.explanation) : null,
637
715
  (0, import_vue2.h)("div", { style: styles.saveRow }, [
@@ -642,9 +720,9 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
642
720
  disabled: isSaving.value || !dirty.value,
643
721
  style: dirty.value ? styles.button : styles.buttonDisabled
644
722
  },
645
- isSaving.value ? "Saving\u2026" : "Save"
723
+ isSaving.value ? t("common.saving") : t("common.save")
646
724
  ),
647
- !dirty.value && !isSaving.value ? (0, import_vue2.h)("span", { style: styles.meta }, "No changes to save") : null,
725
+ !dirty.value && !isSaving.value ? (0, import_vue2.h)("span", { style: styles.meta }, t("templates.editor.noChanges")) : null,
648
726
  !props.system ? (0, import_vue2.h)(
649
727
  "button",
650
728
  {
@@ -659,11 +737,15 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
659
737
  borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 40%, transparent)"
660
738
  }
661
739
  },
662
- isDeleting.value ? "Deleting\u2026" : "Delete"
740
+ isDeleting.value ? t("common.deleting") : t("common.delete")
663
741
  ) : null
664
742
  ])
665
743
  ]),
666
- (0, import_vue2.h)("label", { style: styles.label, for: "template-sample" }, "Sample data"),
744
+ (0, import_vue2.h)(
745
+ "label",
746
+ { style: styles.label, for: "template-sample" },
747
+ t("templates.editor.sampleData")
748
+ ),
667
749
  (0, import_vue2.h)("textarea", {
668
750
  id: "template-sample",
669
751
  style: styles.textarea,
@@ -674,12 +756,16 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
674
756
  sampleJson.value = e.target.value;
675
757
  }
676
758
  }),
677
- sampleError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, sampleError.value) : null
759
+ sampleError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, t(sampleError.value)) : null
678
760
  ]),
679
761
  (0, import_vue2.h)("div", { style: styles.previewColumn }, [
680
762
  (0, import_vue2.h)("div", { style: styles.previewHeader }, [
681
- (0, import_vue2.h)("span", { style: styles.label }, "Preview"),
682
- (0, import_vue2.h)("span", { style: styles.meta }, isPreviewing.value ? "Rendering\u2026" : "Live")
763
+ (0, import_vue2.h)("span", { style: styles.label }, t("templates.editor.preview")),
764
+ (0, import_vue2.h)(
765
+ "span",
766
+ { style: styles.meta },
767
+ isPreviewing.value ? t("templates.editor.rendering") : t("templates.editor.live")
768
+ )
683
769
  ]),
684
770
  previewError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
685
771
  preview.value?.subject ? (0, import_vue2.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
@@ -688,16 +774,16 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
688
774
  // must never execute in the dashboard's origin, where the admin
689
775
  // token lives.
690
776
  (0, import_vue2.h)("iframe", {
691
- title: "Template preview",
777
+ title: t("templates.editor.previewTitle"),
692
778
  style: styles.previewFrame,
693
779
  sandbox: "",
694
780
  srcdoc: preview.value.html
695
781
  })
696
- ) : preview.value ? (0, import_vue2.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue2.h)("p", { style: styles.meta }, "Nothing rendered yet.")
782
+ ) : preview.value ? (0, import_vue2.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue2.h)("p", { style: styles.meta }, t("templates.editor.nothingRendered"))
697
783
  ])
698
784
  ]),
699
785
  revisions.value.length > 0 ? (0, import_vue2.h)("div", { style: styles.revisions }, [
700
- (0, import_vue2.h)("h2", { style: styles.subtitle }, "History"),
786
+ (0, import_vue2.h)("h2", { style: styles.subtitle }, t("templates.editor.history")),
701
787
  (0, import_vue2.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
702
788
  ]) : null
703
789
  ]);
@@ -706,6 +792,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
706
792
  });
707
793
 
708
794
  // src/screens/TemplateListScreen.ts
795
+ var import_client3 = require("@fonderie/client");
709
796
  var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
710
797
  var import_vue3 = require("vue");
711
798
  var builtIn = {
@@ -716,6 +803,33 @@ var builtIn = {
716
803
  border: "1px solid var(--fonderie-border,#e0e0e0)",
717
804
  color: "var(--fonderie-text-muted,#5c5c5c)"
718
805
  };
806
+ var chip = {
807
+ height: "24px",
808
+ boxSizing: "border-box",
809
+ padding: "0 8px",
810
+ borderRadius: "6px",
811
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
812
+ background: "var(--fonderie-surface,#fff)",
813
+ color: "var(--fonderie-text,#171717)",
814
+ fontSize: "12px",
815
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
816
+ cursor: "pointer"
817
+ };
818
+ var chipInactive = { ...chip, opacity: 0.5, textDecoration: "line-through" };
819
+ var row = {
820
+ ...styles.row,
821
+ display: "flex",
822
+ alignItems: "center",
823
+ gap: "8px",
824
+ paddingRight: "16px"
825
+ };
826
+ var rowButton = { ...styles.rowButton, width: "auto", flex: "1", minWidth: "0" };
827
+ var chips = {
828
+ display: "flex",
829
+ flexWrap: "wrap",
830
+ justifyContent: "flex-end",
831
+ gap: "6px"
832
+ };
719
833
  var newButton = {
720
834
  display: "inline-flex",
721
835
  alignItems: "center",
@@ -736,34 +850,60 @@ var TemplateListScreen = (0, import_vue3.defineComponent)({
736
850
  props: {
737
851
  client: { type: Object, required: true },
738
852
  /** Shows a "New template" button (emits create-template). */
739
- allowCreate: { type: Boolean, default: false }
853
+ allowCreate: { type: Boolean, default: false },
854
+ /** The console language; defaults to English. */
855
+ locale: { type: String, default: void 0 }
740
856
  },
741
857
  emits: {
742
- /** The whole row — type AND locale, and whether it is built-in. */
858
+ /**
859
+ * The whole row — type AND locale, and whether it is built-in. One row
860
+ * per email: the row opens the default locale, a locale chip that locale.
861
+ */
743
862
  "select-template": (_template) => true,
744
863
  /** Receives the locales in use, to suggest. */
745
864
  "create-template": (_context) => true
746
865
  },
747
866
  setup(props, { emit }) {
748
867
  const { templates, isLoading, error } = (0, import_vue_courier_admin2.useTemplates)(props.client);
749
- function renderRow(template) {
750
- return (0, import_vue3.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
868
+ const t = (key, params) => (0, import_client3.createAdminT)(props.locale)(key, params);
869
+ function renderRow(group) {
870
+ const primary = group.primary;
871
+ return (0, import_vue3.h)("li", { key: group.type, style: row }, [
751
872
  (0, import_vue3.h)(
752
873
  "button",
753
874
  {
754
875
  type: "button",
755
- style: styles.rowButton,
756
- onClick: () => emit("select-template", template)
876
+ style: rowButton,
877
+ onClick: () => emit("select-template", primary)
757
878
  },
758
879
  [
759
- (0, import_vue3.h)("span", { style: styles.type }, template.type),
760
- (0, import_vue3.h)("span", { style: styles.locale }, template.locale ?? "default locale"),
761
- template.system ? (0, import_vue3.h)("span", { style: builtIn }, "built-in") : null,
762
- (0, import_vue3.h)("span", { style: template.active ? styles.active : styles.inactive }, [
880
+ (0, import_vue3.h)("span", { style: styles.type }, group.type),
881
+ group.system ? (0, import_vue3.h)("span", { style: builtIn }, t("templates.builtInBadge")) : null,
882
+ (0, import_vue3.h)("span", { style: primary.active ? styles.active : styles.inactive }, [
763
883
  (0, import_vue3.h)("span", { style: styles.dot }),
764
- template.active ? "active" : "inactive"
884
+ primary.active ? t("common.status.active") : t("common.status.inactive")
765
885
  ])
766
886
  ]
887
+ ),
888
+ // Siblings of the row button, not inside it: a button cannot hold buttons.
889
+ (0, import_vue3.h)(
890
+ "span",
891
+ { style: chips },
892
+ group.entries.map((entry) => {
893
+ const name = entry.locale ?? t("templates.defaultChip");
894
+ return (0, import_vue3.h)(
895
+ "button",
896
+ {
897
+ key: entry.locale ?? "",
898
+ type: "button",
899
+ style: entry.active ? chip : chipInactive,
900
+ "aria-label": t("templates.list.openLocale", { type: group.type, locale: name }),
901
+ title: entry.active ? void 0 : t("common.status.inactive"),
902
+ onClick: () => emit("select-template", entry)
903
+ },
904
+ name
905
+ );
906
+ })
767
907
  )
768
908
  ]);
769
909
  }
@@ -779,7 +919,7 @@ var TemplateListScreen = (0, import_vue3.defineComponent)({
779
919
  }
780
920
  },
781
921
  [
782
- (0, import_vue3.h)("h1", { style: styles.listTitle }, "Templates"),
922
+ (0, import_vue3.h)("h1", { style: styles.listTitle }, t("templates.list.title")),
783
923
  props.allowCreate ? (0, import_vue3.h)(
784
924
  "button",
785
925
  {
@@ -788,21 +928,17 @@ var TemplateListScreen = (0, import_vue3.defineComponent)({
788
928
  onClick: () => emit("create-template", {
789
929
  locales: [
790
930
  ...new Set(
791
- templates.value.map((t) => t.locale).filter((l) => !!l)
931
+ templates.value.map((tpl) => tpl.locale).filter((l) => !!l)
792
932
  )
793
933
  ]
794
934
  })
795
935
  },
796
- "New template"
936
+ t("templates.list.newTemplate")
797
937
  ) : null
798
938
  ]
799
939
  ),
800
- (0, import_vue3.h)(
801
- "p",
802
- { style: styles.hint },
803
- "Every email the app sends. Open one to edit its copy and preview it live."
804
- ),
805
- isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, "Loading templates\u2026") : error.value ? (0, import_vue3.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : (0, import_vue3.h)("ul", { style: styles.list }, templates.value.map(renderRow))
940
+ (0, import_vue3.h)("p", { style: styles.hint }, t("templates.list.hint")),
941
+ isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, t("templates.list.loading")) : error.value ? (0, import_vue3.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : (0, import_vue3.h)("ul", { style: styles.list }, (0, import_client3.groupTemplatesByType)(templates.value).map(renderRow))
806
942
  ]);
807
943
  }
808
944
  });