@fonderie/vue-admin-screens 1.5.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,15 +1894,18 @@ 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");
1900
+ var LEDGER_KINDS = ["purchase", "grant", "usage", "refund", "adjustment", "expiry"];
1758
1901
  var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1759
1902
  name: "FonderieSubscriberBillingInner",
1760
1903
  props: {
1761
1904
  client: { type: Object, required: true },
1762
1905
  subscriberType: { type: String, required: true },
1763
1906
  subscriberId: { type: String, required: true },
1764
- currency: { type: String, default: "" }
1907
+ currency: { type: String, default: "" },
1908
+ locale: { type: String, default: void 0 }
1765
1909
  },
1766
1910
  emits: { "change-currency": (_currency) => true },
1767
1911
  setup(props, { emit }) {
@@ -1775,15 +1919,18 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1775
1919
  const note = (0, import_vue13.ref)("");
1776
1920
  const granted = (0, import_vue13.ref)(null);
1777
1921
  return () => {
1922
+ const t2 = (0, import_client11.createAdminT)(props.locale);
1923
+ const ledgerKind = (kind) => LEDGER_KINDS.includes(kind) ? t2(`billing.ledgerKind.${kind}`) : kind;
1778
1924
  const s = subscription.value;
1779
1925
  const w = wallet.value;
1780
- if (isLoading.value && !w && !s) return (0, import_vue13.h)("p", { style: styles.status }, "Loading billing\u2026");
1781
- 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;
1782
1929
  return (0, import_vue13.h)("div", [
1783
1930
  error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1784
1931
  (0, import_vue13.h)("div", { style: { ...styles.grid, marginBottom: "16px" } }, [
1785
1932
  (0, import_vue13.h)("div", { style: styles.card }, [
1786
- (0, import_vue13.h)("div", { style: styles.statLabel }, "Plan"),
1933
+ (0, import_vue13.h)("div", { style: styles.statLabel }, t2("billing.plan")),
1787
1934
  (0, import_vue13.h)(
1788
1935
  "div",
1789
1936
  {
@@ -1796,11 +1943,11 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1796
1943
  }
1797
1944
  },
1798
1945
  [
1799
- (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")),
1800
1947
  ...s ? [
1801
- (0, import_vue13.h)("span", { style: styles.muted }, `\xB7 ${s.interval}`),
1802
- pill(statusTone(s.status), s.status)
1803
- ] : [(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"))]
1804
1951
  ]
1805
1952
  ),
1806
1953
  end ? (0, import_vue13.h)("div", { style: styles.statHint }, `${end.label} ${end.date}`) : null,
@@ -1822,7 +1969,7 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1822
1969
  }
1823
1970
  },
1824
1971
  [
1825
- (0, import_vue13.h)("div", { style: styles.statLabel }, "Credits"),
1972
+ (0, import_vue13.h)("div", { style: styles.statLabel }, t2("billing.credits")),
1826
1973
  (0, import_vue13.h)(
1827
1974
  "form",
1828
1975
  {
@@ -1838,11 +1985,11 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1838
1985
  onInput: (e) => {
1839
1986
  currencyDraft.value = e.target.value;
1840
1987
  },
1841
- placeholder: w?.currency ?? "currency",
1988
+ placeholder: w?.currency ?? t2("billing.currencyPlaceholder"),
1842
1989
  maxlength: 8,
1843
1990
  style: { ...styles.input, height: "26px", width: "84px", fontSize: "12px" },
1844
- "aria-label": "Wallet currency",
1845
- title: "Show the wallet in another currency"
1991
+ "aria-label": t2("billing.currencyLabel"),
1992
+ title: t2("billing.currencyTitle")
1846
1993
  })
1847
1994
  ]
1848
1995
  )
@@ -1856,15 +2003,11 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1856
2003
  (0, import_vue13.h)(
1857
2004
  "div",
1858
2005
  { style: styles.statHint },
1859
- `${w.purchased ?? "0"} permanent \xB7 ${w.granted ?? "0"} plan allowance${w.grantedExpiresAt ? ` (expires ${new Date(w.grantedExpiresAt).toLocaleDateString()})` : ""}`
1860
- )
1861
- ] : [
1862
- (0, import_vue13.h)(
1863
- "div",
1864
- { style: styles.statHint },
1865
- "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
+ }) : ""}`
1866
2009
  )
1867
- ]
2010
+ ] : [(0, import_vue13.h)("div", { style: styles.statHint }, t2("billing.noWallet"))]
1868
2011
  ])
1869
2012
  ]),
1870
2013
  w ? (0, import_vue13.h)(
@@ -1887,7 +2030,7 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1887
2030
  ...note.value ? { description: note.value } : {},
1888
2031
  idempotencyKey: key
1889
2032
  }).then(() => {
1890
- granted.value = `Granted ${a} ${w.currency}.`;
2033
+ granted.value = t2("billing.granted", { amount: a, currency: w.currency });
1891
2034
  amount.value = "";
1892
2035
  note.value = "";
1893
2036
  }).catch(() => {
@@ -1895,21 +2038,25 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1895
2038
  }
1896
2039
  },
1897
2040
  [
1898
- (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
+ ),
1899
2046
  (0, import_vue13.h)("input", {
1900
2047
  value: amount.value,
1901
2048
  onInput: (e) => amount.value = e.target.value,
1902
- placeholder: `amount (${w.currency})`,
2049
+ placeholder: t2("billing.amountPlaceholder", { currency: w.currency }),
1903
2050
  inputmode: "numeric",
1904
2051
  style: { ...styles.input, width: "150px" },
1905
- "aria-label": "Amount"
2052
+ "aria-label": t2("billing.amountLabel")
1906
2053
  }),
1907
2054
  (0, import_vue13.h)("input", {
1908
2055
  value: note.value,
1909
2056
  onInput: (e) => note.value = e.target.value,
1910
- placeholder: "reason (optional)",
2057
+ placeholder: t2("billing.reasonPlaceholder"),
1911
2058
  style: { ...styles.input, flex: 1, minWidth: "180px" },
1912
- "aria-label": "Reason"
2059
+ "aria-label": t2("billing.reasonLabel")
1913
2060
  }),
1914
2061
  (0, import_vue13.h)(
1915
2062
  "button",
@@ -1918,27 +2065,32 @@ var SubscriberBillingInner = (0, import_vue13.defineComponent)({
1918
2065
  style: styles.buttonPrimary,
1919
2066
  disabled: !/^\d+$/.test(amount.value.trim())
1920
2067
  },
1921
- [icon("plus", 14), "Grant"]
2068
+ [icon("plus", 14), t2("billing.grant")]
1922
2069
  ),
1923
2070
  granted.value ? (0, import_vue13.h)("span", { style: styles.ok }, granted.value) : null
1924
2071
  ]
1925
2072
  ) : null,
1926
- 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", [
1927
2074
  table(
1928
- ["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
+ ],
1929
2081
  ledger.value.map(
1930
- (t2) => (0, import_vue13.h)("tr", { key: t2.id }, [
1931
- 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), {
1932
2084
  ...styles.muted,
1933
2085
  whiteSpace: "nowrap"
1934
2086
  }),
1935
- td(t2.type),
1936
- td(t2.amount, styles.mono),
1937
- td(t2.description ?? "", styles.muted)
2087
+ td(ledgerKind(tx.type)),
2088
+ td(tx.amount, styles.mono),
2089
+ td(tx.description ?? "", styles.muted)
1938
2090
  ])
1939
2091
  )
1940
2092
  ),
1941
- hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger()) : null
2093
+ hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger(), t2("common.loadMore")) : null
1942
2094
  ]) : null
1943
2095
  ]);
1944
2096
  };
@@ -1949,7 +2101,8 @@ var SubscriberBilling = (0, import_vue13.defineComponent)({
1949
2101
  props: {
1950
2102
  client: { type: Object, required: true },
1951
2103
  subscriberType: { type: String, required: true },
1952
- subscriberId: { type: String, required: true }
2104
+ subscriberId: { type: String, required: true },
2105
+ locale: { type: String, default: void 0 }
1953
2106
  },
1954
2107
  setup(props) {
1955
2108
  const currency = (0, import_vue13.ref)("");
@@ -1959,6 +2112,7 @@ var SubscriberBilling = (0, import_vue13.defineComponent)({
1959
2112
  subscriberType: props.subscriberType,
1960
2113
  subscriberId: props.subscriberId,
1961
2114
  currency: currency.value,
2115
+ ...props.locale ? { locale: props.locale } : {},
1962
2116
  "onChange-currency": (c) => {
1963
2117
  currency.value = c;
1964
2118
  }
@@ -1976,7 +2130,8 @@ var UsersScreen = (0, import_vue14.defineComponent)({
1976
2130
  // their page. Everyone has a wallet; no subscription means the free tier.
1977
2131
  billingClient: { type: Object, default: void 0 },
1978
2132
  // Open this user on arrival (e.g. from the Subscriptions list).
1979
- openUserId: { type: String, default: void 0 }
2133
+ openUserId: { type: String, default: void 0 },
2134
+ locale: { type: String, default: void 0 }
1980
2135
  },
1981
2136
  setup(props) {
1982
2137
  const input = (0, import_vue14.ref)("");
@@ -1994,16 +2149,18 @@ var UsersScreen = (0, import_vue14.defineComponent)({
1994
2149
  const sessions = (0, import_vue_admin11.useAdminUserSessions)(props.client, userId);
1995
2150
  const history = (0, import_vue_admin11.useAdminLoginHistory)(props.client, userId, { limit: 20 });
1996
2151
  const showList = (0, import_vue14.computed)(() => !email.value && !selectedId.value);
1997
- 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"));
1998
2154
  const row = (k, v) => (0, import_vue14.h)("tr", [td(k), td(v)]);
1999
2155
  const planCell = (id) => {
2000
2156
  if (!plans.value) return (0, import_vue14.h)("span", { style: styles.muted }, "\u2026");
2001
2157
  const sub = plans.value.get(`user/${id}`);
2002
- 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"));
2003
2160
  return [
2004
2161
  (0, import_vue14.h)("strong", sub.plan),
2005
2162
  " ",
2006
- sub.status !== "active" ? pill(statusTone(sub.status), sub.status) : null
2163
+ sub.status !== "active" ? pill(statusTone(sub.status), statusLabel(tr.value, sub.status)) : null
2007
2164
  ];
2008
2165
  };
2009
2166
  const clear = () => {
@@ -2012,13 +2169,12 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2012
2169
  input.value = "";
2013
2170
  };
2014
2171
  return () => {
2172
+ const t2 = tr.value;
2173
+ const loc = props.locale;
2015
2174
  const u = user.value;
2016
2175
  const list = showDeleted.value ? deletedList : activeList;
2017
2176
  return (0, import_vue14.h)("div", { style: styles.container }, [
2018
- pageHeader(
2019
- "Users",
2020
- "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
2021
- ),
2177
+ pageHeader(t2("users.title"), t2("users.lead")),
2022
2178
  (0, import_vue14.h)(
2023
2179
  "form",
2024
2180
  {
@@ -2034,16 +2190,26 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2034
2190
  type: "email",
2035
2191
  value: input.value,
2036
2192
  onInput: (e) => input.value = e.target.value,
2037
- placeholder: "email address",
2193
+ placeholder: t2("users.emailPlaceholder"),
2038
2194
  style: { ...styles.input, minWidth: "280px" },
2039
- "aria-label": "Email"
2195
+ "aria-label": t2("users.emailLabel")
2040
2196
  }),
2041
2197
  (0, import_vue14.h)(
2042
2198
  "button",
2043
2199
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2044
- [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
2045
2212
  ),
2046
- showList.value ? refreshButton2("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost),
2047
2213
  showList.value ? (0, import_vue14.h)(
2048
2214
  "fieldset",
2049
2215
  {
@@ -2055,7 +2221,7 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2055
2221
  padding: "0",
2056
2222
  minWidth: "0"
2057
2223
  },
2058
- "aria-label": "Which accounts"
2224
+ "aria-label": t2("users.whichAccounts")
2059
2225
  },
2060
2226
  [false, true].map(
2061
2227
  (d) => (0, import_vue14.h)(
@@ -2070,7 +2236,7 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2070
2236
  height: "28px"
2071
2237
  }
2072
2238
  },
2073
- d ? "Deleted" : "Active"
2239
+ d ? t2("users.deletedAccounts") : t2("users.activeAccounts")
2074
2240
  )
2075
2241
  )
2076
2242
  ) : null
@@ -2078,8 +2244,19 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2078
2244
  ),
2079
2245
  showList.value ? (0, import_vue14.h)("div", [
2080
2246
  list.error.value ? (0, import_vue14.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2081
- list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
2082
- 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
+ ],
2083
2260
  list.users.value.map(
2084
2261
  (u2) => (0, import_vue14.h)("tr", { key: u2.id }, [
2085
2262
  td(
@@ -2097,88 +2274,94 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2097
2274
  `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue14.h)("span", { style: styles.muted }, "\u2014")
2098
2275
  ),
2099
2276
  props.billingClient ? td(planCell(u2.id)) : null,
2100
- td(new Date(u2.createdAt).toLocaleDateString()),
2277
+ td((0, import_client12.formatAdminDate)(u2.createdAt, loc, "date")),
2101
2278
  td(
2102
- 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"))
2103
2280
  )
2104
2281
  ])
2105
2282
  )
