@fonderie/vue-admin-screens 1.4.0 → 1.6.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.js CHANGED
@@ -1,6 +1,13 @@
1
1
  // src/screens/AdminShell.ts
2
+ import {
3
+ createAdminT as createAdminT16
4
+ } from "@fonderie/client";
2
5
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
3
- import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
6
+ import {
7
+ TemplateCreateScreen,
8
+ TemplateEditorScreen,
9
+ TemplateListScreen
10
+ } from "@fonderie/vue-courier-admin-screens";
4
11
  import { computed as computed3, defineComponent as defineComponent15, h as h17, onBeforeUnmount, onMounted, ref as ref9 } from "vue";
5
12
 
6
13
  // src/styles.ts
@@ -557,10 +564,10 @@ function empty(title, body, iconName = "check") {
557
564
  body ? h("div", body) : null
558
565
  ]);
559
566
  }
560
- function refreshButton(onClick, busy, label = "Refresh") {
567
+ function refreshButton(onClick, busy, label = "Refresh", busyLabel = "Working\u2026") {
561
568
  return h("button", { type: "button", style: styles.button, onClick, disabled: busy }, [
562
569
  icon("refresh", 14),
563
- busy ? "Working\u2026" : label
570
+ busy ? busyLabel : label
564
571
  ]);
565
572
  }
566
573
  function methodChip(method) {
@@ -587,6 +594,10 @@ function methodChip(method) {
587
594
  }
588
595
 
589
596
  // src/screens/AdminLogScreen.ts
597
+ import {
598
+ createAdminT,
599
+ formatAdminDate
600
+ } from "@fonderie/client";
590
601
  import { useAdminLog } from "@fonderie/vue-admin";
591
602
  import { defineComponent, h as h3 } from "vue";
592
603
 
@@ -614,15 +625,11 @@ function table(headers, rows) {
614
625
  ]);
615
626
  }
616
627
  var td = (children, extra = {}) => h2("td", { style: { ...styles.td, ...extra } }, children);
617
- var refreshButton2 = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
628
+ var refreshButton2 = (label, disabled, onClick, busyLabel = "Working\u2026") => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
618
629
  icon("refresh", 14),
619
- disabled ? "Working\u2026" : label
630
+ disabled ? busyLabel : label
620
631
  ]);
621
- var loadMoreButton = (onClick) => h2(
622
- "button",
623
- { type: "button", style: { ...styles.button, marginTop: "12px" }, onClick },
624
- "Load more"
625
- );
632
+ var loadMoreButton = (onClick, label = "Load more") => h2("button", { type: "button", style: { ...styles.button, marginTop: "12px" }, onClick }, label);
626
633
  var actionButton = (label, disabled, onClick, style = styles.button) => h2("button", { type: "button", style, disabled, onClick }, label);
627
634
 
628
635
  // src/screens/AdminLogScreen.ts
