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