2106
2283
  ),
2107
- list.isLoading.value ? (0, import_vue14.h)("p", { style: styles.status }, "Loading\u2026") : null,
2108
- 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
2109
2286
  ]) : null,
2110
2287
  error.value && !(error.value.status === 404 && selectedId.value) ? (0, import_vue14.h)(
2111
2288
  "p",
2112
2289
  { style: styles.error, role: "alert" },
2113
- error.value.status === 404 ? "No user with that email." : error.value.explanation
2290
+ error.value.status === 404 ? t2("users.noUserWithEmail") : error.value.explanation
2114
2291
  ) : null,
2115
2292
  // Opened by id (e.g. from Subscriptions) and the account is gone —
2116
2293
  // deleted or purged — while its billing rows remain. Say so, and
2117
2294
  // still show the money.
2118
2295
  error.value?.status === 404 && selectedId.value ? [
2119
2296
  (0, import_vue14.h)("div", { style: styles.notice, role: "status" }, [
2120
- (0, import_vue14.h)("strong", "No account with id "),
2297
+ (0, import_vue14.h)("strong", `${t2("users.missingIdPrefix")} `),
2121
2298
  (0, import_vue14.h)("code", { style: styles.code }, selectedId.value),
2122
2299
  (0, import_vue14.h)("strong", "."),
2123
- " It was deleted, or never existed here. Its billing records remain."
2300
+ ` ${t2("users.missingIdBody")}`
2124
2301
  ]),
2125
- 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,
2126
2303
  props.billingClient ? (0, import_vue14.h)(SubscriberBilling, {
2127
2304
  key: `missing-${selectedId.value}`,
2128
2305
  client: props.billingClient,
2129
2306
  subscriberType: "user",
2130
- subscriberId: selectedId.value
2307
+ subscriberId: selectedId.value,
2308
+ ...loc ? { locale: loc } : {}
2131
2309
  }) : null
2132
2310
  ] : null,