@@ -630,69 +637,90 @@ var AdminLogScreen = defineComponent({
630
637
  name: "FonderieAdminLogScreen",
631
638
  props: {
632
639
  client: { type: Object, required: true },
633
- pageSize: { type: Number, default: 50 }
640
+ pageSize: { type: Number, default: 50 },
641
+ locale: { type: String, default: void 0 }
634
642
  },
635
643
  setup(props) {
636
644
  const { entries, hasMore, isLoading, error, refresh, loadMore } = useAdminLog(props.client, {
637
645
  limit: props.pageSize
638
646
  });
639
- return () => h3("div", { style: styles.container }, [
640
- pageHeader(
641
- "Admin log",
642
- "Every request made through this surface, newest first \u2014 refused ones included.",
643
- [refreshButton2("Refresh", isLoading.value, () => void refresh())]
644
- ),
645
- error.value ? h3(
646
- "p",
647
- { style: styles.error, role: "alert" },
648
- error.value.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.value.explanation
649
- ) : entries.value.length === 0 && !isLoading.value ? empty("No requests yet", void 0, "log") : [
650
- table(
651
- ["When", "Actor", "Request", "Status", "Module"],
652
- entries.value.map(
653
- (e) => h3("tr", { key: e.id }, [
654
- td(new Date(e.at).toLocaleString(), {
655
- ...styles.muted,
656
- whiteSpace: "nowrap"
657
- }),
658
- td(e.actor),
659
- td([
660
- methodChip(e.method),
661
- " ",
662
- h3("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.path)
663
- ]),
664
- td(
665
- [
666
- pill(
667
- e.status >= 500 ? "bad" : e.status >= 400 ? "warn" : "ok",
668
- String(e.status)
669
- ),
670
- " ",
671
- h3("span", { style: styles.muted }, `${e.durationMs} ms`)
672
- ],
673
- { whiteSpace: "nowrap" }
674
- ),
675
- td(e.module, styles.muted)
676
- ])
647
+ return () => {
648
+ const t2 = createAdminT(props.locale);
649
+ return h3("div", { style: styles.container }, [
650
+ pageHeader(t2("log.title"), t2("log.lead"), [
651
+ refreshButton2(
652
+ t2("common.refresh"),
653
+ isLoading.value,
654
+ () => void refresh(),
655
+ t2("common.working")
677
656
  )
678
- ),
679
- isLoading.value ? h3("p", { style: styles.status }, "Loading\u2026") : null,
680
- hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
681
- ]
682
- ]);
657
+ ]),
658
+ error.value ? h3(
659
+ "p",
660
+ { style: styles.error, role: "alert" },
661
+ error.value.status === 404 ? t2("log.off") : error.value.explanation
662
+ ) : entries.value.length === 0 && !isLoading.value ? empty(t2("log.empty"), void 0, "log") : [
663
+ table(
664
+ [
665
+ t2("log.colWhen"),
666
+ t2("log.colActor"),
667
+ t2("log.colRequest"),
668
+ t2("log.colStatus"),
669
+ t2("log.colModule")
670
+ ],
671
+ entries.value.map(
672
+ (e) => h3("tr", { key: e.id }, [
673
+ td(formatAdminDate(e.at, props.locale), {
674
+ ...styles.muted,
675
+ whiteSpace: "nowrap"
676
+ }),
677
+ td(e.actor),
678
+ td([
679
+ methodChip(e.method),
680
+ " ",
681
+ h3("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.path)
682
+ ]),
683
+ td(
684
+ [
685
+ pill(
686
+ e.status >= 500 ? "bad" : e.status >= 400 ? "warn" : "ok",
687
+ String(e.status)
688
+ ),
689
+ " ",
690
+ h3("span", { style: styles.muted }, `${e.durationMs} ms`)
691
+ ],
692
+ { whiteSpace: "nowrap" }
693
+ ),
694
+ td(e.module, styles.muted)
695
+ ])
696
+ )
697
+ ),
698
+ isLoading.value ? h3("p", { style: styles.status }, t2("common.loading")) : null,
699
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore(), t2("common.loadMore")) : null
700
+ ]
701
+ ]);
702
+ };
683
703
  }
684
704
  });
685
705
 
686
706
  // src/screens/AttentionScreen.ts
707
+ import {
708
+ createAdminT as createAdminT2,
709
+ formatAdminDate as formatAdminDate2
710
+ } from "@fonderie/client";
687
711
  import { useAttention, useManifest } from "@fonderie/vue-admin";
688
712
  import { defineComponent as defineComponent2, h as h4 } from "vue";
689
713
  var AttentionScreen = defineComponent2({
690
714
  name: "FonderieAttentionScreen",
691
- props: { client: { type: Object, required: true } },
715
+ props: {
716
+ client: { type: Object, required: true },
717
+ locale: { type: String, default: void 0 }
718
+ },
692
719
  setup(props) {
693
720
  const { attention, isLoading, error, refresh } = useAttention(props.client);
694
721
  const { manifest } = useManifest(props.client);
695
722
  return () => {
723
+ const t2 = createAdminT2(props.locale);
696
724
  const a = attention.value;
697
725
  const m = manifest.value;
698
726
  const errors = a?.items.filter((i) => i.severity === "error") ?? [];
@@ -701,34 +729,39 @@ var AttentionScreen = defineComponent2({
701
729
  const total = m?.modules.length ?? 0;
702
730
  const tiles = h4("div", { style: styles.grid }, [
703
731
  stat(
704
- "Needs action",
732
+ t2("attention.needsAction"),
705
733
  a ? errors.length : "\u2014",
706
- "errors to fix",
734
+ t2("attention.needsActionHint"),
707
735
  errors.length ? "bad" : a ? "ok" : void 0
708
736
  ),
709
- stat("Advice", a ? advice.length : "\u2014", "worth a look", advice.length ? "warn" : void 0),
710
737
  stat(
711
- "Modules ready",
738
+ t2("attention.advice"),
739
+ a ? advice.length : "\u2014",
740
+ t2("attention.adviceHint"),
741
+ advice.length ? "warn" : void 0
742
+ ),
743
+ stat(
744
+ t2("attention.modulesReady"),
712
745
  m ? `${ready}/${total}` : "\u2014",
713
- m ? `admin ${m.admin.version}` : void 0,
746
+ m ? t2("attention.modulesReadyHint", { version: m.admin.version }) : void 0,
714
747
  m && ready < total ? "warn" : m ? "ok" : void 0
715
748
  ),
716
- stat("Routes", m ? m.routes.length : "\u2014", m ? m.env : void 0)
749
+ stat(t2("attention.routes"), m ? m.routes.length : "\u2014", m ? m.env : void 0)
717
750
  ]);
718
751
  return page(
719
- "Attention",
752
+ t2("attention.title"),
720
753
  // Tiles show while the checks run; only the list waits.
721
754
  { isLoading: { value: false }, error },
722
755
  () => {
723
756
  if (isLoading.value && !a)
724
- return [tiles, h4("p", { style: styles.status }, "Running the checks\u2026")];
757
+ return [tiles, h4("p", { style: styles.status }, t2("attention.running"))];
725
758
  if (!a) return tiles;
726
759
  if (a.items.length === 0)
727
760
  return [
728
761
  tiles,
729
762
  empty(
730
- "Nothing needs you",
731
- `Every check passes. Checked ${new Date(a.generatedAt).toLocaleString()}.`
763
+ t2("attention.emptyTitle"),
764
+ t2("attention.emptyBody", { time: formatAdminDate2(a.generatedAt, props.locale) })
732
765
  )
733
766
  ];
734
767
  return [
@@ -768,7 +801,7 @@ var AttentionScreen = defineComponent2({
768
801
  [
769
802
  pill(
770
803
  item.severity === "error" ? "bad" : "warn",
771
- item.severity === "error" ? "error" : "advice"
804
+ item.severity === "error" ? t2("common.status.error") : t2("common.status.advice")
772
805
  ),
773
806
  h4("span", { style: styles.mono }, item.source)
774
807
  ]
@@ -781,9 +814,18 @@ var AttentionScreen = defineComponent2({
781
814
  )
782
815
  ];
783
816
  },
784
- [refreshButton2("Refresh", isLoading.value, () => void refresh())],
817
+ [
818
+ refreshButton2(
819
+ t2("common.refresh"),
820
+ isLoading.value,
821
+ () => void refresh(),
822
+ t2("common.working")
823
+ )
824
+ ],
785
825
  {
786
- lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
826
+ lead: a ? t2("attention.leadChecked", {
827
+ time: formatAdminDate2(a.generatedAt, props.locale, "time")
828
+ }) : t2("attention.lead")
787
829
  }
788
830
  );
789
831
  };
@@ -791,28 +833,29 @@ var AttentionScreen = defineComponent2({
791
833
  });
792
834
 
793
835
  // src/screens/EnvironmentScreen.ts
836
+ import { createAdminT as createAdminT3 } from "@fonderie/client";
794
837
  import { useAdminEnvironment } from "@fonderie/vue-admin";
795
838
  import { defineComponent as defineComponent3, h as h5 } from "vue";
796
839
  var EnvironmentScreen = defineComponent3({
797
840
  name: "FonderieEnvironmentScreen",
798
- props: { client: { type: Object, required: true } },
841
+ props: {
842
+ client: { type: Object, required: true },
843
+ locale: { type: String, default: void 0 }
844
+ },
799
845
  setup(props) {
800
846
  const { report, isLoading, error } = useAdminEnvironment(props.client);
801
847
  return () => {
848
+ const t2 = createAdminT3(props.locale);
802
849
  const r = report.value;
803
850
  const missing = r?.env.filter((e) => !e.set).length ?? 0;
804
851
  return page(
805
- "Environment",
852
+ t2("environment.title"),
806
853
  { isLoading, error },
807
854
  () => {
808
855
  if (!r) return null;
809
856
  return [
810
- h5("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Variables"),
811
- r.env.length === 0 ? h5(
812
- "p",
813
- { style: styles.muted },
814
- "No variables declared \u2014 pass `env` to AdminModule."
815
- ) : h5(
857
+ h5("h2", { style: { ...styles.subtitle, marginTop: 0 } }, t2("environment.variables")),
858
+ r.env.length === 0 ? h5("p", { style: styles.muted }, t2("environment.noVariables")) : h5(
816
859
  "div",
817
860
  {
818
861
  style: {
@@ -837,12 +880,15 @@ var EnvironmentScreen = defineComponent3({
837
880
  },
838
881
  [
839
882
  h5("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.name),
840
- pill(e.set ? "ok" : "bad", e.set ? "set" : "missing")
883
+ pill(
884
+ e.set ? "ok" : "bad",
885
+ e.set ? t2("common.status.set") : t2("common.status.missing")
886
+ )
841
887
  ]
842
888
  )
843
889
  )
844
890
  ),
845
- h5("h2", { style: styles.subtitle }, "Module readiness"),
891
+ h5("h2", { style: styles.subtitle }, t2("environment.moduleReadiness")),
846
892
  h5(
847
893
  "ul",
848
894
  { style: styles.list },
@@ -862,7 +908,7 @@ var EnvironmentScreen = defineComponent3({
862
908
  h5("span", { style: { ...styles.mono, fontWeight: 600 } }, m.name),
863
909
  pill(
864
910
  m.problems.length === 0 ? "ok" : m.problems.some((p) => p.severity === "error") ? "bad" : "warn",
865
- m.problems.length === 0 ? "ready" : `${m.problems.length} problem${m.problems.length === 1 ? "" : "s"}`
911
+ m.problems.length === 0 ? t2("common.status.ready") : m.problems.length === 1 ? t2("environment.problemOne") : t2("environment.problemMany", { n: m.problems.length })
866
912
  )
867
913
  ]
868
914
  ),
@@ -886,10 +932,14 @@ var EnvironmentScreen = defineComponent3({
886
932
  ];
887
933
  },
888
934
  [
889
- r && r.env.length > 0 ? pill(missing ? "bad" : "ok", missing ? `${missing} missing` : "all set") : null
935
+ r && r.env.length > 0 ? pill(
936
+ missing ? "bad" : "ok",
937
+ missing ? t2("environment.missingCount", { n: missing }) : t2("environment.allSet")
938
+ ) : null
890
939
  ],
891
940
  {
892
- lead: "Whether each module is configured and each variable the app reads is set. Values are never shown."
941
+ loadingText: t2("common.loading"),
942
+ lead: t2("environment.lead")
893
943
  }
894
944
  );
895
945
  };
@@ -897,103 +947,131 @@ var EnvironmentScreen = defineComponent3({
897
947
  });
898
948
 
899
949
  // src/screens/DoctorScreen.ts
950
+ import { createAdminT as createAdminT4 } from "@fonderie/client";
900
951
  import { useDoctor } from "@fonderie/vue-admin";
901
952
  import { defineComponent as defineComponent4, h as h6 } from "vue";
902
953
  var DoctorScreen = defineComponent4({
903
954
  name: "FonderieDoctorScreen",
904
- props: { client: { type: Object, required: true } },
955
+ props: {
956
+ client: { type: Object, required: true },
957
+ locale: { type: String, default: void 0 }
958
+ },
905
959
  setup(props) {
906
960
  const { report, isLoading, error, refresh } = useDoctor(props.client);
907
- return () => page(
908
- "Doctor",
909
- { isLoading: { value: isLoading.value && !report.value }, error },
910
- () => {
911
- const r = report.value;
912
- if (!r) return null;
913
- return table(
914
- [
915
- { label: "Result", style: { width: "110px" } },
916
- "Check",
917
- { label: "Time", style: { textAlign: "right", width: "90px" } }
918
- ],
919
- r.checks.map(
920
- (c) => h6("tr", { key: c.name }, [
921
- td(
922
- pill(
923
- c.skipped ? "neutral" : c.ok ? c.findings.length ? "warn" : "ok" : "bad",
924
- c.skipped ? "skipped" : c.ok ? c.findings.length ? "advice" : "ok" : "failed"
925
- )
926
- ),
927
- td([
928
- h6("div", [
929
- h6("span", { style: { ...styles.mono, fontWeight: 600 } }, c.name),
930
- " ",
931
- h6("span", { style: styles.muted }, c.module)
932
- ]),
933
- c.skipped ? h6("div", { style: { ...styles.muted, marginTop: "4px" } }, c.skipped) : null,
934
- ...c.findings.map(
935
- (f) => h6(
936
- "div",
937
- {
938
- key: f,
939
- style: {
940
- marginTop: "4px",
941
- fontSize: "13px",
942
- lineHeight: 1.55,
943
- color: c.ok ? "var(--fonderie-text,#171717)" : "var(--fonderie-danger,#e00)"
944
- }
945
- },
946
- f
961
+ return () => {
962
+ const t2 = createAdminT4(props.locale);
963
+ return page(
964
+ t2("doctor.title"),
965
+ { isLoading: { value: isLoading.value && !report.value }, error },
966
+ () => {
967
+ const r = report.value;
968
+ if (!r) return null;
969
+ return table(
970
+ [
971
+ { label: t2("doctor.colResult"), style: { width: "110px" } },
972
+ t2("doctor.colCheck"),
973
+ { label: t2("doctor.colTime"), style: { textAlign: "right", width: "90px" } }
974
+ ],
975
+ r.checks.map(
976
+ (c) => h6("tr", { key: c.name }, [
977
+ td(
978
+ pill(
979
+ c.skipped ? "neutral" : c.ok ? c.findings.length ? "warn" : "ok" : "bad",
980
+ c.skipped ? t2("common.status.skipped") : c.ok ? c.findings.length ? t2("common.status.advice") : t2("common.status.ok") : t2("common.status.failed")
947
981
  )
948
- )
949
- ]),
950
- td(`${c.durationMs} ms`, {
951
- ...styles.muted,
952
- textAlign: "right",
953
- whiteSpace: "nowrap"
954
- })
955
- ])
982
+ ),
983
+ td([
984
+ h6("div", [
985
+ h6("span", { style: { ...styles.mono, fontWeight: 600 } }, c.name),
986
+ " ",
987
+ h6("span", { style: styles.muted }, c.module)
988
+ ]),
989
+ c.skipped ? h6("div", { style: { ...styles.muted, marginTop: "4px" } }, c.skipped) : null,
990
+ ...c.findings.map(
991
+ (f) => h6(
992
+ "div",
993
+ {
994
+ key: f,
995
+ style: {
996
+ marginTop: "4px",
997
+ fontSize: "13px",
998
+ lineHeight: 1.55,
999
+ color: c.ok ? "var(--fonderie-text,#171717)" : "var(--fonderie-danger,#e00)"
1000
+ }
1001
+ },
1002
+ f
1003
+ )
1004
+ )
1005
+ ]),
1006
+ td(`${c.durationMs} ms`, {
1007
+ ...styles.muted,
1008
+ textAlign: "right",
1009
+ whiteSpace: "nowrap"
1010
+ })
1011
+ ])
1012
+ )
1013
+ );
1014
+ },
1015
+ [
1016
+ report.value && !isLoading.value ? pill(
1017
+ report.value.ok ? "ok" : "bad",
1018
+ report.value.ok ? t2("doctor.allPass") : t2("doctor.oneFailed")
1019
+ ) : null,
1020
+ refreshButton2(
1021
+ t2("doctor.runAgain"),
1022
+ isLoading.value,
1023
+ () => void refresh(),
1024
+ t2("common.working")
956
1025
  )
957
- );
958
- },
959
- [
960
- report.value && !isLoading.value ? pill(
961
- report.value.ok ? "ok" : "bad",
962
- report.value.ok ? "all checks pass" : "a check failed"
963
- ) : null,
964
- refreshButton2("Run again", isLoading.value, () => void refresh())
965
- ],
966
- {
967
- loadingText: "Running the checks\u2026",
968
- lead: "Every reconciliation check across the installed modules, run on demand."
969
- }
970
- );
1026
+ ],
1027
+ {
1028
+ loadingText: t2("doctor.running"),
1029
+ lead: t2("doctor.lead")
1030
+ }
1031
+ );
1032
+ };
971
1033
  }
972
1034
  });
973
1035
 
974
1036
  // src/screens/ModulesScreen.ts
1037
+ import { createAdminT as createAdminT5 } from "@fonderie/client";
975
1038
  import { useManifest as useManifest2 } from "@fonderie/vue-admin";
976
1039
  import { defineComponent as defineComponent5, h as h7 } from "vue";
977
1040
  var ModulesScreen = defineComponent5({
978
1041
  name: "FonderieModulesScreen",
979
- props: { client: { type: Object, required: true } },
1042
+ props: {
1043
+ client: { type: Object, required: true },
1044
+ locale: { type: String, default: void 0 }
1045
+ },
980
1046
  setup(props) {
981
1047
  const { manifest, isLoading, error } = useManifest2(props.client);
982
1048
  return () => {
1049
+ const t2 = createAdminT5(props.locale);
983
1050
  const m = manifest.value;
984
1051
  return page(
985
- "Modules",
1052
+ t2("modules.title"),
986
1053
  { isLoading, error },
987
1054
  () => {
988
1055
  if (!m) return null;
989
1056
  return table(
990
- ["Module", "Version", "Readiness", "Admin"],
1057
+ [
1058
+ t2("modules.colModule"),
1059
+ t2("modules.colVersion"),
1060
+ t2("modules.colReadiness"),
1061
+ t2("modules.colAdmin")
1062
+ ],
991
1063
  m.modules.map(
992
1064
  (mod) => h7("tr", { key: mod.name }, [
993
1065
  td(mod.name, { ...styles.mono, fontWeight: 600 }),
994
- td(mod.version ?? h7("span", { style: styles.muted }, "not reported"), styles.mono),
1066
+ td(
1067
+ mod.version ?? h7("span", { style: styles.muted }, t2("modules.notReported")),
1068
+ styles.mono
1069
+ ),
995
1070
  td([
996
- pill(mod.readiness.ok ? "ok" : "bad", mod.readiness.ok ? "ready" : "error"),
1071
+ pill(
1072
+ mod.readiness.ok ? "ok" : "bad",
1073
+ mod.readiness.ok ? t2("common.status.ready") : t2("common.status.error")
1074
+ ),
997
1075
  ...mod.readiness.problems.map(
998
1076
  (p) => h7(
999
1077
  "div",
@@ -1010,7 +1088,7 @@ var ModulesScreen = defineComponent5({
1010
1088
  )
1011
1089
  ]),
1012
1090
  td(
1013
- mod.describesAdmin ? h7("span", { style: styles.badge }, "describes admin") : h7("span", { style: styles.muted }, "\u2014")
1091
+ mod.describesAdmin ? h7("span", { style: styles.badge }, t2("modules.describesAdmin")) : h7("span", { style: styles.muted }, "\u2014")
1014
1092
  )
1015
1093
  ])
1016
1094
  )
@@ -1018,7 +1096,13 @@ var ModulesScreen = defineComponent5({
1018
1096
  },
1019
1097
  [],
1020
1098
  {
1021
- lead: m ? `${m.env} \xB7 admin ${m.admin.version} \xB7 admin log ${m.admin.log ? "on" : "off"} \xB7 ${m.routes.length} routes` : "Every installed module, its version and whether it is ready."
1099
+ loadingText: t2("common.loading"),
1100
+ lead: m ? t2("modules.leadSummary", {
1101
+ env: m.env,
1102
+ version: m.admin.version,
1103
+ log: m.admin.log ? t2("modules.logOn") : t2("modules.logOff"),
1104
+ routes: m.routes.length
1105
+ }) : t2("modules.lead")
1022
1106
  }
1023
1107
  );
1024
1108
  };
@@ -1026,24 +1110,34 @@ var ModulesScreen = defineComponent5({
1026
1110
  });
1027
1111
 
1028
1112
  // src/screens/RoutesScreen.ts
1113
+ import { createAdminT as createAdminT6 } from "@fonderie/client";
1029
1114
  import { useAdminRoutes } from "@fonderie/vue-admin";
1030
1115
  import { defineComponent as defineComponent6, h as h8, ref } from "vue";
1031
1116
  var RoutesScreen = defineComponent6({
1032
1117
  name: "FonderieRoutesScreen",
1033
- props: { client: { type: Object, required: true } },
1118
+ props: {
1119
+ client: { type: Object, required: true },
1120
+ locale: { type: String, default: void 0 }
1121
+ },
1034
1122
  setup(props) {
1035
1123
  const { report, isLoading, error } = useAdminRoutes(props.client);
1036
1124
  const q = ref("");
1037
1125
  return () => {
1126
+ const t2 = createAdminT6(props.locale);
1038
1127
  const needle = q.value.trim().toLowerCase();
1039
1128
  const routes = (report.value?.routes ?? []).filter(
1040
1129
  (r) => !needle || `${r.method} ${r.path} ${r.module ?? "application"} ${r.guard}`.toLowerCase().includes(needle)
1041
1130
  );
1042
1131
  return page(
1043
- "Routes",
1132
+ t2("routes.title"),
1044
1133
  { isLoading, error },
1045
1134
  () => table(
1046
- [{ label: "Method", style: { width: "80px" } }, "Path", "Guard", "Module"],
1135
+ [
1136
+ { label: t2("routes.colMethod"), style: { width: "80px" } },
1137
+ t2("routes.colPath"),
1138
+ t2("routes.colGuard"),
1139
+ t2("routes.colModule")
1140
+ ],
1047
1141
  [
1048
1142
  ...routes.map(
1049
1143
  (r) => h8("tr", { key: `${r.method} ${r.path}` }, [
@@ -1056,7 +1150,7 @@ var RoutesScreen = defineComponent6({
1056
1150
  false
1057
1151
  )
1058
1152
  ),
1059
- td(r.module ?? "application", styles.muted)
1153
+ td(r.module ?? t2("routes.application"), styles.muted)
1060
1154
  ])
1061
1155
  ),
1062
1156
  ...routes.length === 0 ? [
@@ -1064,7 +1158,7 @@ var RoutesScreen = defineComponent6({
1064
1158
  h8(
1065
1159
  "td",
1066
1160
  { style: { ...styles.td, ...styles.muted }, colspan: 4 },
1067
- `No route matches \u201C${q.value}\u201D.`
1161
+ t2("routes.noMatch", { q: q.value })
1068
1162
  )
1069
1163
  ])
1070
1164
  ] : []
@@ -1074,16 +1168,17 @@ var RoutesScreen = defineComponent6({
1074
1168
  h8("input", {
1075
1169
  type: "search",
1076
1170
  value: q.value,
1077
- placeholder: "Filter by path, method, module\u2026",
1171
+ placeholder: t2("routes.filterPlaceholder"),
1078
1172
  style: { ...styles.input, width: "280px" },
1079
- "aria-label": "Filter routes",
1173
+ "aria-label": t2("routes.filterLabel"),
1080
1174
  onInput: (e) => {
1081
1175
  q.value = e.target.value;
1082
1176
  }
1083
1177
  })
1084
1178
  ],
1085
1179
  {
1086
- lead: report.value ? `${report.value.routes.length} routes exposed by this deployment, with the guard in front of each.` : "Every exposed route, with its guard."
1180
+ loadingText: t2("common.loading"),
1181
+ lead: report.value ? t2("routes.leadCount", { n: report.value.routes.length }) : t2("routes.lead")
1087
1182
  }
1088
1183
  );
1089
1184
  };
@@ -1091,27 +1186,33 @@ var RoutesScreen = defineComponent6({
1091
1186
  });
1092
1187
 
1093
1188
  // src/screens/MigrationsScreen.ts
1189
+ import {
1190
+ createAdminT as createAdminT7
1191
+ } from "@fonderie/client";
1094
1192
  import { useAdminMigrations } from "@fonderie/vue-admin";
1095
1193
  import { defineComponent as defineComponent7, h as h9 } from "vue";
1096
- function why(m, everApplied) {
1194
+ function why(t2, m, everApplied) {
1097
1195
  if (m.pending.length === 0) return null;
1098
- if (m.blockedBy) return `Apply "${m.blockedBy}" first \u2014 it runs before this one and is behind.`;
1196
+ if (m.blockedBy) return t2("migrations.blockedBy", { module: m.blockedBy });
1099
1197
  if (everApplied && m.pending.some((p) => p.impact === "destructive"))
1100
- return "Contains a migration that deletes data. No down-migration brings it back \u2014 apply this one through CI or `npm run migrate`.";
1198
+ return t2("migrations.destructiveBlocked");
1101
1199
  return null;
1102
1200
  }
1103
1201
  var MigrationsScreen = defineComponent7({
1104
1202
  name: "FonderieMigrationsScreen",
1105
- props: { client: { type: Object, required: true } },
1203
+ props: {
1204
+ client: { type: Object, required: true },
1205
+ locale: { type: String, default: void 0 }
1206
+ },
1106
1207
  setup(props) {
1107
1208
  const { report, isLoading, error, refresh, apply } = useAdminMigrations(props.client);
1108
- const section = (m, everApplied) => {
1109
- const blocked = why(m, everApplied);
1209
+ const section = (t2, m, everApplied) => {
1210
+ const blocked = why(t2, m, everApplied);
1110
1211
  const files = m.pending.map((p) => p.file);
1111
1212
  return h9("section", { style: { ...styles.card, marginBottom: "16px" } }, [
1112
1213
  h9("h2", { style: { ...styles.subtitle, marginTop: 0 } }, [
1113
1214
  h9("span", { style: styles.mono }, m.name),
1114
- pill("warn", `${m.pending.length} pending`)
1215
+ pill("warn", t2("migrations.pendingCount", { n: m.pending.length }))
1115
1216
  ]),
1116
1217
  h9(
1117
1218
  "ul",
@@ -1120,7 +1221,7 @@ var MigrationsScreen = defineComponent7({
1120
1221
  (p) => h9("li", [
1121
1222
  h9("code", { style: styles.code }, p.file),
1122
1223
  " ",
1123
- p.impact === "destructive" ? pill("bad", "destructive") : pill("neutral", "additive"),
1224
+ p.impact === "destructive" ? pill("bad", t2("migrations.destructive")) : pill("neutral", t2("migrations.additive")),
1124
1225
  p.destructive.length > 0 ? h9(
1125
1226
  "ul",
1126
1227
  p.destructive.map((s) => h9("li", [h9("code", s.slice(0, 120))]))
@@ -1136,57 +1237,64 @@ var MigrationsScreen = defineComponent7({
1136
1237
  disabled: isLoading.value || !m.appliable,
1137
1238
  onClick: () => {
1138
1239
  if (window.confirm(
1139
- `Apply ${m.pending.length} migration(s) to "${m.name}"? This changes the database schema.`
1240
+ t2("migrations.confirmApply", { n: m.pending.length, module: m.name })
1140
1241
  ))
1141
1242
  void apply(m.name, files).catch(() => {
1142
1243
  });
1143
1244
  }
1144
1245
  },
1145
- `Apply ${m.pending.length} migration${m.pending.length === 1 ? "" : "s"}`
1246
+ m.pending.length === 1 ? t2("migrations.applyOne") : t2("migrations.applyMany", { n: m.pending.length })
1146
1247
  )
1147
1248
  ]);
1148
1249
  };
1149
- return () => page(
1150
- "Migrations",
1151
- { isLoading, error },
1152
- () => {
1153
- const r = report.value;
1154
- if (!r) return null;
1155
- const behind = r.modules.filter((m) => m.pending.length > 0);
1156
- if (behind.length === 0)
1157
- return empty("Every module is up to date", "No pending migrations.", "migrations");
1158
- return [
1159
- r.everApplied ? null : h9(
1160
- "p",
1161
- { style: styles.notice },
1162
- "This database has never been migrated \u2014 treating it as a first install, so nothing is held back."
1163
- ),
1164
- ...behind.map((m) => section(m, r.everApplied))
1165
- ].filter(Boolean);
1166
- },
1167
- [refreshButton2("Refresh", isLoading.value, () => void refresh())],
1168
- {
1169
- lead: "Schema changes each module ships, and whether this database has them.",
1170
- errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
1171
- }
1172
- );
1250
+ return () => {
1251
+ const t2 = createAdminT7(props.locale);
1252
+ return page(
1253
+ t2("migrations.title"),
1254
+ { isLoading, error },
1255
+ () => {
1256
+ const r = report.value;
1257
+ if (!r) return null;
1258
+ const behind = r.modules.filter((m) => m.pending.length > 0);
1259
+ if (behind.length === 0)
1260
+ return empty(t2("migrations.upToDate"), t2("migrations.noPending"), "migrations");
1261
+ return [
1262
+ r.everApplied ? null : h9("p", { style: styles.notice }, t2("migrations.firstInstall")),
1263
+ ...behind.map((m) => section(t2, m, r.everApplied))
1264
+ ].filter(Boolean);
1265
+ },
1266
+ [
1267
+ refreshButton2(
1268
+ t2("common.refresh"),
1269
+ isLoading.value,
1270
+ () => void refresh(),
1271
+ t2("common.working")
1272
+ )
1273
+ ],
1274
+ {
1275
+ loadingText: t2("common.loading"),
1276
+ lead: t2("migrations.lead"),
1277
+ errorText: (e) => e.status === 403 ? t2("migrations.needsWrite") : e.explanation
1278
+ }
1279
+ );
1280
+ };
1173
1281
  }
1174
1282
  });
1175
1283
 
1176
1284
  // src/screens/OperatorsScreen.ts
1285
+ import {
1286
+ createAdminT as createAdminT8,
1287
+ formatAdminDate as formatAdminDate3
1288
+ } from "@fonderie/client";
1177
1289
  import { useAdminOperators } from "@fonderie/vue-admin";
1178
1290
  import { defineComponent as defineComponent8, h as h10, ref as ref2 } from "vue";
1179
1291
  var LEVELS = [
1180
- { label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
1181
- { label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
1182
- {
1183
- label: "Owner",
1184
- scopes: ["read", "write", "secrets"],
1185
- hint: "secrets, tokens and operators too"
1186
- }
1292
+ { id: "read", scopes: ["read"] },
1293
+ { id: "editor", scopes: ["read", "write"] },
1294
+ { id: "owner", scopes: ["read", "write", "secrets"] }
1187
1295
  ];
1188
- var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
1189
- var scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
1296
+ var levelOf = (scopes) => scopes.includes("secrets") ? "owner" : scopes.includes("write") ? "editor" : "read";
1297
+ var scopesFor = (id) => LEVELS.find((l) => l.id === id)?.scopes ?? ["read"];
1190
1298
  var small = { height: "28px" };
1191
1299
  var OperatorsScreen = defineComponent8({
1192
1300
  name: "FonderieOperatorsScreen",
@@ -1194,18 +1302,19 @@ var OperatorsScreen = defineComponent8({
1194
1302
  client: { type: Object, required: true },
1195
1303
  // The signed-in operator's email: their own row offers no disable or
1196
1304
  // recovery (the server refuses both for yourself anyway).
1197
- me: { type: String, default: void 0 }
1305
+ me: { type: String, default: void 0 },
1306
+ locale: { type: String, default: void 0 }
1198
1307
  },
1199
1308
  setup(props) {
1200
1309
  const { report, isLoading, error, invite, recover, update, revokeLink } = useAdminOperators(
1201
1310
  props.client
1202
1311
  );
1203
1312
  const email = ref2("");
1204
- const level = ref2("Editor");
1313
+ const level = ref2("editor");
1205
1314
  const minted = ref2(null);
1206
1315
  const copied = ref2(false);
1207
- const status = (o) => o.disabledAt ? pill("neutral", "disabled") : o.locked ? pill("warn", "locked") : !o.enrolled ? pill("warn", "setting up") : pill("ok", "active");
1208
- const mintedNotice = (link) => {
1316
+ const status = (t2, o) => o.disabledAt ? pill("neutral", t2("common.status.disabled")) : o.locked ? pill("warn", t2("common.status.locked")) : !o.enrolled ? pill("warn", t2("operators.settingUp")) : pill("ok", t2("common.status.active"));
1317
+ const mintedNotice = (t2, link) => {
1209
1318
  const url = `${window.location.origin}${link.url}`;
1210
1319
  return h10(
1211
1320
  "div",
@@ -1217,8 +1326,8 @@ var OperatorsScreen = defineComponent8({
1217
1326
  }
1218
1327
  },
1219
1328
  [
1220
- h10("strong", `Send this link to ${link.email}. It is shown once.`),
1221
- ` It works once and expires ${new Date(link.expiresAt).toLocaleString()}.`,
1329
+ h10("strong", t2("operators.mintedTitle", { email: link.email })),
1330
+ ` ${t2("operators.mintedBody", { date: formatAdminDate3(link.expiresAt, props.locale) })}`,
1222
1331
  h10(
1223
1332
  "div",
1224
1333
  { style: { display: "flex", gap: "8px", marginTop: "8px", alignItems: "center" } },
@@ -1238,14 +1347,14 @@ var OperatorsScreen = defineComponent8({
1238
1347
  copied.value = true;
1239
1348
  }
1240
1349
  },
1241
- copied.value ? "Copied" : "Copy"
1350
+ copied.value ? t2("common.copied") : t2("common.copy")
1242
1351
  ),
1243
1352
  h10(
1244
1353
  "button",
1245
1354
  {
1246
1355
  type: "button",
1247
1356
  style: styles.buttonGhost,
1248
- "aria-label": "Dismiss",
1357
+ "aria-label": t2("common.dismiss"),
1249
1358
  onClick: () => {
1250
1359
  minted.value = null;
1251
1360
  copied.value = false;
@@ -1263,19 +1372,17 @@ var OperatorsScreen = defineComponent8({
1263
1372
  minted.value = l;
1264
1373
  };
1265
1374
  return () => {
1375
+ const t2 = createAdminT8(props.locale);
1266
1376
  const r = report.value;
1267
1377
  const e = error.value;
1268
1378
  return h10("div", { style: styles.container }, [
1269
- pageHeader(
1270
- "Operators",
1271
- "The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
1272
- ),
1379
+ pageHeader(t2("operators.title"), t2("operators.lead")),
1273
1380
  e ? h10(
1274
1381
  "p",
1275
1382
  { style: styles.error, role: "alert" },
1276
- e.status === 403 && e.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : e.explanation
1383
+ e.status === 403 && e.reason === "FORBIDDEN" ? t2("operators.needsOwner") : e.explanation
1277
1384
  ) : null,
1278
- minted.value ? mintedNotice(minted.value) : null,
1385
+ minted.value ? mintedNotice(t2, minted.value) : null,
1279
1386
  h10(
1280
1387
  "form",
1281
1388
  {
@@ -1298,15 +1405,19 @@ var OperatorsScreen = defineComponent8({
1298
1405
  },
1299
1406
  [
1300
1407
  icon("users", 16),
1301
- h10("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Invite"),
1408
+ h10(
1409
+ "strong",
1410
+ { style: { fontSize: "13.5px", marginRight: "4px" } },
1411
+ t2("operators.invite")
1412
+ ),
1302
1413
  h10("input", {
1303
1414
  type: "email",
1304
1415
  required: true,
1305
1416
  value: email.value,
1306
1417
  onInput: (ev) => email.value = ev.target.value,
1307
- placeholder: "teammate@company.com",
1418
+ placeholder: t2("operators.invitePlaceholder"),
1308
1419
  style: { ...styles.input, flex: 1, minWidth: "220px" },
1309
- "aria-label": "Email to invite"
1420
+ "aria-label": t2("operators.inviteLabel")
1310
1421
  }),
1311
1422
  h10(
1312
1423
  "select",
@@ -1314,21 +1425,31 @@ var OperatorsScreen = defineComponent8({
1314
1425
  value: level.value,
1315
1426
  onChange: (ev) => level.value = ev.target.value,
1316
1427
  style: styles.input,
1317
- "aria-label": "Access level"
1428
+ "aria-label": t2("operators.accessLevelLabel")
1318
1429
  },
1319
1430
  LEVELS.map(
1320
- (l) => h10("option", { key: l.label, value: l.label }, `${l.label} \u2014 ${l.hint}`)
1431
+ (l) => h10(
1432
+ "option",
1433
+ { key: l.id, value: l.id },
1434
+ `${t2(`operators.level.${l.id}`)} \u2014 ${t2(`operators.levelHint.${l.id}`)}`
1435
+ )
1321
1436
  )
1322
1437
  ),
1323
1438
  h10(
1324
1439
  "button",
1325
1440
  { type: "submit", style: styles.buttonPrimary, disabled: !email.value.trim() },
1326
- [icon("plus", 14), "Create invite link"]
1441
+ [icon("plus", 14), t2("operators.createInvite")]
1327
1442
  )
1328
1443
  ]
1329
1444
  ),
1330
- isLoading.value && !r ? h10("p", { style: styles.status }, "Loading\u2026") : r && r.operators.length === 0 ? empty("No operators yet", void 0, "users") : r ? table(
1331
- ["Operator", "Access", "Status", "Last sign-in", ""],
1445
+ isLoading.value && !r ? h10("p", { style: styles.status }, t2("common.loading")) : r && r.operators.length === 0 ? empty(t2("operators.emptyTitle"), void 0, "users") : r ? table(
1446
+ [
1447
+ t2("operators.col.operator"),
1448
+ t2("operators.col.access"),
1449
+ t2("operators.col.status"),
1450
+ t2("operators.col.lastSignIn"),
1451
+ ""
1452
+ ],
1332
1453
  r.operators.map((o) => {
1333
1454
  const mine = o.email === props.me;
1334
1455
  return h10("tr", { key: o.id }, [
@@ -1347,25 +1468,33 @@ var OperatorsScreen = defineComponent8({
1347
1468
  }).catch(() => {
1348
1469
  }),
1349
1470
  style: { ...styles.input, ...small, fontSize: "12.5px" },
1350
- "aria-label": `Access level for ${o.email}`
1471
+ "aria-label": t2("operators.accessLevelFor", { email: o.email })
1351
1472
  },
1352
- LEVELS.map((l) => h10("option", { key: l.label, value: l.label }, l.label))
1473
+ LEVELS.map(
1474
+ (l) => h10("option", { key: l.id, value: l.id }, t2(`operators.level.${l.id}`))
1475
+ )
1353
1476
  )
1354
1477
  ),
1355
1478
  td([
1356
- status(o),
1479
+ status(t2, o),
1357
1480
  o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? h10(
1358
1481
  "div",
1359
1482
  { style: { ...styles.muted, marginTop: "4px" } },
1360
- `${o.backupCodesLeft} backup code(s) left`
1483
+ t2(
1484
+ o.backupCodesLeft === 1 ? "operators.backupCodesLeftOne" : "operators.backupCodesLeftMany",
1485
+ { n: o.backupCodesLeft }
1486
+ )
1361
1487
  ) : null
1362
1488
  ]),
1363
- td(o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never", {
1364
- ...styles.muted,
1365
- whiteSpace: "nowrap"
1366
- }),
1367
1489
  td(
1368
- mine ? h10("span", { style: styles.muted }, "you") : [
1490
+ o.lastLoginAt ? formatAdminDate3(o.lastLoginAt, props.locale) : t2("common.never"),
1491
+ {
1492
+ ...styles.muted,
1493
+ whiteSpace: "nowrap"
1494
+ }
1495
+ ),
1496
+ td(
1497
+ mine ? h10("span", { style: styles.muted }, t2("common.you")) : [
1369
1498
  h10(
1370
1499
  "button",
1371
1500
  {
@@ -1373,13 +1502,13 @@ var OperatorsScreen = defineComponent8({
1373
1502
  style: { ...styles.button, ...small, marginRight: "6px" },
1374
1503
  onClick: () => {
1375
1504
  if (window.confirm(
1376
- `Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
1505
+ t2("operators.recoveryConfirm", { email: o.email })
1377
1506
  ))
1378
1507
  void recover(o.id).then(setMinted).catch(() => {
1379
1508
  });
1380
1509
  }
1381
1510
  },
1382
- "Recovery link"
1511
+ t2("operators.recoveryLink")
1383
1512
  ),
1384
1513
  h10(
1385
1514
  "button",
@@ -1391,7 +1520,7 @@ var OperatorsScreen = defineComponent8({
1391
1520
  },
1392
1521
  onClick: () => {
1393
1522
  if (o.disabledAt || window.confirm(
1394
- `Disable ${o.email}? They are signed out immediately.`
1523
+ t2("operators.disableConfirm", { email: o.email })
1395
1524
  ))
1396
1525
  void update(o.id, { disabled: !o.disabledAt }).catch(
1397
1526
  () => {
@@ -1399,7 +1528,7 @@ var OperatorsScreen = defineComponent8({
1399
1528
  );
1400
1529
  }
1401
1530
  },
1402
- o.disabledAt ? "Enable" : "Disable"
1531
+ o.disabledAt ? t2("operators.enable") : t2("operators.disable")
1403
1532
  )
1404
1533
  ],
1405
1534
  { textAlign: "right", whiteSpace: "nowrap" }
@@ -1408,7 +1537,7 @@ var OperatorsScreen = defineComponent8({
1408
1537
  })
1409
1538
  ) : null,
1410
1539
  r && r.links.length > 0 ? [
1411
- h10("h2", { style: styles.subtitle }, "Pending links"),
1540
+ h10("h2", { style: styles.subtitle }, t2("operators.pendingLinks")),
1412
1541
  h10(
1413
1542
  "ul",
1414
1543
  { style: styles.list },
@@ -1420,15 +1549,25 @@ var OperatorsScreen = defineComponent8({
1420
1549
  style: { ...styles.row, display: "flex", gap: "12px", alignItems: "center" }
1421
1550
  },
1422
1551
  [
1423
- pill(l.kind === "invite" ? "info" : "warn", l.kind, false),
1552
+ pill(
1553
+ l.kind === "invite" ? "info" : "warn",
1554
+ l.kind === "invite" ? t2("operators.linkKind.invite") : t2("operators.linkKind.recovery"),
1555
+ false
1556
+ ),
1424
1557
  h10("span", { style: { flex: 1 } }, [
1425
1558
  l.email,
1426
- l.kind === "invite" ? h10("span", { style: styles.muted }, ` \xB7 ${levelOf(l.scopes)}`) : null
1559
+ l.kind === "invite" ? h10(
1560
+ "span",
1561
+ { style: styles.muted },
1562
+ ` \xB7 ${t2(`operators.level.${levelOf(l.scopes)}`)}`
1563
+ ) : null
1427
1564
  ]),
1428
1565
  h10(
1429
1566
  "span",
1430
1567
  { style: styles.muted },
1431
- `expires ${new Date(l.expiresAt).toLocaleString()}`
1568
+ t2("operators.expiresOn", {
1569
+ date: formatAdminDate3(l.expiresAt, props.locale)
1570
+ })
1432
1571
  ),
1433
1572
  h10(
1434
1573
  "button",
@@ -1438,7 +1577,7 @@ var OperatorsScreen = defineComponent8({
1438
1577
  onClick: () => void revokeLink(l.id).catch(() => {
1439
1578
  })
1440
1579
  },
1441
- "Revoke"
1580
+ t2("operators.revoke")
1442
1581
  )
1443
1582
  ]
1444
1583
  )
@@ -1451,12 +1590,19 @@ var OperatorsScreen = defineComponent8({
1451
1590
  });
1452
1591
 
1453
1592
  // src/screens/TokensScreen.ts
1593
+ import {
1594
+ createAdminT as createAdminT9,
1595
+ formatAdminDate as formatAdminDate4
1596
+ } from "@fonderie/client";
1454
1597
  import { useAdminTokens } from "@fonderie/vue-admin";
1455
1598
  import { defineComponent as defineComponent9, h as h11, ref as ref3 } from "vue";
1456
1599
  var ALL = ["read", "write", "secrets"];
1457
1600
  var TokensScreen = defineComponent9({
1458
1601
  name: "FonderieTokensScreen",
1459
- props: { client: { type: Object, required: true } },
1602
+ props: {
1603
+ client: { type: Object, required: true },
1604
+ locale: { type: String, default: void 0 }
1605
+ },
1460
1606
  setup(props) {
1461
1607
  const { report, isLoading, error, issue, revoke } = useAdminTokens(props.client);
1462
1608
  const name = ref3("");
@@ -1465,25 +1611,27 @@ var TokensScreen = defineComponent9({
1465
1611
  const minted = ref3(null);
1466
1612
  const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
1467
1613
  return () => {
1614
+ const t2 = createAdminT9(props.locale);
1615
+ const loc = props.locale;
1468
1616
  const r = report.value;
1469
1617
  return h11("div", { style: styles.container }, [
1470
- pageHeader(
1471
- "Tokens",
1472
- "Credentials for machines: the root token's strength, scoped tokens for the CLI and CI, and any legacy per-module tokens. People sign in as operators."
1473
- ),
1618
+ pageHeader(t2("tokens.title"), t2("tokens.lead")),
1474
1619
  error.value ? h11(
1475
1620
  "p",
1476
1621
  { style: styles.error, role: "alert" },
1477
- error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
1622
+ error.value.status === 401 ? t2("tokens.needsRoot") : error.value.explanation
1478
1623
  ) : null,
1479
- h11("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
1624
+ h11("h2", { style: { ...styles.subtitle, marginTop: 0 } }, t2("tokens.rootToken")),
1480
1625
  r ? h11(
1481
1626
  "div",
1482
1627
  { style: { ...styles.card, display: "flex", gap: "12px", alignItems: "flex-start" } },
1483
1628
  [
1484
1629
  icon("lock", 18, { marginTop: "2px" }),
1485
1630
  h11("div", [
1486
- pill(r.admin.ok ? "ok" : "bad", r.admin.ok ? "strong" : "weak"),
1631
+ pill(
1632
+ r.admin.ok ? "ok" : "bad",
1633
+ r.admin.ok ? t2("tokens.strong") : t2("tokens.weak")
1634
+ ),
1487
1635
  ...r.admin.problems.map(
1488
1636
  (p) => h11(
1489
1637
  "div",
@@ -1501,12 +1649,8 @@ var TokensScreen = defineComponent9({
1501
1649
  ])
1502
1650
  ]
1503
1651
  ) : null,
1504
- h11("h2", { style: styles.subtitle }, "Issued tokens"),
1505
- r?.issued === null ? h11(
1506
- "p",
1507
- { style: styles.muted },
1508
- "Issuing is off \u2014 give AdminModule a store and run its migrations."
1509
- ) : [
1652
+ h11("h2", { style: styles.subtitle }, t2("tokens.issuedTokens")),
1653
+ r?.issued === null ? h11("p", { style: styles.muted }, t2("tokens.issuingOff")) : [
1510
1654
  minted.value ? h11(
1511
1655
  "div",
1512
1656
  {
@@ -1517,7 +1661,7 @@ var TokensScreen = defineComponent9({
1517
1661
  }
1518
1662
  },
1519
1663
  [
1520
- h11("strong", "Copy this now \u2014 it is never shown again."),
1664
+ h11("strong", t2("tokens.copyNow")),
1521
1665
  ` (${minted.value.name})`,
1522
1666
  h11(
1523
1667
  "div",
@@ -1545,8 +1689,8 @@ var TokensScreen = defineComponent9({
1545
1689
  name: name.value.trim(),
1546
1690
  scopes: scopes.value,
1547
1691
  ...days.value && Number.isInteger(d) && d > 0 ? { expiresInDays: d } : {}
1548
- }).then((t2) => {
1549
- minted.value = { name: t2.name, token: t2.token };
1692
+ }).then((tk) => {
1693
+ minted.value = { name: tk.name, token: tk.token };
1550
1694
  name.value = "";
1551
1695
  days.value = "";
1552
1696
  }).catch(() => {
@@ -1557,9 +1701,9 @@ var TokensScreen = defineComponent9({
1557
1701
  h11("input", {
1558
1702
  value: name.value,
1559
1703
  onInput: (e) => name.value = e.target.value,
1560
- placeholder: "name",
1704
+ placeholder: t2("tokens.namePlaceholder"),
1561
1705
  style: styles.input,
1562
- "aria-label": "Name"
1706
+ "aria-label": t2("tokens.nameLabel")
1563
1707
  }),
1564
1708
  ...ALL.map(
1565
1709
  (s) => h11(
@@ -1581,16 +1725,16 @@ var TokensScreen = defineComponent9({
1581
1725
  checked: scopes.value.includes(s),
1582
1726
  onChange: () => toggle(s)
1583
1727
  }),
1584
- ` ${s}`
1728
+ ` ${t2(`tokens.scope.${s}`)}`
1585
1729
  ]
1586
1730
  )
1587
1731
  ),
1588
1732
  h11("input", {
1589
1733
  value: days.value,
1590
1734
  onInput: (e) => days.value = e.target.value,
1591
- placeholder: "days (optional)",
1735
+ placeholder: t2("tokens.daysPlaceholder"),
1592
1736
  style: { ...styles.input, width: "140px" },
1593
- "aria-label": "Days"
1737
+ "aria-label": t2("tokens.daysLabel")
1594
1738
  }),
1595
1739
  h11(
1596
1740
  "button",
@@ -1599,70 +1743,66 @@ var TokensScreen = defineComponent9({
1599
1743
  style: styles.buttonPrimary,
1600
1744
  disabled: !name.value.trim() || scopes.value.length === 0 || isLoading.value
1601
1745
  },
1602
- [icon("plus", 14), "Issue token"]
1746
+ [icon("plus", 14), t2("tokens.issue")]
1603
1747
  )
1604
1748
  ]
1605
1749
  ),
1606
1750
  r?.issued && r.issued.length > 0 ? table(
1607
- ["Name", "Scopes", "Created", "Expires", "Last used", ""],
1751
+ [
1752
+ t2("tokens.col.name"),
1753
+ t2("tokens.col.scopes"),
1754
+ t2("tokens.col.created"),
1755
+ t2("tokens.col.expires"),
1756
+ t2("tokens.col.lastUsed"),
1757
+ ""
1758
+ ],
1608
1759
  r.issued.map(
1609
- (t2) => h11("tr", { key: t2.id }, [
1610
- td([t2.name, t2.revokedAt ? [" ", pill("neutral", "revoked")] : ""]),
1611
- td(t2.scopes.join(", "), styles.mono),
1760
+ (tk) => h11("tr", { key: tk.id }, [
1761
+ td([
1762
+ tk.name,
1763
+ tk.revokedAt ? [" ", pill("neutral", t2("tokens.revoked"))] : ""
1764
+ ]),
1765
+ td(tk.scopes.map((sc) => t2(`tokens.scope.${sc}`)).join(", "), styles.mono),
1612
1766
  td(
1613
- `${new Date(t2.createdAt).toLocaleDateString()} \xB7 ${t2.createdBy}`,
1767
+ `${formatAdminDate4(tk.createdAt, loc, "date")} \xB7 ${tk.createdBy}`,
1614
1768
  styles.muted
1615
1769
  ),
1616
1770
  td(
1617
- t2.expiresAt ? new Date(t2.expiresAt).toLocaleDateString() : "never",
1771
+ tk.expiresAt ? formatAdminDate4(tk.expiresAt, loc, "date") : t2("common.never"),
1618
1772
  styles.muted
1619
1773
  ),
1620
1774
  td(
1621
- t2.lastUsedAt ? new Date(t2.lastUsedAt).toLocaleString() : "never",
1775
+ tk.lastUsedAt ? formatAdminDate4(tk.lastUsedAt, loc) : t2("common.never"),
1622
1776
  styles.muted
1623
1777
  ),
1624
1778
  td(
1625
- t2.revokedAt ? "" : h11(
1779
+ tk.revokedAt ? "" : h11(
1626
1780
  "button",
1627
1781
  {
1628
1782
  type: "button",
1629
1783
  style: styles.buttonDanger,
1630
1784
  onClick: () => {
1631
- if (window.confirm(
1632
- `Revoke "${t2.name}"? Anything using it stops working immediately.`
1633
- ))
1634
- void revoke(t2.id).catch(() => {
1785
+ if (window.confirm(t2("tokens.revokeConfirm", { name: tk.name })))
1786
+ void revoke(tk.id).catch(() => {
1635
1787
  });
1636
1788
  }
1637
1789
  },
1638
- "Revoke"
1790
+ t2("tokens.revoke")
1639
1791
  )
1640
1792
  )
1641
1793
  ])
1642
1794
  )
1643
- ) : empty(
1644
- "No scoped tokens yet",
1645
- "Issue a read-only token for a dashboard or a teammate instead of sharing the root.",
1646
- "tokens"
1647
- )
1795
+ ) : empty(t2("tokens.emptyTitle"), t2("tokens.emptyBody"), "tokens")
1648
1796
  ],
1649
- h11("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
1650
- r && r.legacy.length === 0 ? h11(
1651
- "p",
1652
- { style: styles.muted },
1653
- "None \u2014 every brick's admin surface goes through this token."
1654
- ) : h11(
1797
+ h11("h2", { style: styles.subtitle }, t2("tokens.legacyTitle")),
1798
+ r && r.legacy.length === 0 ? h11("p", { style: styles.muted }, t2("tokens.legacyNone")) : h11(
1655
1799
  "ul",
1656
1800
  { style: styles.list },
1657
1801
  (r?.legacy ?? []).map(
1658
1802
  (l) => h11("li", { key: l.module, style: styles.row }, [
1659
1803
  h11("span", { style: styles.mono }, l.module),
1660
1804
  " ",
1661
- h11(
1662
- "span",
1663
- { style: styles.muted },
1664
- "still registers its standalone routes with its own token (deprecated)"
1665
- )
1805
+ h11("span", { style: styles.muted }, t2("tokens.legacyRow"))
1666
1806
  ])
1667
1807
  )
1668
1808
  )
@@ -1672,7 +1812,11 @@ var TokensScreen = defineComponent9({
1672
1812
  });
1673
1813
 
1674
1814
  // src/screens/UsersScreen.ts
1675
- import { describeLocation } from "@fonderie/client";
1815
+ import {
1816
+ createAdminT as createAdminT12,
1817
+ describeLocation,
1818
+ formatAdminDate as formatAdminDate7
1819
+ } from "@fonderie/client";
1676
1820
  import {
1677
1821
  useAdminLoginHistory,
1678
1822
  useAdminUser,
@@ -1682,14 +1826,38 @@ import {
1682
1826
  import { computed as computed2, defineComponent as defineComponent11, h as h13, ref as ref6 } from "vue";
1683
1827
 
1684
1828
  // src/billing.ts
1829
+ import {
1830
+ createAdminT as createAdminT10,
1831
+ formatAdminDate as formatAdminDate5
1832
+ } from "@fonderie/client";
1685
1833
  import { ref as ref4 } from "vue";
1686
- function periodEnd(s) {
1834
+ function periodEnd(s, locale) {
1687
1835
  if (!s.currentPeriodEnd) return null;
1688
- const date = new Date(s.currentPeriodEnd).toLocaleDateString();
1836
+ const t2 = createAdminT10(locale);
1837
+ const date = formatAdminDate5(s.currentPeriodEnd, locale, "date");
1689
1838
  if (s.status === "canceled")
1690
- return { label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? "Ended" : "Ends", date };
1691
- if (s.cancelAtPeriodEnd) return { label: "Ends", date };
1692
- return { label: "Renews", date };
1839
+ return {
1840
+ label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? t2("billing.ended") : t2("billing.ends"),
1841
+ date
1842
+ };
1843
+ if (s.cancelAtPeriodEnd) return { label: t2("billing.ends"), date };
1844
+ return { label: t2("billing.renews"), date };
1845
+ }
1846
+ var STATUS_KEYS = [
1847
+ "active",
1848
+ "trialing",
1849
+ "past_due",
1850
+ "unpaid",
1851
+ "canceled",
1852
+ "incomplete",
1853
+ "incomplete_expired",
1854
+ "paused"
1855
+ ];
1856
+ function statusLabel(t2, status) {
1857
+ return STATUS_KEYS.includes(status) ? t2(`billing.statusLabel.${status}`) : status;
1858
+ }
1859
+ function intervalLabel(t2, interval) {
1860
+ return interval === "month" || interval === "year" ? t2(`billing.intervalLabel.${interval}`) : interval;
1693
1861
  }
1694
1862
  var statusTone = (status) => status === "active" ? "ok" : status === "trialing" ? "info" : status === "past_due" || status === "unpaid" ? "warn" : "neutral";
1695
1863
  function useSubscriptionIndex(client) {
@@ -1718,31 +1886,46 @@ function useSubscriptionIndex(client) {
1718
1886
  }
1719
1887
 
1720
1888
  // src/screens/SubscriberBilling.ts
1889
+ import {
1890
+ createAdminT as createAdminT11,
1891
+ formatAdminDate as formatAdminDate6
1892
+ } from "@fonderie/client";
1721
1893
  import { useAdminSubscriber } from "@fonderie/vue-admin";
1722
1894
  import { computed, defineComponent as defineComponent10, h as h12, ref as ref5 } from "vue";
1723
- var SubscriberBilling = defineComponent10({
1724
- name: "FonderieSubscriberBilling",
1895
+ var LEDGER_KINDS = ["purchase", "grant", "usage", "refund", "adjustment", "expiry"];
1896
+ var SubscriberBillingInner = defineComponent10({
1897
+ name: "FonderieSubscriberBillingInner",
1725
1898
  props: {
1726
1899
  client: { type: Object, required: true },
1727
1900
  subscriberType: { type: String, required: true },
1728
- subscriberId: { type: String, required: true }
1901
+ subscriberId: { type: String, required: true },
1902
+ currency: { type: String, default: "" },
1903
+ locale: { type: String, default: void 0 }
1729
1904
  },
1730
- setup(props) {
1905
+ emits: { "change-currency": (_currency) => true },
1906
+ setup(props, { emit }) {
1731
1907
  const subscriber = computed(() => ({ type: props.subscriberType, id: props.subscriberId }));
1732
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
1908
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, {
1909
+ limit: 20,
1910
+ ...props.currency ? { currency: props.currency } : {}
1911
+ });
1912
+ const currencyDraft = ref5(props.currency);
1733
1913
  const amount = ref5("");
1734
1914
  const note = ref5("");
1735
1915
  const granted = ref5(null);
1736
1916
  return () => {
1917
+ const t2 = createAdminT11(props.locale);
1918
+ const ledgerKind = (kind) => LEDGER_KINDS.includes(kind) ? t2(`billing.ledgerKind.${kind}`) : kind;
1737
1919
  const s = subscription.value;
1738
1920
  const w = wallet.value;
1739
- if (isLoading.value && !w && !s) return h12("p", { style: styles.status }, "Loading billing\u2026");
1740
- const end = s ? periodEnd(s) : null;
1921
+ if (isLoading.value && !w && !s)
1922
+ return h12("p", { style: styles.status }, t2("billing.loading"));
1923
+ const end = s ? periodEnd(s, props.locale) : null;
1741
1924
  return h12("div", [
1742
1925
  error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1743
1926
  h12("div", { style: { ...styles.grid, marginBottom: "16px" } }, [
1744
1927
  h12("div", { style: styles.card }, [
1745
- h12("div", { style: styles.statLabel }, "Plan"),
1928
+ h12("div", { style: styles.statLabel }, t2("billing.plan")),
1746
1929
  h12(
1747
1930
  "div",
1748
1931
  {
@@ -1755,11 +1938,11 @@ var SubscriberBilling = defineComponent10({
1755
1938
  }
1756
1939
  },
1757
1940
  [
1758
- h12("strong", { style: { fontSize: "18px" } }, s ? s.plan : "Free"),
1941
+ h12("strong", { style: { fontSize: "18px" } }, s ? s.plan : t2("billing.free")),
1759
1942
  ...s ? [
1760
- h12("span", { style: styles.muted }, `\xB7 ${s.interval}`),
1761
- pill(statusTone(s.status), s.status)
1762
- ] : [h12("span", { style: styles.muted }, "no subscription")]
1943
+ h12("span", { style: styles.muted }, `\xB7 ${intervalLabel(t2, s.interval)}`),
1944
+ pill(statusTone(s.status), statusLabel(t2, s.status))
1945
+ ] : [h12("span", { style: styles.muted }, t2("billing.noSubscription"))]
1763
1946
  ]
1764
1947
  ),
1765
1948
  end ? h12("div", { style: styles.statHint }, `${end.label} ${end.date}`) : null,
@@ -1770,7 +1953,43 @@ var SubscriberBilling = defineComponent10({
1770
1953
  ) : null
1771
1954
  ]),
1772
1955
  h12("div", { style: styles.card }, [
1773
- h12("div", { style: styles.statLabel }, "Credits"),
1956
+ h12(
1957
+ "div",
1958
+ {
1959
+ style: {
1960
+ display: "flex",
1961
+ justifyContent: "space-between",
1962
+ alignItems: "center",
1963
+ gap: "8px"
1964
+ }
1965
+ },
1966
+ [
1967
+ h12("div", { style: styles.statLabel }, t2("billing.credits")),
1968
+ h12(
1969
+ "form",
1970
+ {
1971
+ style: { display: "flex", gap: "4px" },
1972
+ onSubmit: (e) => {
1973
+ e.preventDefault();
1974
+ emit("change-currency", currencyDraft.value.trim().toUpperCase());
1975
+ }
1976
+ },
1977
+ [
1978
+ h12("input", {
1979
+ value: currencyDraft.value,
1980
+ onInput: (e) => {
1981
+ currencyDraft.value = e.target.value;
1982
+ },
1983
+ placeholder: w?.currency ?? t2("billing.currencyPlaceholder"),
1984
+ maxlength: 8,
1985
+ style: { ...styles.input, height: "26px", width: "84px", fontSize: "12px" },
1986
+ "aria-label": t2("billing.currencyLabel"),
1987
+ title: t2("billing.currencyTitle")
1988
+ })
1989
+ ]
1990
+ )
1991
+ ]
1992
+ ),
1774
1993
  ...w ? [
1775
1994
  h12("div", { style: styles.statValue }, [
1776
1995
  `${w.balance} `,
@@ -1779,15 +1998,11 @@ var SubscriberBilling = defineComponent10({
1779
1998
  h12(
1780
1999
  "div",
1781
2000
  { style: styles.statHint },
1782
- `${w.purchased ?? "0"} permanent \xB7 ${w.granted ?? "0"} plan allowance${w.grantedExpiresAt ? ` (expires ${new Date(w.grantedExpiresAt).toLocaleDateString()})` : ""}`
2001
+ `${t2("billing.breakdown", { permanent: w.purchased ?? "0", allowance: w.granted ?? "0" })}${w.grantedExpiresAt ? t2("billing.expires", {
2002
+ date: formatAdminDate6(w.grantedExpiresAt, props.locale, "date")
2003
+ }) : ""}`
1783
2004
  )
1784
- ] : [
1785
- h12(
1786
- "div",
1787
- { style: styles.statHint },
1788
- "No wallet \u2014 billing has no wallet configuration."
1789
- )
1790
- ]
2005
+ ] : [h12("div", { style: styles.statHint }, t2("billing.noWallet"))]
1791
2006
  ])
1792
2007
  ]),
1793
2008
  w ? h12(
@@ -1810,7 +2025,7 @@ var SubscriberBilling = defineComponent10({
1810
2025
  ...note.value ? { description: note.value } : {},
1811
2026
  idempotencyKey: key
1812
2027
  }).then(() => {
1813
- granted.value = `Granted ${a} ${w.currency}.`;
2028
+ granted.value = t2("billing.granted", { amount: a, currency: w.currency });
1814
2029
  amount.value = "";
1815
2030
  note.value = "";
1816
2031
  }).catch(() => {
@@ -1818,21 +2033,25 @@ var SubscriberBilling = defineComponent10({
1818
2033
  }
1819
2034
  },
1820
2035
  [
1821
- h12("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Grant credits"),
2036
+ h12(
2037
+ "strong",
2038
+ { style: { fontSize: "13.5px", marginRight: "4px" } },
2039
+ t2("billing.grantCredits")
2040
+ ),
1822
2041
  h12("input", {
1823
2042
  value: amount.value,
1824
2043
  onInput: (e) => amount.value = e.target.value,
1825
- placeholder: `amount (${w.currency})`,
2044
+ placeholder: t2("billing.amountPlaceholder", { currency: w.currency }),
1826
2045
  inputmode: "numeric",
1827
2046
  style: { ...styles.input, width: "150px" },
1828
- "aria-label": "Amount"
2047
+ "aria-label": t2("billing.amountLabel")
1829
2048
  }),
1830
2049
  h12("input", {
1831
2050
  value: note.value,
1832
2051
  onInput: (e) => note.value = e.target.value,
1833
- placeholder: "reason (optional)",
2052
+ placeholder: t2("billing.reasonPlaceholder"),
1834
2053
  style: { ...styles.input, flex: 1, minWidth: "180px" },
1835
- "aria-label": "Reason"
2054
+ "aria-label": t2("billing.reasonLabel")
1836
2055
  }),
1837
2056
  h12(
1838
2057
  "button",
@@ -1841,32 +2060,60 @@ var SubscriberBilling = defineComponent10({
1841
2060
  style: styles.buttonPrimary,
1842
2061
  disabled: !/^\d+$/.test(amount.value.trim())
1843
2062
  },
1844
- [icon("plus", 14), "Grant"]
2063
+ [icon("plus", 14), t2("billing.grant")]
1845
2064
  ),
1846
2065
  granted.value ? h12("span", { style: styles.ok }, granted.value) : null
1847
2066
  ]
1848
2067
  ) : null,
1849
- w ? ledger.value.length === 0 ? empty("No credit movements yet", void 0, "subscriber") : h12("div", [
2068
+ w ? ledger.value.length === 0 ? empty(t2("billing.noMovements"), void 0, "subscriber") : h12("div", [
1850
2069
  table(
1851
- ["When", "Kind", "Amount", "Note"],
2070
+ [
2071
+ t2("billing.col.when"),
2072
+ t2("billing.col.kind"),
2073
+ t2("billing.col.amount"),
2074
+ t2("billing.col.note")
2075
+ ],
1852
2076
  ledger.value.map(
1853
- (t2) => h12("tr", { key: t2.id }, [
1854
- td(new Date(t2.createdAt).toLocaleString(), {
2077
+ (tx) => h12("tr", { key: tx.id }, [
2078
+ td(formatAdminDate6(tx.createdAt, props.locale), {
1855
2079
  ...styles.muted,
1856
2080
  whiteSpace: "nowrap"
1857
2081
  }),
1858
- td(t2.type),
1859
- td(t2.amount, styles.mono),
1860
- td(t2.description ?? "", styles.muted)
2082
+ td(ledgerKind(tx.type)),
2083
+ td(tx.amount, styles.mono),
2084
+ td(tx.description ?? "", styles.muted)
1861
2085
  ])
1862
2086
  )
1863
2087
  ),
1864
- hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger()) : null
2088
+ hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger(), t2("common.loadMore")) : null
1865
2089
  ]) : null
1866
2090
  ]);
1867
2091
  };
1868
2092
  }
1869
2093
  });
2094
+ var SubscriberBilling = defineComponent10({
2095
+ name: "FonderieSubscriberBilling",
2096
+ props: {
2097
+ client: { type: Object, required: true },
2098
+ subscriberType: { type: String, required: true },
2099
+ subscriberId: { type: String, required: true },
2100
+ locale: { type: String, default: void 0 }
2101
+ },
2102
+ setup(props) {
2103
+ const currency = ref5("");
2104
+ return () => h12(SubscriberBillingInner, {
2105
+ key: `${props.subscriberType}:${props.subscriberId}:${currency.value}`,
2106
+ client: props.client,
2107
+ subscriberType: props.subscriberType,
2108
+ subscriberId: props.subscriberId,
2109
+ currency: currency.value,
2110
+ ...props.locale ? { locale: props.locale } : {},
2111
+ "onChange-currency": (c) => {
2112
+ currency.value = c;
2113
+ }
2114
+ });
2115
+ }
2116
+ });
1870
2117
 
1871
2118
  // src/screens/UsersScreen.ts
1872
2119
  var UsersScreen = defineComponent11({
@@ -1878,7 +2125,8 @@ var UsersScreen = defineComponent11({
1878
2125
  // their page. Everyone has a wallet; no subscription means the free tier.
1879
2126
  billingClient: { type: Object, default: void 0 },
1880
2127
  // Open this user on arrival (e.g. from the Subscriptions list).
1881
- openUserId: { type: String, default: void 0 }
2128
+ openUserId: { type: String, default: void 0 },
2129
+ locale: { type: String, default: void 0 }
1882
2130
  },
1883
2131
  setup(props) {
1884
2132
  const input = ref6("");
@@ -1896,16 +2144,18 @@ var UsersScreen = defineComponent11({
1896
2144
  const sessions = useAdminUserSessions(props.client, userId);
1897
2145
  const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
1898
2146
  const showList = computed2(() => !email.value && !selectedId.value);
1899
- const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
2147
+ const tr = computed2(() => createAdminT12(props.locale));
2148
+ const yesNo = (v) => v ? pill("ok", tr.value("common.yes")) : pill("neutral", tr.value("common.no"));
1900
2149
  const row = (k, v) => h13("tr", [td(k), td(v)]);
1901
2150
  const planCell = (id) => {
1902
2151
  if (!plans.value) return h13("span", { style: styles.muted }, "\u2026");
1903
2152
  const sub = plans.value.get(`user/${id}`);
1904
- if (!sub || sub.status === "canceled") return h13("span", { style: styles.muted }, "free");
2153
+ if (!sub || sub.status === "canceled")
2154
+ return h13("span", { style: styles.muted }, tr.value("common.status.free"));
1905
2155
  return [
1906
2156
  h13("strong", sub.plan),
1907
2157
  " ",
1908
- sub.status !== "active" ? pill(statusTone(sub.status), sub.status) : null
2158
+ sub.status !== "active" ? pill(statusTone(sub.status), statusLabel(tr.value, sub.status)) : null
1909
2159
  ];
1910
2160
  };
1911
2161
  const clear = () => {
@@ -1914,13 +2164,12 @@ var UsersScreen = defineComponent11({
1914
2164
  input.value = "";
1915
2165
  };
1916
2166
  return () => {
2167
+ const t2 = tr.value;
2168
+ const loc = props.locale;
1917
2169
  const u = user.value;
1918
2170
  const list = showDeleted.value ? deletedList : activeList;
1919
2171
  return h13("div", { style: styles.container }, [
1920
- pageHeader(
1921
- "Users",
1922
- "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1923
- ),
2172
+ pageHeader(t2("users.title"), t2("users.lead")),
1924
2173
  h13(
1925
2174
  "form",
1926
2175
  {
@@ -1936,16 +2185,26 @@ var UsersScreen = defineComponent11({
1936
2185
  type: "email",
1937
2186
  value: input.value,
1938
2187
  onInput: (e) => input.value = e.target.value,
1939
- placeholder: "email address",
2188
+ placeholder: t2("users.emailPlaceholder"),
1940
2189
  style: { ...styles.input, minWidth: "280px" },
1941
- "aria-label": "Email"
2190
+ "aria-label": t2("users.emailLabel")
1942
2191
  }),
1943
2192
  h13(
1944
2193
  "button",
1945
2194
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1946
- [icon("search", 14), "Look up"]
2195
+ [icon("search", 14), t2("common.lookUp")]
2196
+ ),
2197
+ showList.value ? refreshButton2(
2198
+ t2("common.refresh"),
2199
+ list.isLoading.value,
2200
+ () => void list.refresh(),
2201
+ t2("common.working")
2202
+ ) : actionButton(
2203
+ [icon("back", 14), t2("users.allUsers")],
2204
+ false,
2205
+ clear,
2206
+ styles.buttonGhost
1947
2207
  ),
1948
- showList.value ? refreshButton2("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost),
1949
2208
  showList.value ? h13(
1950
2209
  "fieldset",
1951
2210
  {
@@ -1957,7 +2216,7 @@ var UsersScreen = defineComponent11({
1957
2216
  padding: "0",
1958
2217
  minWidth: "0"
1959
2218
  },
1960
- "aria-label": "Which accounts"
2219
+ "aria-label": t2("users.whichAccounts")
1961
2220
  },
1962
2221
  [false, true].map(
1963
2222
  (d) => h13(
@@ -1972,7 +2231,7 @@ var UsersScreen = defineComponent11({
1972
2231
  height: "28px"
1973
2232
  }
1974
2233
  },
1975
- d ? "Deleted" : "Active"
2234
+ d ? t2("users.deletedAccounts") : t2("users.activeAccounts")
1976
2235
  )
1977
2236
  )
1978
2237
  ) : null
@@ -1980,8 +2239,19 @@ var UsersScreen = defineComponent11({
1980
2239
  ),
1981
2240
  showList.value ? h13("div", [
1982
2241
  list.error.value ? h13("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1983
- list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
1984
- props.billingClient ? ["Email", "Name", "Plan", "Created", "Status"] : ["Email", "Name", "Created", "Status"],
2242
+ list.users.value.length === 0 && !list.isLoading.value ? empty(t2("users.emptyTitle"), t2("users.emptyBody"), "users") : table(
2243
+ props.billingClient ? [
2244
+ t2("users.col.email"),
2245
+ t2("users.col.name"),
2246
+ t2("users.col.plan"),
2247
+ t2("users.col.created"),
2248
+ t2("users.col.status")
2249
+ ] : [
2250
+ t2("users.col.email"),
2251
+ t2("users.col.name"),
2252
+ t2("users.col.created"),
2253
+ t2("users.col.status")
2254
+ ],
1985
2255
  list.users.value.map(
1986
2256
  (u2) => h13("tr", { key: u2.id }, [
1987
2257
  td(
@@ -1999,88 +2269,94 @@ var UsersScreen = defineComponent11({
1999
2269
  `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h13("span", { style: styles.muted }, "\u2014")
2000
2270
  ),
2001
2271
  props.billingClient ? td(planCell(u2.id)) : null,
2002
- td(new Date(u2.createdAt).toLocaleDateString()),
2272
+ td(formatAdminDate7(u2.createdAt, loc, "date")),
2003
2273
  td(
2004
- u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
2274
+ u2.suspended ? pill("warn", t2("common.status.suspended")) : u2.deletedAt ? pill("neutral", t2("common.status.deleted")) : pill("ok", t2("common.status.active"))
2005
2275
  )
2006
2276
  ])
2007
2277
  )
2008
2278
  ),
2009
- list.isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
2010
- list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
2279
+ list.isLoading.value ? h13("p", { style: styles.status }, t2("common.loading")) : null,
2280
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore(), t2("common.loadMore")) : null
2011
2281
  ]) : null,
2012
2282
  error.value && !(error.value.status === 404 && selectedId.value) ? h13(
2013
2283
  "p",
2014
2284
  { style: styles.error, role: "alert" },
2015
- error.value.status === 404 ? "No user with that email." : error.value.explanation
2285
+ error.value.status === 404 ? t2("users.noUserWithEmail") : error.value.explanation
2016
2286
  ) : null,
2017
2287
  // Opened by id (e.g. from Subscriptions) and the account is gone —
2018
2288
  // deleted or purged — while its billing rows remain. Say so, and
2019
2289
  // still show the money.
2020
2290
  error.value?.status === 404 && selectedId.value ? [
2021
2291
  h13("div", { style: styles.notice, role: "status" }, [
2022
- h13("strong", "No account with id "),
2292
+ h13("strong", `${t2("users.missingIdPrefix")} `),
2023
2293
  h13("code", { style: styles.code }, selectedId.value),
2024
2294
  h13("strong", "."),
2025
- " It was deleted, or never existed here. Its billing records remain."
2295
+ ` ${t2("users.missingIdBody")}`
2026
2296
  ]),
2027
- props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
2297
+ props.billingClient ? h13("h2", { style: styles.subtitle }, t2("users.planCredits")) : null,
2028
2298
  props.billingClient ? h13(SubscriberBilling, {
2029
2299
  key: `missing-${selectedId.value}`,
2030
2300
  client: props.billingClient,
2031
2301
  subscriberType: "user",
2032
- subscriberId: selectedId.value
2302
+ subscriberId: selectedId.value,
2303
+ ...loc ? { locale: loc } : {}
2033
2304
  }) : null
2034
2305
  ] : null,
2035
2306
  u ? [
2036
2307
  h13("h2", { style: styles.subtitle }, [
2037
2308
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
2038
2309
  " ",
2039
- u.suspended ? pill("warn", "suspended") : null,
2040
- u.deletedAt ? pill("neutral", "deleted") : null
2310
+ u.suspended ? pill("warn", t2("common.status.suspended")) : null,
2311
+ u.deletedAt ? pill("neutral", t2("common.status.deleted")) : null
2041
2312
  ]),
2042
2313
  h13("table", { style: styles.table }, [
2043
2314
  h13("tbody", [
2044
- row("id", h13("span", { style: styles.mono }, u.id)),
2045
- row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
2046
- row("MFA", yesNo(u.mfaEnabled)),
2315
+ row(t2("users.field.id"), h13("span", { style: styles.mono }, u.id)),
2316
+ row(t2("users.field.email"), [
2317
+ u.email,
2318
+ ` \xB7 ${t2("users.field.verified")} `,
2319
+ yesNo(u.isEmailVerified)
2320
+ ]),
2321
+ row(t2("users.field.mfa"), yesNo(u.mfaEnabled)),
2047
2322
  row(
2048
- "provider",
2049
- `${u.provider || "password"}${u.hasPassword ? "" : " \xB7 no password set"}`
2323
+ t2("users.field.provider"),
2324
+ `${u.provider || t2("users.passwordProvider")}${u.hasPassword ? "" : ` \xB7 ${t2("users.noPasswordSet")}`}`
2050
2325
  ),
2051
2326
  row(
2052
- "last login",
2053
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h13("span", { style: styles.muted }, "never")
2327
+ t2("users.field.lastLogin"),
2328
+ u.lastLogin ? formatAdminDate7(u.lastLogin, loc) : h13("span", { style: styles.muted }, t2("common.never"))
2054
2329
  ),
2055
- row("created", new Date(u.createdAt).toLocaleString())
2330
+ row(t2("users.field.created"), formatAdminDate7(u.createdAt, loc))
2056
2331
  ])
2057
2332
  ]),
2058
2333
  u.deletedAt ? h13("div", { style: { ...styles.notice, marginTop: "12px" }, role: "status" }, [
2059
- h13("strong", `Deleted on ${new Date(u.deletedAt).toLocaleString()}.`),
2060
- " The account cannot sign in, and it is erased when the retention purge runs. Its billing records stay."
2334
+ h13("strong", t2("users.deletedOn", { date: formatAdminDate7(u.deletedAt, loc) })),
2335
+ ` ${t2("users.deletedBody")}`
2061
2336
  ]) : h13("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
2062
- u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
2063
- "Suspend",
2337
+ u.suspended ? actionButton(t2("users.unsuspend"), isLoading.value, () => void unsuspend()) : actionButton(
2338
+ t2("users.suspend"),
2064
2339
  isLoading.value,
2065
2340
  () => void suspend(),
2066
2341
  styles.buttonDanger
2067
2342
  ),
2068
2343
  actionButton(
2069
- "Sign out everywhere",
2344
+ t2("users.signOutEverywhere"),
2070
2345
  isLoading.value,
2071
2346
  () => void revokeSessions().then(() => sessions.refresh())
2072
2347
  )
2073
2348
  ]),
2074
- props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
2349
+ props.billingClient ? h13("h2", { style: styles.subtitle }, t2("users.planCredits")) : null,
2075
2350
  props.billingClient ? h13(SubscriberBilling, {
2076
2351
  key: u.id,
2077
2352
  client: props.billingClient,
2078
2353
  subscriberType: "user",
2079
- subscriberId: u.id
2354
+ subscriberId: u.id,
2355
+ ...loc ? { locale: loc } : {}
2080
2356
  }) : null,
2081
2357
  u.deletedAt ? null : [
2082
- h13("h2", { style: styles.subtitle }, "Live sessions"),
2083
- sessions.sessions.value.length === 0 ? h13("p", { style: styles.muted }, "None.") : h13(
2358
+ h13("h2", { style: styles.subtitle }, t2("users.liveSessions")),
2359
+ sessions.sessions.value.length === 0 ? h13("p", { style: styles.muted }, t2("common.none")) : h13(
2084
2360
  "ul",
2085
2361
  { style: styles.list },
2086
2362
  sessions.sessions.value.map(
@@ -2098,18 +2374,21 @@ var UsersScreen = defineComponent11({
2098
2374
  h13(
2099
2375
  "span",
2100
2376
  { style: styles.muted },
2101
- `since ${new Date(s.createdAt).toLocaleString()}`
2377
+ t2("users.since", { date: formatAdminDate7(s.createdAt, loc) })
2102
2378
  )
2103
2379
  ])
2104
2380
  )
2105
2381
  ),
2106
- h13("h2", { style: styles.subtitle }, "Recent sign-ins"),
2107
- history.events.value.length === 0 && !history.isLoading.value ? h13("p", { style: styles.muted }, "None recorded.") : h13(
2382
+ h13("h2", { style: styles.subtitle }, t2("users.recentSignIns")),
2383
+ history.events.value.length === 0 && !history.isLoading.value ? h13("p", { style: styles.muted }, t2("users.noneRecorded")) : h13(
2108
2384
  "ul",
2109
2385
  { style: styles.list },
2110
2386
  history.events.value.map(
2111
2387
  (e) => h13("li", { key: e.id, style: styles.row }, [
2112
- pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
2388
+ pill(
2389
+ e.outcome === "success" ? "ok" : "bad",
2390
+ e.outcome === "success" ? t2("users.outcome.success") : e.outcome === "failure" ? t2("users.outcome.failure") : e.outcome
2391
+ ),
2113
2392
  " ",
2114
2393
  h13("span", { style: styles.muted }, e.method),
2115
2394
  " ",
@@ -2120,17 +2399,19 @@ var UsersScreen = defineComponent11({
2120
2399
  { style: styles.muted },
2121
2400
  describeLocation(e.location) ?? ""
2122
2401
  ),
2123
- e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
2402
+ e.location?.proxy || e.location?.hosting ? [
2403
+ " ",
2404
+ pill(
2405
+ "warn",
2406
+ e.location.proxy ? t2("users.proxyVpn") : t2("users.hosting")
2407
+ )
2408
+ ] : "",
2124
2409
  " ",
2125
- h13(
2126
- "span",
2127
- { style: styles.muted },
2128
- new Date(e.createdAt).toLocaleString()
2129
- )
2410
+ h13("span", { style: styles.muted }, formatAdminDate7(e.createdAt, loc))
2130
2411
  ])
2131
2412
  )
2132
2413
  ),
2133
- history.hasMore.value && !history.isLoading.value ? loadMoreButton(() => void history.loadMore()) : null
2414
+ history.hasMore.value && !history.isLoading.value ? loadMoreButton(() => void history.loadMore(), t2("common.loadMore")) : null
2134
2415
  ]
2135
2416
  ] : null
2136
2417
  ]);
@@ -2139,26 +2420,34 @@ var UsersScreen = defineComponent11({
2139
2420
  });
2140
2421
 
2141
2422
  // src/screens/CatalogScreen.ts
2423
+ import { createAdminT as createAdminT13 } from "@fonderie/client";
2142
2424
  import { useAdminCatalog } from "@fonderie/vue-admin";
2143
2425
  import { defineComponent as defineComponent12, h as h14 } from "vue";
2144
2426
  var CatalogScreen = defineComponent12({
2145
2427
  name: "FonderieCatalogScreen",
2146
- props: { client: { type: Object, required: true } },
2428
+ props: {
2429
+ client: { type: Object, required: true },
2430
+ locale: { type: String, default: void 0 }
2431
+ },
2147
2432
  setup(props) {
2148
2433
  const { catalog, isLoading, error, refresh, deletePlan } = useAdminCatalog(props.client);
2149
2434
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
2150
2435
  return () => {
2436
+ const t2 = createAdminT13(props.locale);
2151
2437
  const c = catalog.value;
2152
2438
  return h14("div", { style: styles.container }, [
2153
- pageHeader(
2154
- "Catalog",
2155
- "What you sell: plans as configured in code, and as stored in the database, side by side.",
2156
- [refreshButton2("Refresh", isLoading.value, () => void refresh())]
2157
- ),
2439
+ pageHeader(t2("catalog.title"), t2("catalog.lead"), [
2440
+ refreshButton2(
2441
+ t2("common.refresh"),
2442
+ isLoading.value,
2443
+ () => void refresh(),
2444
+ t2("common.working")
2445
+ )
2446
+ ]),
2158
2447
  error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2159
- isLoading.value && !c ? h14("p", { style: styles.status }, "Loading\u2026") : null,
2448
+ isLoading.value && !c ? h14("p", { style: styles.status }, t2("common.loading")) : null,
2160
2449
  c ? [
2161
- h14("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
2450
+ h14("h2", { style: { ...styles.subtitle, marginTop: 0 } }, t2("catalog.configured")),
2162
2451
  h14(
2163
2452
  "pre",
2164
2453
  {
@@ -2173,13 +2462,17 @@ var CatalogScreen = defineComponent12({
2173
2462
  },
2174
2463
  JSON.stringify(c.configured, null, 2)
2175
2464
  ),
2176
- h14("h2", { style: styles.subtitle }, "Stored (database)"),
2177
- c.stored.length === 0 ? empty(
2178
- "No stored plans",
2179
- "Plans are configured in code; nothing has been written to the database.",
2180
- "catalog"
2181
- ) : table(
2182
- ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
2465
+ h14("h2", { style: styles.subtitle }, t2("catalog.stored")),
2466
+ c.stored.length === 0 ? empty(t2("catalog.emptyTitle"), t2("catalog.emptyBody"), "catalog") : table(
2467
+ [
2468
+ t2("catalog.col.plan"),
2469
+ t2("catalog.col.tier"),
2470
+ t2("catalog.col.seats"),
2471
+ t2("catalog.col.monthly"),
2472
+ t2("catalog.col.yearly"),
2473
+ t2("catalog.col.trial"),
2474
+ ""
2475
+ ],
2183
2476
  c.stored.map(
2184
2477
  (p) => h14("tr", { key: p.id }, [
2185
2478
  td([
@@ -2190,7 +2483,7 @@ var CatalogScreen = defineComponent12({
2190
2483
  td(p.seats == null ? "\u221E" : String(p.seats)),
2191
2484
  td(`${money(p.pricing?.monthly)} ${p.pricing?.currency ?? ""}`),
2192
2485
  td(money(p.pricing?.yearly)),
2193
- td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
2486
+ td(p.trialDays ? t2("catalog.trialDays", { n: p.trialDays }) : "\u2014"),
2194
2487
  td(
2195
2488
  h14(
2196
2489
  "button",
@@ -2198,12 +2491,12 @@ var CatalogScreen = defineComponent12({
2198
2491
  type: "button",
2199
2492
  style: styles.buttonDanger,
2200
2493
  onClick: () => {
2201
- if (window.confirm(`Delete stored plan "${p.name}"?`))
2494
+ if (window.confirm(t2("catalog.deleteConfirm", { name: p.name })))
2202
2495
  void deletePlan(p.id).catch(() => {
2203
2496
  });
2204
2497
  }
2205
2498
  },
2206
- "Delete"
2499
+ t2("common.delete")
2207
2500
  )
2208
2501
  )
2209
2502
  ])
@@ -2216,14 +2509,17 @@ var CatalogScreen = defineComponent12({
2216
2509
  });
2217
2510
 
2218
2511
  // src/screens/SubscriberScreen.ts
2512
+ import {
2513
+ createAdminT as createAdminT14
2514
+ } from "@fonderie/client";
2219
2515
  import { useAdminSubscribers } from "@fonderie/vue-admin";
2220
2516
  import { defineComponent as defineComponent13, h as h15, ref as ref7 } from "vue";
2221
2517
  var FILTERS = [
2222
- { label: "All", match: () => true },
2223
- { label: "Active", match: (s) => s === "active" },
2224
- { label: "Trialing", match: (s) => s === "trialing" },
2225
- { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2226
- { label: "Canceled", match: (s) => s === "canceled" }
2518
+ { id: "all", match: () => true },
2519
+ { id: "active", match: (s) => s === "active" },
2520
+ { id: "trialing", match: (s) => s === "trialing" },
2521
+ { id: "pastDue", match: (s) => s === "past_due" || s === "unpaid" },
2522
+ { id: "canceled", match: (s) => s === "canceled" }
2227
2523
  ];
2228
2524
  var SubscriberScreen = defineComponent13({
2229
2525
  name: "FonderieSubscriberScreen",
@@ -2234,11 +2530,12 @@ var SubscriberScreen = defineComponent13({
2234
2530
  * A user subscriber opens on the Users page, where their plan and credits
2235
2531
  * live beside their account. Omit to open everything here.
2236
2532
  */
2237
- onOpenUser: { type: Function, default: void 0 }
2533
+ onOpenUser: { type: Function, default: void 0 },
2534
+ locale: { type: String, default: void 0 }
2238
2535
  },
2239
2536
  setup(props) {
2240
2537
  const list = useAdminSubscribers(props.client, { limit: props.pageSize });
2241
- const filter = ref7("All");
2538
+ const filter = ref7("all");
2242
2539
  const open = ref7(null);
2243
2540
  const type = ref7("workspace");
2244
2541
  const idInput = ref7("");
@@ -2247,32 +2544,38 @@ var SubscriberScreen = defineComponent13({
2247
2544
  else open.value = { type: t2, id };
2248
2545
  };
2249
2546
  return () => {
2547
+ const t2 = createAdminT14(props.locale);
2548
+ const loc = props.locale ? { locale: props.locale } : {};
2250
2549
  const o = open.value;
2251
2550
  if (o) {
2252
2551
  return h15("div", { style: styles.container }, [
2253
- pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
2552
+ pageHeader(`${o.type}/${o.id}`, t2("billing.subscriptions.detailLead"), [
2254
2553
  h15(
2255
2554
  "button",
2256
2555
  { type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
2257
- [icon("back", 14), "All subscriptions"]
2556
+ [icon("back", 14), t2("billing.subscriptions.back")]
2258
2557
  )
2259
2558
  ]),
2260
2559
  h15(SubscriberBilling, {
2261
2560
  key: `${o.type}/${o.id}`,
2262
2561
  client: props.client,
2263
2562
  subscriberType: o.type,
2264
- subscriberId: o.id
2563
+ subscriberId: o.id,
2564
+ ...loc
2265
2565
  })
2266
2566
  ]);
2267
2567
  }
2268
- const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
2568
+ const match = FILTERS.find((f) => f.id === filter.value)?.match ?? (() => true);
2269
2569
  const rows = list.subscriptions.value.filter((s) => match(s.status));
2270
2570
  return h15("div", { style: styles.container }, [
2271
- pageHeader(
2272
- "Subscriptions",
2273
- "Who pays, and who is behind. A user's plan and credits are also on their page under Users \u2014 free users appear only there.",
2274
- [refreshButton(() => void list.refresh(), list.isLoading.value)]
2275
- ),
2571
+ pageHeader(t2("billing.subscriptions.title"), t2("billing.subscriptions.lead"), [
2572
+ refreshButton(
2573
+ () => void list.refresh(),
2574
+ list.isLoading.value,
2575
+ t2("common.refresh"),
2576
+ t2("common.working")
2577
+ )
2578
+ ]),
2276
2579
  h15("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
2277
2580
  h15(
2278
2581
  "fieldset",
@@ -2286,21 +2589,24 @@ var SubscriberScreen = defineComponent13({
2286
2589
  padding: 0,
2287
2590
  minWidth: 0
2288
2591
  },
2289
- "aria-label": "Filter by status"
2592
+ "aria-label": t2("billing.subscriptions.filterLabel")
2290
2593
  },
2291
2594
  FILTERS.map((f) => {
2292
2595
  const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
2293
- const on = filter.value === f.label;
2596
+ const on = filter.value === f.id;
2294
2597
  return h15(
2295
2598
  "button",
2296
2599
  {
2297
- key: f.label,
2600
+ key: f.id,
2298
2601
  type: "button",
2299
2602
  "aria-pressed": on,
2300
- onClick: () => filter.value = f.label,
2603
+ onClick: () => filter.value = f.id,
2301
2604
  style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
2302
2605
  },
2303
- [f.label, h15("span", { style: { opacity: 0.7 } }, String(n))]
2606
+ [
2607
+ t2(`billing.subscriptions.filter.${f.id}`),
2608
+ h15("span", { style: { opacity: 0.7 } }, String(n))
2609
+ ]
2304
2610
  );
2305
2611
  })
2306
2612
  ),
@@ -2320,37 +2626,42 @@ var SubscriberScreen = defineComponent13({
2320
2626
  value: type.value,
2321
2627
  onChange: (e) => type.value = e.target.value,
2322
2628
  style: styles.input,
2323
- "aria-label": "Type"
2629
+ "aria-label": t2("billing.subscriptions.typeLabel")
2324
2630
  },
2325
2631
  [
2326
- h15("option", { value: "workspace" }, "workspace"),
2327
- h15("option", { value: "user" }, "user")
2632
+ h15("option", { value: "workspace" }, t2("billing.subscriptions.typeWorkspace")),
2633
+ h15("option", { value: "user" }, t2("billing.subscriptions.typeUser"))
2328
2634
  ]
2329
2635
  ),
2330
2636
  h15("input", {
2331
2637
  value: idInput.value,
2332
2638
  onInput: (e) => idInput.value = e.target.value,
2333
- placeholder: "subscriber id",
2639
+ placeholder: t2("billing.subscriptions.idPlaceholder"),
2334
2640
  style: { ...styles.input, width: "220px" },
2335
- "aria-label": "Subscriber id"
2641
+ "aria-label": t2("billing.subscriptions.idLabel")
2336
2642
  }),
2337
2643
  h15(
2338
2644
  "button",
2339
2645
  { type: "submit", style: styles.button, disabled: !idInput.value.trim() },
2340
- [icon("search", 14), "Open"]
2646
+ [icon("search", 14), t2("common.open")]
2341
2647
  )
2342
2648
  ]
2343
2649
  )
2344
2650
  ]),
2345
2651
  list.error.value ? h15("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2346
2652
  rows.length === 0 && !list.isLoading.value ? empty(
2347
- filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
2348
- "Subscriptions appear here once someone checks out.",
2653
+ t2(`billing.subscriptions.empty.${filter.value}`),
2654
+ t2("billing.subscriptions.emptyHint"),
2349
2655
  "subscriber"
2350
2656
  ) : table(
2351
- ["Subscriber", "Plan", "Status", "Renews / ends"],
2657
+ [
2658
+ t2("billing.subscriptions.col.subscriber"),
2659
+ t2("billing.subscriptions.col.plan"),
2660
+ t2("billing.subscriptions.col.status"),
2661
+ t2("billing.subscriptions.col.renewsEnds")
2662
+ ],
2352
2663
  rows.map((s) => {
2353
- const end = periodEnd(s);
2664
+ const end = periodEnd(s, props.locale);
2354
2665
  return h15("tr", { key: s.id }, [
2355
2666
  td(
2356
2667
  h15(
@@ -2366,11 +2677,11 @@ var SubscriberScreen = defineComponent13({
2366
2677
  td([
2367
2678
  h15("strong", s.plan),
2368
2679
  " ",
2369
- h15("span", { style: styles.muted }, `\xB7 ${s.interval}`)
2680
+ h15("span", { style: styles.muted }, `\xB7 ${intervalLabel(t2, s.interval)}`)
2370
2681
  ]),
2371
2682
  td([
2372
- pill(statusTone(s.status), s.status),
2373
- s.cancelAtPeriodEnd && s.status !== "canceled" ? h15("span", [" ", pill("warn", "cancels", false)]) : null
2683
+ pill(statusTone(s.status), statusLabel(t2, s.status)),
2684
+ s.cancelAtPeriodEnd && s.status !== "canceled" ? h15("span", [" ", pill("warn", t2("billing.subscriptions.cancels"), false)]) : null
2374
2685
  ]),
2375
2686
  td(
2376
2687
  end ? [h15("span", { style: styles.muted }, end.label), ` ${end.date}`] : h15("span", { style: styles.muted }, "\u2014"),
@@ -2379,24 +2690,29 @@ var SubscriberScreen = defineComponent13({
2379
2690
  ]);
2380
2691
  })
2381
2692
  ),
2382
- list.isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
2383
- list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
2693
+ list.isLoading.value ? h15("p", { style: styles.status }, t2("common.loading")) : null,
2694
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore(), t2("common.loadMore")) : null
2384
2695
  ]);
2385
2696
  };
2386
2697
  }
2387
2698
  });
2388
2699
 
2389
2700
  // src/screens/AuditScreen.ts
2701
+ import {
2702
+ createAdminT as createAdminT15,
2703
+ formatAdminDate as formatAdminDate8
2704
+ } from "@fonderie/client";
2390
2705
  import { useAdminAudit } from "@fonderie/vue-admin";
2391
2706
  import { defineComponent as defineComponent14, h as h16, ref as ref8 } from "vue";
2392
2707
  var AuditScreen = defineComponent14({
2393
2708
  name: "FonderieAuditScreen",
2394
2709
  props: {
2395
2710
  client: { type: Object, required: true },
2396
- pageSize: { type: Number, default: 50 }
2711
+ pageSize: { type: Number, default: 50 },
2712
+ locale: { type: String, default: void 0 }
2397
2713
  },
2398
2714
  setup(props) {
2399
- const draft = ref8({ workspaceId: "", type: "", actorId: "" });
2715
+ const draft = ref8({ workspaceId: "", type: "", actorId: "", from: "", to: "" });
2400
2716
  const query = ref8({ limit: props.pageSize });
2401
2717
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
2402
2718
  props.client,
@@ -2409,87 +2725,120 @@ var AuditScreen = defineComponent14({
2409
2725
  style: styles.input,
2410
2726
  "aria-label": placeholder
2411
2727
  });
2412
- return () => h16("div", { style: styles.container }, [
2413
- pageHeader(
2414
- "Audit",
2415
- "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2416
- ),
2417
- h16(
2418
- "form",
2419
- {
2420
- style: styles.toolbar,
2421
- onSubmit: (e) => {
2422
- e.preventDefault();
2423
- const d = draft.value;
2424
- query.value = {
2425
- limit: props.pageSize,
2426
- ...d.workspaceId.trim() ? { workspaceId: d.workspaceId.trim() } : {},
2427
- ...d.type.trim() ? { type: d.type.trim() } : {},
2428
- ...d.actorId.trim() ? { actorId: d.actorId.trim() } : {}
2429
- };
2430
- }
2431
- },
2432
- [
2433
- field("workspaceId", "workspace id (all if empty)"),
2434
- field("type", "event type"),
2435
- field("actorId", "actor id"),
2436
- h16(
2437
- "button",
2438
- { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2439
- [icon("search", 14), "Filter"]
2440
- ),
2441
- refreshButton2("Refresh", isLoading.value, () => void refresh())
2442
- ]
2443
- ),
2444
- error.value ? h16("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2445
- events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
2446
- ["When", "Type", "Workspace", "Actor", "Request"],
2447
- events.value.map(
2448
- (e) => h16("tr", { key: e.id }, [
2449
- td(new Date(e.createdAt).toLocaleString(), styles.muted),
2450
- td(e.type, styles.mono),
2451
- td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
2452
- td(e.actorId ?? "\u2014", styles.mono),
2453
- td(e.requestId ?? "", styles.muted)
2454
- ])
2455
- )
2456
- ),
2457
- isLoading.value ? h16("p", { style: styles.status }, "Loading\u2026") : null,
2458
- hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
2459
- ]);
2728
+ return () => {
2729
+ const t2 = createAdminT15(props.locale);
2730
+ return h16("div", { style: styles.container }, [
2731
+ pageHeader(t2("audit.title"), t2("audit.lead")),
2732
+ h16(
2733
+ "form",
2734
+ {
2735
+ style: styles.toolbar,
2736
+ onSubmit: (e) => {
2737
+ e.preventDefault();
2738
+ const d = draft.value;
2739
+ query.value = {
2740
+ limit: props.pageSize,
2741
+ ...d.workspaceId.trim() ? { workspaceId: d.workspaceId.trim() } : {},
2742
+ ...d.type.trim() ? { type: d.type.trim() } : {},
2743
+ ...d.actorId.trim() ? { actorId: d.actorId.trim() } : {},
2744
+ // Dates are whole days in the operator's time zone: "to" includes that day.
2745
+ ...d.from ? { from: /* @__PURE__ */ new Date(`${d.from}T00:00:00`) } : {},
2746
+ ...d.to ? { to: /* @__PURE__ */ new Date(`${d.to}T23:59:59.999`) } : {}
2747
+ };
2748
+ }
2749
+ },
2750
+ [
2751
+ field("workspaceId", t2("audit.workspacePlaceholder")),
2752
+ field("type", t2("audit.typePlaceholder")),
2753
+ field("actorId", t2("audit.actorPlaceholder")),
2754
+ ...["from", "to"].map(
2755
+ (k) => h16("input", {
2756
+ type: "date",
2757
+ value: draft.value[k],
2758
+ onInput: (e) => draft.value = { ...draft.value, [k]: e.target.value },
2759
+ style: styles.input,
2760
+ "aria-label": k === "from" ? t2("audit.fromLabel") : t2("audit.toLabel"),
2761
+ title: k === "from" ? t2("audit.fromTitle") : t2("audit.toTitle")
2762
+ })
2763
+ ),
2764
+ h16(
2765
+ "button",
2766
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2767
+ [icon("search", 14), t2("common.filter")]
2768
+ ),
2769
+ refreshButton2(
2770
+ t2("common.refresh"),
2771
+ isLoading.value,
2772
+ () => void refresh(),
2773
+ t2("common.working")
2774
+ )
2775
+ ]
2776
+ ),
2777
+ error.value ? h16("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2778
+ events.value.length === 0 && !isLoading.value ? empty(t2("audit.empty"), void 0, "audit") : table(
2779
+ [
2780
+ t2("audit.col.when"),
2781
+ t2("audit.col.type"),
2782
+ t2("audit.col.workspace"),
2783
+ t2("audit.col.actor"),
2784
+ t2("audit.col.request")
2785
+ ],
2786
+ events.value.map(
2787
+ (e) => h16("tr", { key: e.id }, [
2788
+ td(formatAdminDate8(e.createdAt, props.locale), styles.muted),
2789
+ td(e.type, styles.mono),
2790
+ td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
2791
+ td(e.actorId ?? "\u2014", styles.mono),
2792
+ td(e.requestId ?? "", styles.muted)
2793
+ ])
2794
+ )
2795
+ ),
2796
+ isLoading.value ? h16("p", { style: styles.status }, t2("common.loading")) : null,
2797
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore(), t2("common.loadMore")) : null
2798
+ ]);
2799
+ };
2460
2800
  }
2461
2801
  });
2462
2802
 
2463
2803
  // src/screens/AdminShell.ts
2464
2804
  var NAV = [
2465
- { group: "Today", items: [{ page: "attention", label: "Attention" }] },
2805
+ { group: "nav.groups.today", items: [{ page: "attention", label: "nav.items.attention" }] },
2466
2806
  {
2467
- group: "System",
2807
+ group: "nav.groups.system",
2468
2808
  items: [
2469
- { page: "modules", label: "Modules" },
2470
- { page: "environment", label: "Environment" },
2471
- { page: "doctor", label: "Doctor" },
2472
- { page: "routes", label: "Routes" }
2809
+ { page: "modules", label: "nav.items.modules" },
2810
+ { page: "environment", label: "nav.items.environment" },
2811
+ { page: "doctor", label: "nav.items.doctor" },
2812
+ { page: "routes", label: "nav.items.routes" }
2473
2813
  ]
2474
2814
  },
2475
- { group: "People", items: [{ page: "users", label: "Users", needs: "auth" }] },
2476
2815
  {
2477
- group: "Money",
2816
+ group: "nav.groups.people",
2817
+ items: [{ page: "users", label: "nav.items.users", needs: "auth" }]
2818
+ },
2819
+ {
2820
+ group: "nav.groups.money",
2478
2821
  items: [
2479
- { page: "catalog", label: "Catalog", needs: "billing" },
2480
- { page: "subscriber", label: "Subscriptions", needs: "billing" }
2822
+ { page: "catalog", label: "nav.items.catalog", needs: "billing" },
2823
+ { page: "subscriber", label: "nav.items.subscriptions", needs: "billing" }
2481
2824
  ]
2482
2825
  },
2483
- { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
2484
- { group: "Messaging", items: [{ page: "templates", label: "Templates", needs: "courier" }] },
2485
2826
  {
2486
- group: "Activity",
2827
+ group: "nav.groups.settings",
2828
+ items: [{ page: "settings", label: "nav.items.settings", needs: "config" }]
2829
+ },
2830
+ {
2831
+ group: "nav.groups.messaging",
2832
+ items: [{ page: "templates", label: "nav.items.templates", needs: "courier" }]
2833
+ },
2834
+ {
2835
+ group: "nav.groups.activity",
2487
2836
  items: [
2488
- { page: "audit", label: "Audit", needs: "audit" },
2489
- { page: "log", label: "Admin log" },
2490
- { page: "operators", label: "Operators", needs: "operators" },
2491
- { page: "tokens", label: "Tokens" },
2492
- { page: "migrations", label: "Migrations" }
2837
+ { page: "audit", label: "nav.items.audit", needs: "audit" },
2838
+ { page: "log", label: "nav.items.log" },
2839
+ { page: "operators", label: "nav.items.operators", needs: "operators" },
2840
+ { page: "tokens", label: "nav.items.tokens" },
2841
+ { page: "migrations", label: "nav.items.migrations" }
2493
2842
  ]
2494
2843
  }
2495
2844
  ];
@@ -2538,12 +2887,19 @@ var AdminShell = defineComponent15({
2538
2887
  // shows the Operators page.
2539
2888
  operators: { type: Boolean, default: false },
2540
2889
  // The signed-in operator's email, when a person (not a token) is signed in.
2541
- currentOperator: { type: String, default: void 0 }
2890
+ currentOperator: { type: String, default: void 0 },
2891
+ // Where the app serves GET /config/public — marks public keys on the
2892
+ // Config page and previews exactly what frontends receive.
2893
+ publicConfigUrl: { type: String, default: void 0 },
2894
+ // The console's own language (the operator's choice, independent of the
2895
+ // locales the app serves). Default English.
2896
+ locale: { type: String, default: void 0 }
2542
2897
  },
2543
2898
  // `footer` slot: pinned to the bottom of the sidebar — session controls
2544
2899
  // (theme, sign out). The React shell takes the same thing as a `footer` prop.
2545
2900
  emits: { navigate: (_page) => true },
2546
2901
  setup(props, { emit, slots }) {
2902
+ const tr = computed3(() => createAdminT16(props.locale));
2547
2903
  const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2548
2904
  const narrow = ref9(query?.matches ?? false);
2549
2905
  const drawer = ref9(false);
@@ -2570,90 +2926,141 @@ var AdminShell = defineComponent15({
2570
2926
  const missing = (text) => h17("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2571
2927
  const body = () => {
2572
2928
  const c = props.client;
2929
+ const t2 = tr.value;
2930
+ const loc = props.locale ? { locale: props.locale } : {};
2931
+ const missingClient = (client) => missing(t2("shell.missingClient", { client }));
2573
2932
  switch (current.value) {
2574
2933
  case "attention":
2575
- return h17(AttentionScreen, { client: c });
2934
+ return h17(AttentionScreen, { client: c, ...loc });
2576
2935
  case "modules":
2577
- return h17(ModulesScreen, { client: c });
2936
+ return h17(ModulesScreen, { client: c, ...loc });
2578
2937
  case "doctor":
2579
- return h17(DoctorScreen, { client: c });
2938
+ return h17(DoctorScreen, { client: c, ...loc });
2580
2939
  case "environment":
2581
- return h17(EnvironmentScreen, { client: c });
2940
+ return h17(EnvironmentScreen, { client: c, ...loc });
2582
2941
  case "routes":
2583
- return h17(RoutesScreen, { client: c });
2942
+ return h17(RoutesScreen, { client: c, ...loc });
2584
2943
  case "tokens":
2585
- return h17(TokensScreen, { client: c });
2944
+ return h17(TokensScreen, { client: c, ...loc });
2586
2945
  case "operators":
2587
2946
  return h17(OperatorsScreen, {
2588
2947
  client: c,
2948
+ ...loc,
2589
2949
  ...props.currentOperator ? { me: props.currentOperator } : {}
2590
2950
  });
2591
2951
  case "migrations":
2592
- return h17(MigrationsScreen, { client: c });
2952
+ return h17(MigrationsScreen, { client: c, ...loc });
2593
2953
  case "log":
2594
- return h17(AdminLogScreen, { client: c });
2954
+ return h17(AdminLogScreen, { client: c, ...loc });
2595
2955
  case "catalog":
2596
- return props.billingClient ? h17(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2956
+ return props.billingClient ? h17(CatalogScreen, { client: props.billingClient, ...loc }) : missingClient("BillingAdminClient");
2597
2957
  case "subscriber":
2598
2958
  return props.billingClient ? h17(SubscriberScreen, {
2599
2959
  client: props.billingClient,
2960
+ ...loc,
2600
2961
  ...props.authClient ? {
2601
2962
  onOpenUser: (id) => {
2602
2963
  openUser.value = id;
2603
2964
  go("users");
2604
2965
  }
2605
2966
  } : {}
2606
- }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2967
+ }) : missingClient("BillingAdminClient");
2607
2968
  case "audit":
2608
- return props.auditClient ? h17(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2969
+ return props.auditClient ? h17(AuditScreen, { client: props.auditClient, ...loc }) : missingClient("AuditAdminClient");
2609
2970
  case "users":
2610
2971
  return props.authClient ? h17(UsersScreen, {
2611
2972
  key: openUser.value ?? "list",
2612
2973
  client: props.authClient,
2974
+ ...loc,
2613
2975
  ...props.billingClient ? { billingClient: props.billingClient } : {},
2614
2976
  ...openUser.value ? { openUserId: openUser.value } : {}
2615
- }) : missing("Pass an AuthAdminClient to look up users here.");
2977
+ }) : missingClient("AuthAdminClient");
2616
2978
  case "settings": {
2617
2979
  const cc = props.configClient;
2618
- if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
2980
+ if (!cc) return missingClient("ConfigAdminClient");
2619
2981
  const e = editing.value;
2620
- if (e && e.kind !== "template")
2982
+ if (e && (e.kind === "config" || e.kind === "secret")) {
2983
+ const env = e.environment ?? props.environment;
2621
2984
  return [
2622
- back("Config & secrets"),
2985
+ back(t2("nav.items.settings")),
2623
2986
  h17(ConfigEditorScreen, {
2987
+ key: `${e.kind}:${e.key}:${e.environment ?? ""}`,
2624
2988
  client: cc,
2989
+ ...loc,
2625
2990
  kind: e.kind,
2626
2991
  configKey: e.key,
2627
- ...props.environment !== void 0 ? { environment: props.environment } : {},
2628
- onSaved: () => editing.value = null
2992
+ ...env !== void 0 ? { environment: env } : {},
2993
+ ...e.environments ? { environments: e.environments } : {},
2994
+ onSaved: () => editing.value = null,
2995
+ onDeleted: () => editing.value = null
2629
2996
  })
2630
2997
  ];
2998
+ }
2999
+ const create = (kind) => (c2) => editing.value = {
3000
+ kind,
3001
+ key: "",
3002
+ environments: c2.environments,
3003
+ ...c2.environment ? { environment: c2.environment } : {}
3004
+ };
2631
3005
  return h17(ConfigListScreen, {
2632
3006
  client: cc,
3007
+ ...loc,
2633
3008
  ...props.environment !== void 0 ? { environment: props.environment } : {},
2634
- "onSelect-config": (key) => editing.value = { kind: "config", key },
2635
- "onSelect-secret": (key) => editing.value = { kind: "secret", key },
3009
+ ...props.publicConfigUrl ? { publicConfigUrl: props.publicConfigUrl } : {},
3010
+ "onSelect-config": (key, environment) => editing.value = { kind: "config", key, environment },
3011
+ "onSelect-secret": (key, environment) => editing.value = { kind: "secret", key, environment },
2636
3012
  allowCreate: true,
2637
- "onCreate-config": () => editing.value = { kind: "config", key: "" },
2638
- "onCreate-secret": () => editing.value = { kind: "secret", key: "" }
3013
+ "onCreate-config": create("config"),
3014
+ "onCreate-secret": create("secret")
2639
3015
  });
2640
3016
  }
2641
3017
  case "templates": {
2642
3018
  const kc = props.courierClient;
2643
- if (!kc) return missing("Pass a CourierAdminClient to manage templates here.");
3019
+ if (!kc) return missingClient("CourierAdminClient");
2644
3020
  const e = editing.value;
3021
+ if (e?.kind === "template-new")
3022
+ return [
3023
+ back(t2("nav.items.templates")),
3024
+ h17(TemplateCreateScreen, {
3025
+ client: kc,
3026
+ ...loc,
3027
+ ...e.type ? { type: e.type } : {},
3028
+ ...e.locales ? { locales: e.locales } : {},
3029
+ onCreated: (c2) => editing.value = {
3030
+ kind: "template",
3031
+ type: c2.type,
3032
+ locale: c2.locale,
3033
+ system: false
3034
+ }
3035
+ })
3036
+ ];
2645
3037
  if (e?.kind === "template")
2646
3038
  return [
2647
- back("Templates"),
3039
+ back(t2("nav.items.templates")),
2648
3040
  h17(TemplateEditorScreen, {
3041
+ key: `${e.type}:${e.locale ?? ""}`,
2649
3042
  client: kc,
3043
+ ...props.locale ? { uiLocale: props.locale } : {},
2650
3044
  type: e.type,
2651
- onSaved: () => editing.value = null
3045
+ locale: e.locale,
3046
+ system: e.system,
3047
+ allowAddLocale: true,
3048
+ onSaved: () => editing.value = null,
3049
+ onDeleted: () => editing.value = null,
3050
+ "onAdd-locale": (type) => editing.value = { kind: "template-new", type }
2652
3051
  })
2653
3052
  ];
2654
3053
  return h17(TemplateListScreen, {
2655
3054
  client: kc,
2656
- "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
3055
+ ...loc,
3056
+ allowCreate: true,
3057
+ "onSelect-template": (t3) => editing.value = {
3058
+ kind: "template",
3059
+ type: t3.type,
3060
+ locale: t3.locale ?? null,
3061
+ system: t3.system === true
3062
+ },
3063
+ "onCreate-template": (c2) => editing.value = { kind: "template-new", locales: c2.locales }
2657
3064
  });
2658
3065
  }
2659
3066
  }
@@ -2687,7 +3094,7 @@ var AdminShell = defineComponent15({
2687
3094
  {
2688
3095
  type: "button",
2689
3096
  style: styles.buttonGhost,
2690
- "aria-label": "Close menu",
3097
+ "aria-label": tr.value("shell.closeMenu"),
2691
3098
  onClick: () => drawer.value = false
2692
3099
  },
2693
3100
  [icon("close")]
@@ -2701,7 +3108,7 @@ var AdminShell = defineComponent15({
2701
3108
  ...styles.nav,
2702
3109
  ...narrow.value ? { width: "280px", maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2703
3110
  },
2704
- "aria-label": "Admin"
3111
+ "aria-label": tr.value("shell.navLabel")
2705
3112
  },
2706
3113
  [
2707
3114
  brand(),
@@ -2712,7 +3119,7 @@ var AdminShell = defineComponent15({
2712
3119
  const items = g.items.filter(visible);
2713
3120
  if (items.length === 0) return null;
2714
3121
  return h17("div", { key: g.group }, [
2715
- h17("div", { style: styles.navGroup }, g.group),
3122
+ h17("div", { style: styles.navGroup }, tr.value(g.group)),
2716
3123
  ...items.map((i) => {
2717
3124
  const active = current.value === i.page;
2718
3125
  return h17(
@@ -2728,7 +3135,10 @@ var AdminShell = defineComponent15({
2728
3135
  go(i.page);
2729
3136
  }
2730
3137
  },
2731
- [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
3138
+ [
3139
+ icon(ICON[i.page], 16, active ? styles.navIconActive : void 0),
3140
+ tr.value(i.label)
3141
+ ]
2732
3142
  );
2733
3143
  })
2734
3144
  ]);
@@ -2738,7 +3148,8 @@ var AdminShell = defineComponent15({
2738
3148
  ]
2739
3149
  );
2740
3150
  return () => {
2741
- const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
3151
+ const titleKey = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label;
3152
+ const title = titleKey ? tr.value(titleKey) : "";
2742
3153
  return h17(
2743
3154
  "div",
2744
3155
  { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
@@ -2750,7 +3161,7 @@ var AdminShell = defineComponent15({
2750
3161
  {
2751
3162
  type: "button",
2752
3163
  style: styles.buttonGhost,
2753
- "aria-label": "Open menu",
3164
+ "aria-label": tr.value("shell.openMenu"),
2754
3165
  "aria-expanded": drawer.value,
2755
3166
  onClick: () => drawer.value = true
2756
3167
  },