2133
2311
  u ? [
2134
2312
  (0, import_vue14.h)("h2", { style: styles.subtitle }, [
2135
2313
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
2136
2314
  " ",
2137
- u.suspended ? pill("warn", "suspended") : null,
2138
- 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
2139
2317
  ]),
2140
2318
  (0, import_vue14.h)("table", { style: styles.table }, [
2141
2319
  (0, import_vue14.h)("tbody", [
2142
- row("id", (0, import_vue14.h)("span", { style: styles.mono }, u.id)),
2143
- row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
2144
- 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)),
2145
2327
  row(
2146
- "provider",
2147
- `${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")}`}`
2148
2330
  ),
2149
2331
  row(
2150
- "last login",
2151
- 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"))
2152
2334
  ),
2153
- row("created", new Date(u.createdAt).toLocaleString())
2335
+ row(t2("users.field.created"), (0, import_client12.formatAdminDate)(u.createdAt, loc))
2154
2336
  ])
2155
2337
  ]),
2156
2338
  u.deletedAt ? (0, import_vue14.h)("div", { style: { ...styles.notice, marginTop: "12px" }, role: "status" }, [
2157
- (0, import_vue14.h)("strong", `Deleted on ${new Date(u.deletedAt).toLocaleString()}.`),
2158
- " 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")}`
2159
2341
  ]) : (0, import_vue14.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
2160
- u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
2161
- "Suspend",
2342
+ u.suspended ? actionButton(t2("users.unsuspend"), isLoading.value, () => void unsuspend()) : actionButton(
2343
+ t2("users.suspend"),
2162
2344
  isLoading.value,
2163
2345
  () => void suspend(),
2164
2346
  styles.buttonDanger
2165
2347
  ),
2166
2348
  actionButton(
2167
- "Sign out everywhere",
2349
+ t2("users.signOutEverywhere"),
2168
2350
  isLoading.value,
2169
2351
  () => void revokeSessions().then(() => sessions.refresh())
2170
2352
  )
2171
2353
  ]),
2172
- 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,
2173
2355
  props.billingClient ? (0, import_vue14.h)(SubscriberBilling, {
2174
2356
  key: u.id,
2175
2357
  client: props.billingClient,
2176
2358
  subscriberType: "user",
2177
- subscriberId: u.id
2359
+ subscriberId: u.id,
2360
+ ...loc ? { locale: loc } : {}
2178
2361
  }) : null,
2179
2362
  u.deletedAt ? null : [
2180
- (0, import_vue14.h)("h2", { style: styles.subtitle }, "Live sessions"),
2181
- 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)(
2182
2365
  "ul",
2183
2366
  { style: styles.list },
2184
2367
  sessions.sessions.value.map(
@@ -2188,7 +2371,7 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2188
2371
  (0, import_vue14.h)(
2189
2372
  "span",
2190
2373
  { style: styles.muted },
2191
- (0, import_client.describeLocation)(s.location) ?? ""
2374
+ (0, import_client12.describeLocation)(s.location) ?? ""
2192
2375
  ),
2193
2376
  " ",
2194
2377
  (0, import_vue14.h)("span", { style: styles.muted }, s.userAgent ?? ""),
@@ -2196,18 +2379,21 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2196
2379
  (0, import_vue14.h)(
2197
2380
  "span",
2198
2381
  { style: styles.muted },
2199
- `since ${new Date(s.createdAt).toLocaleString()}`
2382
+ t2("users.since", { date: (0, import_client12.formatAdminDate)(s.createdAt, loc) })
2200
2383
  )
2201
2384
  ])
2202
2385
  )
2203
2386
  ),
2204
- (0, import_vue14.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
2205
- 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)(
2206
2389
  "ul",
2207
2390
  { style: styles.list },
2208
2391
  history.events.value.map(
2209
2392
  (e) => (0, import_vue14.h)("li", { key: e.id, style: styles.row }, [
2210
- 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
+ ),
2211
2397
  " ",
2212
2398
  (0, import_vue14.h)("span", { style: styles.muted }, e.method),
2213
2399
  " ",
@@ -2216,19 +2402,21 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2216
2402
  (0, import_vue14.h)(
2217
2403
  "span",
2218
2404
  { style: styles.muted },
2219
- (0, import_client.describeLocation)(e.location) ?? ""
2405
+ (0, import_client12.describeLocation)(e.location) ?? ""
2220
2406
  ),
2221
- 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
+ ] : "",
2222
2414
  " ",
2223
- (0, import_vue14.h)(
2224
- "span",
2225
- { style: styles.muted },
2226
- new Date(e.createdAt).toLocaleString()
2227
- )
2415
+ (0, import_vue14.h)("span", { style: styles.muted }, (0, import_client12.formatAdminDate)(e.createdAt, loc))
2228
2416
  ])
2229
2417
  )
2230
2418
  ),
2231
- 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
2232
2420
  ]
2233
2421
  ] : null
2234
2422
  ]);
@@ -2237,26 +2425,34 @@ var UsersScreen = (0, import_vue14.defineComponent)({
2237
2425
  });
2238
2426
 
2239
2427
  // src/screens/CatalogScreen.ts
2428
+ var import_client13 = require("@fonderie/client");
2240
2429
  var import_vue_admin12 = require("@fonderie/vue-admin");
2241
2430
  var import_vue15 = require("vue");
2242
2431
  var CatalogScreen = (0, import_vue15.defineComponent)({
2243
2432
  name: "FonderieCatalogScreen",
2244
- props: { client: { type: Object, required: true } },
2433
+ props: {
2434
+ client: { type: Object, required: true },
2435
+ locale: { type: String, default: void 0 }
2436
+ },
2245
2437
  setup(props) {
2246
2438
  const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin12.useAdminCatalog)(props.client);
2247
2439
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
2248
2440
  return () => {
2441
+ const t2 = (0, import_client13.createAdminT)(props.locale);
2249
2442
  const c = catalog.value;
2250
2443
  return (0, import_vue15.h)("div", { style: styles.container }, [
2251
- pageHeader(
2252
- "Catalog",
2253
- "What you sell: plans as configured in code, and as stored in the database, side by side.",
2254
- [refreshButton2("Refresh", isLoading.value, () => void refresh())]
2255
- ),
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
+ ]),
2256
2452
  error.value ? (0, import_vue15.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2257
- 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,
2258
2454
  c ? [
2259
- (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")),
2260
2456
  (0, import_vue15.h)(
2261
2457
  "pre",
2262
2458
  {
@@ -2271,13 +2467,17 @@ var CatalogScreen = (0, import_vue15.defineComponent)({
2271
2467
  },
2272
2468
  JSON.stringify(c.configured, null, 2)
2273
2469
  ),
2274
- (0, import_vue15.h)("h2", { style: styles.subtitle }, "Stored (database)"),
2275
- c.stored.length === 0 ? empty(
2276
- "No stored plans",
2277
- "Plans are configured in code; nothing has been written to the database.",
2278
- "catalog"
2279
- ) : table(
2280
- ["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
+ ],
2281
2481
  c.stored.map(
2282
2482
  (p) => (0, import_vue15.h)("tr", { key: p.id }, [
2283
2483
  td([
@@ -2288,7 +2488,7 @@ var CatalogScreen = (0, import_vue15.defineComponent)({
2288
2488
  td(p.seats == null ? "\u221E" : String(p.seats)),
2289
2489
  td(`${money(p.pricing?.monthly)} ${p.pricing?.currency ?? ""}`),
2290
2490
  td(money(p.pricing?.yearly)),
2291
- td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
2491
+ td(p.trialDays ? t2("catalog.trialDays", { n: p.trialDays }) : "\u2014"),
2292
2492
  td(
2293
2493
  (0, import_vue15.h)(
2294
2494
  "button",
@@ -2296,12 +2496,12 @@ var CatalogScreen = (0, import_vue15.defineComponent)({
2296
2496
  type: "button",
2297
2497
  style: styles.buttonDanger,
2298
2498
  onClick: () => {
2299
- if (window.confirm(`Delete stored plan "${p.name}"?`))
2499
+ if (window.confirm(t2("catalog.deleteConfirm", { name: p.name })))
2300
2500
  void deletePlan(p.id).catch(() => {
2301
2501
  });
2302
2502
  }
2303
2503
  },
2304
- "Delete"
2504
+ t2("common.delete")
2305
2505
  )
2306
2506
  )
2307
2507
  ])
@@ -2314,14 +2514,15 @@ var CatalogScreen = (0, import_vue15.defineComponent)({
2314
2514
  });
2315
2515
 
2316
2516
  // src/screens/SubscriberScreen.ts
2517
+ var import_client14 = require("@fonderie/client");
2317
2518
  var import_vue_admin13 = require("@fonderie/vue-admin");
2318
2519
  var import_vue16 = require("vue");
2319
2520
  var FILTERS = [
2320
- { label: "All", match: () => true },
2321
- { label: "Active", match: (s) => s === "active" },
2322
- { label: "Trialing", match: (s) => s === "trialing" },
2323
- { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2324
- { 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" }
2325
2526
  ];
2326
2527
  var SubscriberScreen = (0, import_vue16.defineComponent)({
2327
2528
  name: "FonderieSubscriberScreen",
@@ -2332,11 +2533,12 @@ var SubscriberScreen = (0, import_vue16.defineComponent)({
2332
2533
  * A user subscriber opens on the Users page, where their plan and credits
2333
2534
  * live beside their account. Omit to open everything here.
2334
2535
  */
2335
- onOpenUser: { type: Function, default: void 0 }
2536
+ onOpenUser: { type: Function, default: void 0 },
2537
+ locale: { type: String, default: void 0 }
2336
2538
  },
2337
2539
  setup(props) {
2338
2540
  const list = (0, import_vue_admin13.useAdminSubscribers)(props.client, { limit: props.pageSize });
2339
- const filter = (0, import_vue16.ref)("All");
2541
+ const filter = (0, import_vue16.ref)("all");
2340
2542
  const open = (0, import_vue16.ref)(null);
2341
2543
  const type = (0, import_vue16.ref)("workspace");
2342
2544
  const idInput = (0, import_vue16.ref)("");
@@ -2345,32 +2547,38 @@ var SubscriberScreen = (0, import_vue16.defineComponent)({
2345
2547
  else open.value = { type: t2, id };
2346
2548
  };
2347
2549
  return () => {
2550
+ const t2 = (0, import_client14.createAdminT)(props.locale);
2551
+ const loc = props.locale ? { locale: props.locale } : {};
2348
2552
  const o = open.value;
2349
2553
  if (o) {
2350
2554
  return (0, import_vue16.h)("div", { style: styles.container }, [
2351
- pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
2555
+ pageHeader(`${o.type}/${o.id}`, t2("billing.subscriptions.detailLead"), [
2352
2556
  (0, import_vue16.h)(
2353
2557
  "button",
2354
2558
  { type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
2355
- [icon("back", 14), "All subscriptions"]
2559
+ [icon("back", 14), t2("billing.subscriptions.back")]
2356
2560
  )
2357
2561
  ]),
2358
2562
  (0, import_vue16.h)(SubscriberBilling, {
2359
2563
  key: `${o.type}/${o.id}`,
2360
2564
  client: props.client,
2361
2565
  subscriberType: o.type,
2362
- subscriberId: o.id
2566
+ subscriberId: o.id,
2567
+ ...loc
2363
2568
  })
2364
2569
  ]);
2365
2570
  }
2366
- const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
2571
+ const match = FILTERS.find((f) => f.id === filter.value)?.match ?? (() => true);
2367
2572
  const rows = list.subscriptions.value.filter((s) => match(s.status));
2368
2573
  return (0, import_vue16.h)("div", { style: styles.container }, [
2369
- pageHeader(
2370
- "Subscriptions",
2371
- "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.",
2372
- [refreshButton(() => void list.refresh(), list.isLoading.value)]
2373
- ),
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
+ ]),
2374
2582
  (0, import_vue16.h)("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
2375
2583
  (0, import_vue16.h)(
2376
2584
  "fieldset",
@@ -2384,21 +2592,24 @@ var SubscriberScreen = (0, import_vue16.defineComponent)({
2384
2592
  padding: 0,
2385
2593
  minWidth: 0
2386
2594
  },
2387
- "aria-label": "Filter by status"
2595
+ "aria-label": t2("billing.subscriptions.filterLabel")
2388
2596
  },
2389
2597
  FILTERS.map((f) => {
2390
2598
  const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
2391
- const on = filter.value === f.label;
2599
+ const on = filter.value === f.id;
2392
2600
  return (0, import_vue16.h)(
2393
2601
  "button",
2394
2602
  {
2395
- key: f.label,
2603
+ key: f.id,
2396
2604
  type: "button",
2397
2605
  "aria-pressed": on,
2398
- onClick: () => filter.value = f.label,
2606
+ onClick: () => filter.value = f.id,
2399
2607
  style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
2400
2608
  },
2401
- [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
+ ]
2402
2613
  );
2403
2614
  })
2404
2615
  ),
@@ -2418,37 +2629,42 @@ var SubscriberScreen = (0, import_vue16.defineComponent)({
2418
2629
  value: type.value,
2419
2630
  onChange: (e) => type.value = e.target.value,
2420
2631
  style: styles.input,
2421
- "aria-label": "Type"
2632
+ "aria-label": t2("billing.subscriptions.typeLabel")
2422
2633
  },
2423
2634
  [
2424
- (0, import_vue16.h)("option", { value: "workspace" }, "workspace"),
2425
- (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"))
2426
2637
  ]
2427
2638
  ),
2428
2639
  (0, import_vue16.h)("input", {
2429
2640
  value: idInput.value,
2430
2641
  onInput: (e) => idInput.value = e.target.value,
2431
- placeholder: "subscriber id",
2642
+ placeholder: t2("billing.subscriptions.idPlaceholder"),
2432
2643
  style: { ...styles.input, width: "220px" },
2433
- "aria-label": "Subscriber id"
2644
+ "aria-label": t2("billing.subscriptions.idLabel")
2434
2645
  }),
2435
2646
  (0, import_vue16.h)(
2436
2647
  "button",
2437
2648
  { type: "submit", style: styles.button, disabled: !idInput.value.trim() },
2438
- [icon("search", 14), "Open"]
2649
+ [icon("search", 14), t2("common.open")]
2439
2650
  )
2440
2651
  ]
2441
2652
  )
2442
2653
  ]),
2443
2654
  list.error.value ? (0, import_vue16.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2444
2655
  rows.length === 0 && !list.isLoading.value ? empty(
2445
- filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
2446
- "Subscriptions appear here once someone checks out.",
2656
+ t2(`billing.subscriptions.empty.${filter.value}`),
2657
+ t2("billing.subscriptions.emptyHint"),
2447
2658
  "subscriber"
2448
2659
  ) : table(
2449
- ["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
+ ],
2450
2666
  rows.map((s) => {
2451
- const end = periodEnd(s);
2667
+ const end = periodEnd(s, props.locale);
2452
2668
  return (0, import_vue16.h)("tr", { key: s.id }, [
2453
2669
  td(
2454
2670
  (0, import_vue16.h)(
@@ -2464,11 +2680,11 @@ var SubscriberScreen = (0, import_vue16.defineComponent)({
2464
2680
  td([
2465
2681
  (0, import_vue16.h)("strong", s.plan),
2466
2682
  " ",
2467
- (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)}`)
2468
2684
  ]),
2469
2685
  td([
2470
- pill(statusTone(s.status), s.status),
2471
- 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
2472
2688
  ]),
2473
2689
  td(
2474
2690
  end ? [(0, import_vue16.h)("span", { style: styles.muted }, end.label), ` ${end.date}`] : (0, import_vue16.h)("span", { style: styles.muted }, "\u2014"),
@@ -2477,21 +2693,23 @@ var SubscriberScreen = (0, import_vue16.defineComponent)({
2477
2693
  ]);
2478
2694
  })
2479
2695
  ),
2480
- list.isLoading.value ? (0, import_vue16.h)("p", { style: styles.status }, "Loading\u2026") : null,
2481
- 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
2482
2698
  ]);
2483
2699
  };
2484
2700
  }
2485
2701
  });
2486
2702
 
2487
2703
  // src/screens/AuditScreen.ts
2704
+ var import_client15 = require("@fonderie/client");
2488
2705
  var import_vue_admin14 = require("@fonderie/vue-admin");
2489
2706
  var import_vue17 = require("vue");
2490
2707
  var AuditScreen = (0, import_vue17.defineComponent)({
2491
2708
  name: "FonderieAuditScreen",
2492
2709
  props: {
2493
2710
  client: { type: Object, required: true },
2494
- pageSize: { type: Number, default: 50 }
2711
+ pageSize: { type: Number, default: 50 },
2712
+ locale: { type: String, default: void 0 }
2495
2713
  },
2496
2714
  setup(props) {
2497
2715
  const draft = (0, import_vue17.ref)({ workspaceId: "", type: "", actorId: "", from: "", to: "" });
@@ -2507,100 +2725,120 @@ var AuditScreen = (0, import_vue17.defineComponent)({
2507
2725
  style: styles.input,
2508
2726
  "aria-label": placeholder
2509
2727
  });
2510
- return () => (0, import_vue17.h)("div", { style: styles.container }, [
2511
- pageHeader(
2512
- "Audit",
2513
- "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2514
- ),
2515
- (0, import_vue17.h)(
2516
- "form",
2517
- {
2518
- style: styles.toolbar,
2519
- onSubmit: (e) => {
2520
- e.preventDefault();
2521
- const d = draft.value;
2522
- query.value = {
2523
- limit: props.pageSize,
2524
- ...d.workspaceId.trim() ? { workspaceId: d.workspaceId.trim() } : {},
2525
- ...d.type.trim() ? { type: d.type.trim() } : {},
2526
- ...d.actorId.trim() ? { actorId: d.actorId.trim() } : {},
2527
- // Dates are whole days in the operator's time zone: "to" includes that day.
2528
- ...d.from ? { from: /* @__PURE__ */ new Date(`${d.from}T00:00:00`) } : {},
2529
- ...d.to ? { to: /* @__PURE__ */ new Date(`${d.to}T23:59:59.999`) } : {}
2530
- };
2531
- }
2532
- },
2533
- [
2534
- field("workspaceId", "workspace id (all if empty)"),
2535
- field("type", "event type"),
2536
- field("actorId", "actor id"),
2537
- ...["from", "to"].map(
2538
- (k) => (0, import_vue17.h)("input", {
2539
- type: "date",
2540
- value: draft.value[k],
2541
- onInput: (e) => draft.value = { ...draft.value, [k]: e.target.value },
2542
- style: styles.input,
2543
- "aria-label": k === "from" ? "From date" : "To date",
2544
- title: k === "from" ? "From (inclusive)" : "To (inclusive)"
2545
- })
2546
- ),
2547
- (0, import_vue17.h)(
2548
- "button",
2549
- { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2550
- [icon("search", 14), "Filter"]
2551
- ),
2552
- refreshButton2("Refresh", isLoading.value, () => void refresh())
2553
- ]
2554
- ),
2555
- error.value ? (0, import_vue17.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2556
- events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
2557
- ["When", "Type", "Workspace", "Actor", "Request"],
2558
- events.value.map(
2559
- (e) => (0, import_vue17.h)("tr", { key: e.id }, [
2560
- td(new Date(e.createdAt).toLocaleString(), styles.muted),
2561
- td(e.type, styles.mono),
2562
- td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
2563
- td(e.actorId ?? "\u2014", styles.mono),
2564
- td(e.requestId ?? "", styles.muted)
2565
- ])
2566
- )
2567
- ),
2568
- isLoading.value ? (0, import_vue17.h)("p", { style: styles.status }, "Loading\u2026") : null,
2569
- hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
2570
- ]);
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
+ };
2571
2800
  }
2572
2801
  });
2573
2802
 
2574
2803
  // src/screens/AdminShell.ts
2575
2804
  var NAV = [
2576
- { group: "Today", items: [{ page: "attention", label: "Attention" }] },
2805
+ { group: "nav.groups.today", items: [{ page: "attention", label: "nav.items.attention" }] },
2577
2806
  {
2578
- group: "System",
2807
+ group: "nav.groups.system",
2579
2808
  items: [
2580
- { page: "modules", label: "Modules" },
2581
- { page: "environment", label: "Environment" },
2582
- { page: "doctor", label: "Doctor" },
2583
- { 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" }
2584
2813
  ]
2585
2814
  },
2586
- { group: "People", items: [{ page: "users", label: "Users", needs: "auth" }] },
2587
2815
  {
2588
- group: "Money",
2816
+ group: "nav.groups.people",
2817
+ items: [{ page: "users", label: "nav.items.users", needs: "auth" }]
2818
+ },
2819
+ {
2820
+ group: "nav.groups.money",
2589
2821
  items: [
2590
- { page: "catalog", label: "Catalog", needs: "billing" },
2591
- { page: "subscriber", label: "Subscriptions", needs: "billing" }
2822
+ { page: "catalog", label: "nav.items.catalog", needs: "billing" },
2823
+ { page: "subscriber", label: "nav.items.subscriptions", needs: "billing" }
2592
2824
  ]
2593
2825
  },
2594
- { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
2595
- { group: "Messaging", items: [{ page: "templates", label: "Templates", needs: "courier" }] },
2596
2826
  {
2597
- 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",
2598
2836
  items: [
2599
- { page: "audit", label: "Audit", needs: "audit" },
2600
- { page: "log", label: "Admin log" },
2601
- { page: "operators", label: "Operators", needs: "operators" },
2602
- { page: "tokens", label: "Tokens" },
2603
- { 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" }
2604
2842
  ]
2605
2843
  }
2606
2844
  ];
@@ -2652,12 +2890,16 @@ var AdminShell = (0, import_vue18.defineComponent)({
2652
2890
  currentOperator: { type: String, default: void 0 },
2653
2891
  // Where the app serves GET /config/public — marks public keys on the
2654
2892
  // Config page and previews exactly what frontends receive.
2655
- publicConfigUrl: { type: String, default: void 0 }
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 }
2656
2897
  },
2657
2898
  // `footer` slot: pinned to the bottom of the sidebar — session controls
2658
2899
  // (theme, sign out). The React shell takes the same thing as a `footer` prop.
2659
2900
  emits: { navigate: (_page) => true },
2660
2901
  setup(props, { emit, slots }) {
2902
+ const tr = (0, import_vue18.computed)(() => (0, import_client16.createAdminT)(props.locale));
2661
2903
  const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2662
2904
  const narrow = (0, import_vue18.ref)(query?.matches ?? false);
2663
2905
  const drawer = (0, import_vue18.ref)(false);
@@ -2684,60 +2926,67 @@ var AdminShell = (0, import_vue18.defineComponent)({
2684
2926
  const missing = (text) => (0, import_vue18.h)("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2685
2927
  const body = () => {
2686
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 }));
2687
2932
  switch (current.value) {
2688
2933
  case "attention":
2689
- return (0, import_vue18.h)(AttentionScreen, { client: c });
2934
+ return (0, import_vue18.h)(AttentionScreen, { client: c, ...loc });
2690
2935
  case "modules":
2691
- return (0, import_vue18.h)(ModulesScreen, { client: c });
2936
+ return (0, import_vue18.h)(ModulesScreen, { client: c, ...loc });
2692
2937
  case "doctor":
2693
- return (0, import_vue18.h)(DoctorScreen, { client: c });
2938
+ return (0, import_vue18.h)(DoctorScreen, { client: c, ...loc });
2694
2939
  case "environment":
2695
- return (0, import_vue18.h)(EnvironmentScreen, { client: c });
2940
+ return (0, import_vue18.h)(EnvironmentScreen, { client: c, ...loc });
2696
2941
  case "routes":
2697
- return (0, import_vue18.h)(RoutesScreen, { client: c });
2942
+ return (0, import_vue18.h)(RoutesScreen, { client: c, ...loc });
2698
2943
  case "tokens":
2699
- return (0, import_vue18.h)(TokensScreen, { client: c });
2944
+ return (0, import_vue18.h)(TokensScreen, { client: c, ...loc });
2700
2945
  case "operators":
2701
2946
  return (0, import_vue18.h)(OperatorsScreen, {
2702
2947
  client: c,
2948
+ ...loc,
2703
2949
  ...props.currentOperator ? { me: props.currentOperator } : {}
2704
2950
  });
2705
2951
  case "migrations":
2706
- return (0, import_vue18.h)(MigrationsScreen, { client: c });
2952
+ return (0, import_vue18.h)(MigrationsScreen, { client: c, ...loc });
2707
2953
  case "log":
2708
- return (0, import_vue18.h)(AdminLogScreen, { client: c });
2954
+ return (0, import_vue18.h)(AdminLogScreen, { client: c, ...loc });
2709
2955
  case "catalog":
2710
- 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");
2711
2957
  case "subscriber":
2712
2958
  return props.billingClient ? (0, import_vue18.h)(SubscriberScreen, {
2713
2959
  client: props.billingClient,
2960
+ ...loc,
2714
2961
  ...props.authClient ? {
2715
2962
  onOpenUser: (id) => {
2716
2963
  openUser.value = id;
2717
2964
  go("users");
2718
2965
  }
2719
2966
  } : {}
2720
- }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2967
+ }) : missingClient("BillingAdminClient");
2721
2968
  case "audit":
2722
- 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");
2723
2970
  case "users":
2724
2971
  return props.authClient ? (0, import_vue18.h)(UsersScreen, {
2725
2972
  key: openUser.value ?? "list",
2726
2973
  client: props.authClient,
2974
+ ...loc,
2727
2975
  ...props.billingClient ? { billingClient: props.billingClient } : {},
2728
2976
  ...openUser.value ? { openUserId: openUser.value } : {}
2729
- }) : missing("Pass an AuthAdminClient to look up users here.");
2977
+ }) : missingClient("AuthAdminClient");
2730
2978
  case "settings": {
2731
2979
  const cc = props.configClient;
2732
- if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
2980
+ if (!cc) return missingClient("ConfigAdminClient");
2733
2981
  const e = editing.value;
2734
2982
  if (e && (e.kind === "config" || e.kind === "secret")) {
2735
2983
  const env = e.environment ?? props.environment;
2736
2984
  return [
2737
- back("Config & secrets"),
2985
+ back(t2("nav.items.settings")),
2738
2986
  (0, import_vue18.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
2739
2987
  key: `${e.kind}:${e.key}:${e.environment ?? ""}`,
2740
2988
  client: cc,
2989
+ ...loc,
2741
2990
  kind: e.kind,
2742
2991
  configKey: e.key,
2743
2992
  ...env !== void 0 ? { environment: env } : {},
@@ -2755,6 +3004,7 @@ var AdminShell = (0, import_vue18.defineComponent)({
2755
3004
  };
2756
3005
  return (0, import_vue18.h)(import_vue_config_admin_screens.ConfigListScreen, {
2757
3006
  client: cc,
3007
+ ...loc,
2758
3008
  ...props.environment !== void 0 ? { environment: props.environment } : {},
2759
3009
  ...props.publicConfigUrl ? { publicConfigUrl: props.publicConfigUrl } : {},
2760
3010
  "onSelect-config": (key, environment) => editing.value = { kind: "config", key, environment },
@@ -2766,13 +3016,14 @@ var AdminShell = (0, import_vue18.defineComponent)({
2766
3016
  }
2767
3017
  case "templates": {
2768
3018
  const kc = props.courierClient;
2769
- if (!kc) return missing("Pass a CourierAdminClient to manage templates here.");
3019
+ if (!kc) return missingClient("CourierAdminClient");
2770
3020
  const e = editing.value;
2771
3021
  if (e?.kind === "template-new")
2772
3022
  return [
2773
- back("Templates"),
3023
+ back(t2("nav.items.templates")),
2774
3024
  (0, import_vue18.h)(import_vue_courier_admin_screens.TemplateCreateScreen, {
2775
3025
  client: kc,
3026
+ ...loc,
2776
3027
  ...e.type ? { type: e.type } : {},
2777
3028
  ...e.locales ? { locales: e.locales } : {},
2778
3029
  onCreated: (c2) => editing.value = {
@@ -2785,10 +3036,11 @@ var AdminShell = (0, import_vue18.defineComponent)({
2785
3036
  ];
2786
3037
  if (e?.kind === "template")
2787
3038
  return [
2788
- back("Templates"),
3039
+ back(t2("nav.items.templates")),
2789
3040
  (0, import_vue18.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
2790
3041
  key: `${e.type}:${e.locale ?? ""}`,
2791
3042
  client: kc,
3043
+ ...props.locale ? { uiLocale: props.locale } : {},
2792
3044
  type: e.type,
2793
3045
  locale: e.locale,
2794
3046
  system: e.system,
@@ -2800,12 +3052,13 @@ var AdminShell = (0, import_vue18.defineComponent)({
2800
3052
  ];
2801
3053
  return (0, import_vue18.h)(import_vue_courier_admin_screens.TemplateListScreen, {
2802
3054
  client: kc,
3055
+ ...loc,
2803
3056
  allowCreate: true,
2804
- "onSelect-template": (t2) => editing.value = {
3057
+ "onSelect-template": (t3) => editing.value = {
2805
3058
  kind: "template",
2806
- type: t2.type,
2807
- locale: t2.locale ?? null,
2808
- system: t2.system === true
3059
+ type: t3.type,
3060
+ locale: t3.locale ?? null,
3061
+ system: t3.system === true
2809
3062
  },
2810
3063
  "onCreate-template": (c2) => editing.value = { kind: "template-new", locales: c2.locales }
2811
3064
  });
@@ -2841,7 +3094,7 @@ var AdminShell = (0, import_vue18.defineComponent)({
2841
3094
  {
2842
3095
  type: "button",
2843
3096
  style: styles.buttonGhost,
2844
- "aria-label": "Close menu",
3097
+ "aria-label": tr.value("shell.closeMenu"),
2845
3098
  onClick: () => drawer.value = false
2846
3099
  },
2847
3100
  [icon("close")]
@@ -2855,7 +3108,7 @@ var AdminShell = (0, import_vue18.defineComponent)({
2855
3108
  ...styles.nav,
2856
3109
  ...narrow.value ? { width: "280px", maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2857
3110
  },
2858
- "aria-label": "Admin"
3111
+ "aria-label": tr.value("shell.navLabel")
2859
3112
  },
2860
3113
  [
2861
3114
  brand(),
@@ -2866,7 +3119,7 @@ var AdminShell = (0, import_vue18.defineComponent)({
2866
3119
  const items = g.items.filter(visible);
2867
3120
  if (items.length === 0) return null;
2868
3121
  return (0, import_vue18.h)("div", { key: g.group }, [
2869
- (0, import_vue18.h)("div", { style: styles.navGroup }, g.group),
3122
+ (0, import_vue18.h)("div", { style: styles.navGroup }, tr.value(g.group)),
2870
3123
  ...items.map((i) => {
2871
3124
  const active = current.value === i.page;
2872
3125
  return (0, import_vue18.h)(
@@ -2882,7 +3135,10 @@ var AdminShell = (0, import_vue18.defineComponent)({
2882
3135
  go(i.page);
2883
3136
  }
2884
3137
  },
2885
- [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
+ ]
2886
3142
  );
2887
3143
  })
2888
3144
  ]);
@@ -2892,7 +3148,8 @@ var AdminShell = (0, import_vue18.defineComponent)({
2892
3148
  ]
2893
3149
  );
2894
3150
  return () => {
2895
- 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) : "";
2896
3153
  return (0, import_vue18.h)(
2897
3154
  "div",
2898
3155
  { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
@@ -2904,7 +3161,7 @@ var AdminShell = (0, import_vue18.defineComponent)({
2904
3161
  {
2905
3162
  type: "button",
2906
3163
  style: styles.buttonGhost,
2907
- "aria-label": "Open menu",
3164
+ "aria-label": tr.value("shell.openMenu"),
2908
3165
  "aria-expanded": drawer.value,
2909
3166
  onClick: () => drawer.value = true
2910
3167
  },