@twentyfirstmedia/ui-core 0.2.1 → 0.3.1

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.
Files changed (147) hide show
  1. package/dist/components/admin.d.ts.map +1 -1
  2. package/dist/components/admin.js +95 -71
  3. package/dist/components/admin.js.map +1 -1
  4. package/dist/components/agents.d.ts.map +1 -1
  5. package/dist/components/agents.js +49 -35
  6. package/dist/components/agents.js.map +1 -1
  7. package/dist/components/app-chrome.d.ts.map +1 -1
  8. package/dist/components/app-chrome.js +6 -2
  9. package/dist/components/app-chrome.js.map +1 -1
  10. package/dist/components/approvals.d.ts.map +1 -1
  11. package/dist/components/approvals.js +22 -18
  12. package/dist/components/approvals.js.map +1 -1
  13. package/dist/components/building-blocks.d.ts.map +1 -1
  14. package/dist/components/building-blocks.js +6 -2
  15. package/dist/components/building-blocks.js.map +1 -1
  16. package/dist/components/charts.d.ts.map +1 -1
  17. package/dist/components/charts.js +20 -10
  18. package/dist/components/charts.js.map +1 -1
  19. package/dist/components/chat.d.ts.map +1 -1
  20. package/dist/components/chat.js +31 -19
  21. package/dist/components/chat.js.map +1 -1
  22. package/dist/components/commerce.d.ts +2 -2
  23. package/dist/components/commerce.d.ts.map +1 -1
  24. package/dist/components/commerce.js +45 -31
  25. package/dist/components/commerce.js.map +1 -1
  26. package/dist/components/data-display.d.ts +5 -9
  27. package/dist/components/data-display.d.ts.map +1 -1
  28. package/dist/components/data-display.js +34 -35
  29. package/dist/components/data-display.js.map +1 -1
  30. package/dist/components/data-table.d.ts.map +1 -1
  31. package/dist/components/data-table.js +15 -10
  32. package/dist/components/data-table.js.map +1 -1
  33. package/dist/components/developer.d.ts.map +1 -1
  34. package/dist/components/developer.js +20 -11
  35. package/dist/components/developer.js.map +1 -1
  36. package/dist/components/feedback.d.ts.map +1 -1
  37. package/dist/components/feedback.js +12 -6
  38. package/dist/components/feedback.js.map +1 -1
  39. package/dist/components/form.d.ts +2 -1
  40. package/dist/components/form.d.ts.map +1 -1
  41. package/dist/components/form.js +12 -9
  42. package/dist/components/form.js.map +1 -1
  43. package/dist/components/forms.d.ts +2 -1
  44. package/dist/components/forms.d.ts.map +1 -1
  45. package/dist/components/forms.js +92 -42
  46. package/dist/components/forms.js.map +1 -1
  47. package/dist/components/foundation.d.ts +5 -2
  48. package/dist/components/foundation.d.ts.map +1 -1
  49. package/dist/components/foundation.js +15 -12
  50. package/dist/components/foundation.js.map +1 -1
  51. package/dist/components/inputs.d.ts.map +1 -1
  52. package/dist/components/inputs.js +3 -1
  53. package/dist/components/inputs.js.map +1 -1
  54. package/dist/components/mail.d.ts.map +1 -1
  55. package/dist/components/mail.js +15 -10
  56. package/dist/components/mail.js.map +1 -1
  57. package/dist/components/media.d.ts.map +1 -1
  58. package/dist/components/media.js +39 -18
  59. package/dist/components/media.js.map +1 -1
  60. package/dist/components/navigation.d.ts.map +1 -1
  61. package/dist/components/navigation.js +21 -9
  62. package/dist/components/navigation.js.map +1 -1
  63. package/dist/components/operations.d.ts.map +1 -1
  64. package/dist/components/operations.js +37 -27
  65. package/dist/components/operations.js.map +1 -1
  66. package/dist/components/previews.d.ts.map +1 -1
  67. package/dist/components/previews.js +14 -10
  68. package/dist/components/previews.js.map +1 -1
  69. package/dist/components/primitives.d.ts.map +1 -1
  70. package/dist/components/primitives.js +4 -1
  71. package/dist/components/primitives.js.map +1 -1
  72. package/dist/components/real-estate.d.ts.map +1 -1
  73. package/dist/components/real-estate.js +43 -28
  74. package/dist/components/real-estate.js.map +1 -1
  75. package/dist/components/rich-text-editor.d.ts.map +1 -1
  76. package/dist/components/rich-text-editor.js +18 -15
  77. package/dist/components/rich-text-editor.js.map +1 -1
  78. package/dist/components/select-menu.d.ts.map +1 -1
  79. package/dist/components/select-menu.js +4 -1
  80. package/dist/components/select-menu.js.map +1 -1
  81. package/dist/components/shell.d.ts.map +1 -1
  82. package/dist/components/shell.js +8 -3
  83. package/dist/components/shell.js.map +1 -1
  84. package/dist/components/site.d.ts.map +1 -1
  85. package/dist/components/site.js +17 -13
  86. package/dist/components/site.js.map +1 -1
  87. package/dist/components/types.d.ts +102 -3
  88. package/dist/components/types.d.ts.map +1 -1
  89. package/dist/components/workspace.d.ts.map +1 -1
  90. package/dist/components/workspace.js +50 -34
  91. package/dist/components/workspace.js.map +1 -1
  92. package/dist/context.d.ts +39 -0
  93. package/dist/context.d.ts.map +1 -0
  94. package/dist/context.js +76 -0
  95. package/dist/context.js.map +1 -0
  96. package/dist/format.d.ts +33 -6
  97. package/dist/format.d.ts.map +1 -1
  98. package/dist/format.js +75 -10
  99. package/dist/format.js.map +1 -1
  100. package/dist/index.d.ts +3 -2
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +3 -2
  103. package/dist/index.js.map +1 -1
  104. package/dist/provider.d.ts +18 -11
  105. package/dist/provider.d.ts.map +1 -1
  106. package/dist/provider.js +11 -18
  107. package/dist/provider.js.map +1 -1
  108. package/dist/strings.d.ts +503 -0
  109. package/dist/strings.d.ts.map +1 -0
  110. package/dist/strings.js +537 -0
  111. package/dist/strings.js.map +1 -0
  112. package/dist/themes.css +4 -0
  113. package/package.json +1 -1
  114. package/src/components/admin.tsx +117 -93
  115. package/src/components/agents.tsx +55 -41
  116. package/src/components/app-chrome.tsx +6 -2
  117. package/src/components/approvals.tsx +26 -22
  118. package/src/components/building-blocks.tsx +6 -2
  119. package/src/components/charts.tsx +21 -11
  120. package/src/components/chat.tsx +45 -35
  121. package/src/components/commerce.tsx +61 -53
  122. package/src/components/data-display.tsx +38 -45
  123. package/src/components/data-table.tsx +20 -15
  124. package/src/components/developer.tsx +26 -15
  125. package/src/components/feedback.tsx +19 -7
  126. package/src/components/form.tsx +17 -9
  127. package/src/components/forms.tsx +131 -53
  128. package/src/components/foundation.tsx +23 -36
  129. package/src/components/inputs.tsx +4 -2
  130. package/src/components/mail.tsx +16 -11
  131. package/src/components/media.tsx +64 -21
  132. package/src/components/navigation.tsx +25 -13
  133. package/src/components/operations.tsx +40 -32
  134. package/src/components/previews.tsx +17 -15
  135. package/src/components/primitives.tsx +8 -5
  136. package/src/components/real-estate.tsx +55 -35
  137. package/src/components/rich-text-editor.tsx +20 -17
  138. package/src/components/select-menu.tsx +5 -2
  139. package/src/components/shell.tsx +8 -3
  140. package/src/components/site.tsx +18 -14
  141. package/src/components/types.ts +111 -3
  142. package/src/components/workspace.tsx +79 -60
  143. package/src/context.ts +112 -0
  144. package/src/format.ts +108 -15
  145. package/src/index.ts +43 -0
  146. package/src/provider.tsx +36 -40
  147. package/src/strings.ts +1500 -0
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { useControlled, useId } from "../hooks.js";
4
- import { formatMoney, formatNumber } from "../format.js";
4
+ import { useFormat, useStrings } from "../context.js";
5
+ import { ADMIN_STRINGS } from "../strings.js";
5
6
  import { cx } from "../cx.js";
6
7
  import { Icon } from "../Icon.js";
7
8
  import { toast } from "../toast-store.js";
@@ -46,6 +47,8 @@ import type {
46
47
 
47
48
  /* ── Shared: range slider, bar list, status select, tile picker ── */
48
49
  export function RangeSlider(p: RangeSliderProps) {
50
+ const t = useStrings(ADMIN_STRINGS);
51
+ const { formatNumber } = useFormat();
49
52
  const id = useId("rs"),
50
53
  min = p.min || 0,
51
54
  max = p.max != null ? p.max : 100,
@@ -80,7 +83,7 @@ export function RangeSlider(p: RangeSliderProps) {
80
83
  max={max}
81
84
  step={step}
82
85
  value={lo}
83
- aria-label="Minimum"
86
+ aria-label={t.minimum}
84
87
  onChange={(e) => {
85
88
  set(0, e.target.value);
86
89
  }}
@@ -91,7 +94,7 @@ export function RangeSlider(p: RangeSliderProps) {
91
94
  max={max}
92
95
  step={step}
93
96
  value={hi}
94
- aria-label="Maximum"
97
+ aria-label={t.maximum}
95
98
  onChange={(e) => {
96
99
  set(1, e.target.value);
97
100
  }}
@@ -100,7 +103,7 @@ export function RangeSlider(p: RangeSliderProps) {
100
103
  {p.inputs !== false ? (
101
104
  <div className="tf-range-inputs">
102
105
  <label className="tf-range-num">
103
- <small>{"Min"}</small>
106
+ <small>{t.min}</small>
104
107
  <input
105
108
  id={id}
106
109
  className="tf-input"
@@ -116,7 +119,7 @@ export function RangeSlider(p: RangeSliderProps) {
116
119
  –
117
120
  </span>
118
121
  <label className="tf-range-num">
119
- <small>{"Max"}</small>
122
+ <small>{t.max}</small>
120
123
  <input
121
124
  className="tf-input"
122
125
  inputMode="numeric"
@@ -140,6 +143,7 @@ export function RangeSlider(p: RangeSliderProps) {
140
143
  }
141
144
 
142
145
  export function BarList(p: BarListProps) {
146
+ const { formatNumber } = useFormat();
143
147
  const items = p.items || [],
144
148
  max = Math.max.apply(null, items.map((i) => i.value).concat([1])),
145
149
  f = p.format || formatNumber;
@@ -319,6 +323,8 @@ export function TilePicker(p: TilePickerProps) {
319
323
 
320
324
  /* ── Live feed ── */
321
325
  export function LiveFeed(p: LiveFeedProps) {
326
+ const t = useStrings(ADMIN_STRINGS);
327
+ const { formatMoney } = useFormat();
322
328
  const paused = useControlled(p.paused, false, p.onPause),
323
329
  held = React.useRef<LiveFeedItem[] | null>(null),
324
330
  filter = React.useState<string | null>(null);
@@ -334,7 +340,7 @@ export function LiveFeed(p: LiveFeedProps) {
334
340
  <div className="tf-livefeed-head">
335
341
  <span className={cx("tf-live", paused[0] && "off")}>
336
342
  <span className="tf-live-dot" aria-hidden />
337
- {paused[0] ? "Paused" : "Live"}
343
+ {paused[0] ? t.paused : t.live}
338
344
  </span>
339
345
  {types.length > 0 && (
340
346
  <div
@@ -349,7 +355,7 @@ export function LiveFeed(p: LiveFeedProps) {
349
355
  filter[1](null);
350
356
  }}
351
357
  >
352
- {"All"}
358
+ {t.all}
353
359
  </Chip>
354
360
  {types.map((t) => (
355
361
  <Chip
@@ -377,13 +383,13 @@ export function LiveFeed(p: LiveFeedProps) {
377
383
  paused[1](false);
378
384
  }}
379
385
  >
380
- {`${waiting} new`}
386
+ {t.newCount(waiting)}
381
387
  </button>
382
388
  )}
383
389
  <IconButton
384
390
  icon={paused[0] ? "play" : "pause"}
385
391
  size="sm"
386
- label={paused[0] ? "Resume" : "Pause"}
392
+ label={paused[0] ? t.resume : t.pause}
387
393
  onClick={() => {
388
394
  paused[1](!paused[0]);
389
395
  }}
@@ -408,7 +414,7 @@ export function LiveFeed(p: LiveFeedProps) {
408
414
  padding: 16,
409
415
  }}
410
416
  >
411
- {p.emptyText || "Waiting for events…"}
417
+ {p.emptyText || t.waitingForEvents}
412
418
  </p>
413
419
  ) : (
414
420
  shown.map((it) => (
@@ -463,6 +469,8 @@ export function WorldDots() {
463
469
 
464
470
  /* ── Map with pins (background is yours: tiles, an SVG, an image) ── */
465
471
  export function MapView(p: MapViewProps) {
472
+ const t = useStrings(ADMIN_STRINGS);
473
+ const { formatNumber } = useFormat();
466
474
  const b = p.bounds || {
467
475
  n: 84,
468
476
  s: -60,
@@ -487,7 +495,7 @@ export function MapView(p: MapViewProps) {
487
495
  height: Hh,
488
496
  }}
489
497
  role="region"
490
- aria-label={p.label || "Map"}
498
+ aria-label={p.label || t.viewMap}
491
499
  >
492
500
  {p.background || (!p.bounds ? <WorldDots /> : null) || (
493
501
  <svg className="tf-map-grid" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden>
@@ -569,6 +577,8 @@ export function MapView(p: MapViewProps) {
569
577
 
570
578
  /* ── Money and operations for admins ── */
571
579
  export function BalanceCard(p: BalanceCardProps) {
580
+ const t = useStrings(ADMIN_STRINGS);
581
+ const { formatMoney } = useFormat();
572
582
  const days = p.perDay && p.perDay > 0 ? Math.floor(p.balance / p.perDay) : null,
573
583
  low = p.low != null && p.balance <= p.low,
574
584
  st = p.status || (p.balance <= 0 ? "danger" : low ? "warn" : "ok");
@@ -582,7 +592,9 @@ export function BalanceCard(p: BalanceCardProps) {
582
592
  </div>
583
593
  <StatusIndicator
584
594
  status={st}
585
- label={p.statusLabel || (st === "danger" ? "Empty" : low ? "Low" : "OK")}
595
+ label={
596
+ p.statusLabel || (st === "danger" ? t.balanceEmpty : low ? t.balanceLow : t.balanceOk)
597
+ }
586
598
  size="sm"
587
599
  />
588
600
  </div>
@@ -600,16 +612,14 @@ export function BalanceCard(p: BalanceCardProps) {
600
612
  </span>
601
613
  <span className="tf-balance-meta">
602
614
  {[
603
- p.perDay != null && `${formatMoney(p.perDay, p.currency)} per day`,
604
- days != null && p.balance > 0 && `lasts about ${days} days`,
615
+ p.perDay != null && t.perDay(formatMoney(p.perDay, p.currency)),
616
+ days != null && p.balance > 0 && t.lastsAbout(days),
605
617
  ]
606
618
  .filter(Boolean)
607
619
  .join(" · ")}
608
620
  </span>
609
621
  {p.low != null && (
610
- <span className="tf-balance-meta">
611
- {`Warns below ${formatMoney(p.low, p.currency)}`}
612
- </span>
622
+ <span className="tf-balance-meta">{t.warnsBelow(formatMoney(p.low, p.currency))}</span>
613
623
  )}
614
624
  </div>
615
625
  {p.spark && (
@@ -631,7 +641,7 @@ export function BalanceCard(p: BalanceCardProps) {
631
641
  {p.action}
632
642
  {p.onTopUp && (
633
643
  <Button size="sm" variant={low ? "primary" : "soft"} icon="plus" onClick={p.onTopUp}>
634
- {p.topUpLabel || "Top up"}
644
+ {p.topUpLabel || t.topUp}
635
645
  </Button>
636
646
  )}
637
647
  </div>
@@ -641,6 +651,7 @@ export function BalanceCard(p: BalanceCardProps) {
641
651
  }
642
652
 
643
653
  export function ServiceMonitor(p: ServiceMonitorProps) {
654
+ const { formatNumber } = useFormat();
644
655
  return (
645
656
  <div className="tf-svcmon" role="list" aria-label={p.label}>
646
657
  {(p.items || []).map((s) => (
@@ -676,6 +687,7 @@ export function ServiceMonitor(p: ServiceMonitorProps) {
676
687
  }
677
688
 
678
689
  export function RunHistory(p: RunHistoryProps) {
690
+ const { formatNumber } = useFormat();
679
691
  const open = React.useState<string | number | null>(null);
680
692
  return (
681
693
  <div className="tf-runs" role="list" aria-label={p.label}>
@@ -718,6 +730,8 @@ export function RunHistory(p: RunHistoryProps) {
718
730
  }
719
731
 
720
732
  export function WebhookEndpoint(p: WebhookEndpointProps) {
733
+ const t = useStrings(ADMIN_STRINGS);
734
+ const { formatNumber } = useFormat();
721
735
  const open = React.useState(!!p.defaultOpen);
722
736
  return (
723
737
  <div className="tf-hook">
@@ -743,12 +757,12 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
743
757
  </span>
744
758
  )}
745
759
  {p.onToggle && (
746
- <Switch checked={p.enabled !== false} onChange={p.onToggle} label="Enabled" />
760
+ <Switch checked={p.enabled !== false} onChange={p.onToggle} label={t.enabled} />
747
761
  )}
748
762
  {p.menu && (
749
763
  <Popover
750
764
  width={200}
751
- trigger={(s) => <IconButton icon="more" size="sm" label="More" onClick={s.toggle} />}
765
+ trigger={(s) => <IconButton icon="more" size="sm" label={t.more} onClick={s.toggle} />}
752
766
  >
753
767
  {() => <Menu items={p.menu || []} />}
754
768
  </Popover>
@@ -757,7 +771,7 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
757
771
  {p.secret && p.secret}
758
772
  {(p.deliveries || []).length > 0 && (
759
773
  <Collapsible
760
- title="Recent deliveries"
774
+ title={t.recentDeliveries}
761
775
  meta={formatNumber((p.deliveries || []).length)}
762
776
  open={open[0]}
763
777
  onOpenChange={open[1]}
@@ -782,7 +796,7 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
782
796
  p.onRetry?.(d);
783
797
  }}
784
798
  >
785
- {"Retry"}
799
+ {t.retry}
786
800
  </Button>
787
801
  )}
788
802
  </div>
@@ -796,6 +810,7 @@ export function WebhookEndpoint(p: WebhookEndpointProps) {
796
810
  }
797
811
 
798
812
  export function BackupList(p: BackupListProps) {
813
+ const t = useStrings(ADMIN_STRINGS);
799
814
  return (
800
815
  <div className="tf-backups" role="list" aria-label={p.label}>
801
816
  {(p.items || []).map((b) => (
@@ -816,7 +831,7 @@ export function BackupList(p: BackupListProps) {
816
831
  <IconButton
817
832
  icon="download"
818
833
  size="sm"
819
- label="Download"
834
+ label={t.download}
820
835
  onClick={() => {
821
836
  p.onDownload?.(b);
822
837
  }}
@@ -830,7 +845,7 @@ export function BackupList(p: BackupListProps) {
830
845
  p.onRestore?.(b);
831
846
  }}
832
847
  >
833
- {"Restore"}
848
+ {t.restore}
834
849
  </Button>
835
850
  )}
836
851
  </span>
@@ -842,6 +857,7 @@ export function BackupList(p: BackupListProps) {
842
857
  }
843
858
 
844
859
  export function BlockList(p: BlockListProps) {
860
+ const t = useStrings(ADMIN_STRINGS);
845
861
  const q = React.useState("");
846
862
  const list = (p.items || []).filter(
847
863
  (b) =>
@@ -856,7 +872,7 @@ export function BlockList(p: BlockListProps) {
856
872
  >
857
873
  <div className="tf-filterbar">
858
874
  <SearchField
859
- placeholder={p.searchPlaceholder || "Search blocked entries"}
875
+ placeholder={p.searchPlaceholder || t.searchBlocked}
860
876
  value={q[0]}
861
877
  onChange={(e) => {
862
878
  q[1](e.target.value);
@@ -868,12 +884,12 @@ export function BlockList(p: BlockListProps) {
868
884
  />
869
885
  {p.onAdd && (
870
886
  <Button size="sm" icon="plus" onClick={p.onAdd}>
871
- {p.addLabel || "Block"}
887
+ {p.addLabel || t.block}
872
888
  </Button>
873
889
  )}
874
890
  </div>
875
891
  {list.length === 0 ? (
876
- <p className="tf-card-desc">{p.emptyText || "Nothing blocked."}</p>
892
+ <p className="tf-card-desc">{p.emptyText || t.nothingBlocked}</p>
877
893
  ) : (
878
894
  <div className="tf-blocks" role="list">
879
895
  {list.map((b) => (
@@ -889,7 +905,7 @@ export function BlockList(p: BlockListProps) {
889
905
  <small>{[b.kind, b.reason].filter(Boolean).join(" · ")}</small>
890
906
  </span>
891
907
  <span className="tf-blocks-meta">
892
- {[b.by, b.until ? `until ${b.until}` : "permanent"].filter(Boolean).join(" · ")}
908
+ {[b.by, b.until ? t.until(b.until) : t.permanent].filter(Boolean).join(" · ")}
893
909
  </span>
894
910
  {p.onRemove && (
895
911
  <Button
@@ -899,7 +915,7 @@ export function BlockList(p: BlockListProps) {
899
915
  p.onRemove?.(b);
900
916
  }}
901
917
  >
902
- {p.removeLabel || "Unblock"}
918
+ {p.removeLabel || t.unblock}
903
919
  </Button>
904
920
  )}
905
921
  </div>
@@ -914,14 +930,15 @@ export function BlockList(p: BlockListProps) {
914
930
 
915
931
  /* ── Reviews and coupons ── */
916
932
  export function ReviewCard(p: ReviewCardProps) {
933
+ const t = useStrings(ADMIN_STRINGS);
917
934
  const reply = React.useState(false),
918
935
  txt = React.useState(p.reply || "");
919
936
  const st = (
920
937
  {
921
- pending: ["signal", "Waiting"],
922
- published: ["ok", "Published"],
923
- hidden: ["idle", "Hidden"],
924
- flagged: ["warn", "Flagged"],
938
+ pending: ["signal", t.reviewState.pending],
939
+ published: ["ok", t.reviewState.published],
940
+ hidden: ["idle", t.reviewState.hidden],
941
+ flagged: ["warn", t.reviewState.flagged],
925
942
  } as Record<string, [Status, string]>
926
943
  )[p.state || "pending"] as [Status, string];
927
944
  return (
@@ -939,7 +956,7 @@ export function ReviewCard(p: ReviewCardProps) {
939
956
  {p.verified && (
940
957
  <span className="tf-review-ver">
941
958
  {" · "}
942
- {"Verified purchase"}
959
+ {t.verifiedPurchase}
943
960
  </span>
944
961
  )}
945
962
  </b>
@@ -952,7 +969,7 @@ export function ReviewCard(p: ReviewCardProps) {
952
969
  <p className="tf-review-text">{p.highlight ? markWords(p.text, p.highlight) : p.text}</p>
953
970
  {p.reply && !reply[0] && (
954
971
  <div className="tf-review-reply">
955
- <small>{p.replyLabel || "Your reply"}</small>
972
+ <small>{p.replyLabel || t.yourReply}</small>
956
973
  {p.reply}
957
974
  </div>
958
975
  )}
@@ -970,8 +987,8 @@ export function ReviewCard(p: ReviewCardProps) {
970
987
  onChange={(e) => {
971
988
  txt[1](e.target.value);
972
989
  }}
973
- placeholder="Write a public reply"
974
- aria-label="Reply"
990
+ placeholder={t.writeReply}
991
+ aria-label={t.reply}
975
992
  />
976
993
  <div
977
994
  className="tf-row"
@@ -986,7 +1003,7 @@ export function ReviewCard(p: ReviewCardProps) {
986
1003
  reply[1](false);
987
1004
  }}
988
1005
  >
989
- {"Cancel"}
1006
+ {t.cancel}
990
1007
  </Button>
991
1008
  <Button
992
1009
  size="sm"
@@ -996,7 +1013,7 @@ export function ReviewCard(p: ReviewCardProps) {
996
1013
  reply[1](false);
997
1014
  }}
998
1015
  >
999
- {"Post reply"}
1016
+ {t.postReply}
1000
1017
  </Button>
1001
1018
  </div>
1002
1019
  </div>
@@ -1005,12 +1022,12 @@ export function ReviewCard(p: ReviewCardProps) {
1005
1022
  <div className="tf-review-actions">
1006
1023
  {p.state !== "published" && p.onPublish && (
1007
1024
  <Button size="sm" icon="check" onClick={p.onPublish}>
1008
- {"Publish"}
1025
+ {t.publish}
1009
1026
  </Button>
1010
1027
  )}
1011
1028
  {p.state !== "hidden" && p.onHide && (
1012
1029
  <Button size="sm" variant="secondary" icon="eye" onClick={p.onHide}>
1013
- {"Hide"}
1030
+ {t.hide}
1014
1031
  </Button>
1015
1032
  )}
1016
1033
  {p.onReply && (
@@ -1022,7 +1039,7 @@ export function ReviewCard(p: ReviewCardProps) {
1022
1039
  reply[1](true);
1023
1040
  }}
1024
1041
  >
1025
- {p.reply ? "Edit reply" : "Reply"}
1042
+ {p.reply ? t.editReply : t.reply}
1026
1043
  </Button>
1027
1044
  )}
1028
1045
  </div>
@@ -1042,6 +1059,8 @@ function markWords(text: string, words: string[]) {
1042
1059
  }
1043
1060
 
1044
1061
  export function CouponCard(p: CouponCardProps) {
1062
+ const t = useStrings(ADMIN_STRINGS);
1063
+ const { formatMoney, formatNumber } = useFormat();
1045
1064
  const used = p.used || 0,
1046
1065
  st = p.status || (p.limit && used >= p.limit ? "idle" : "ok");
1047
1066
  return (
@@ -1066,7 +1085,7 @@ export function CouponCard(p: CouponCardProps) {
1066
1085
  <button
1067
1086
  type="button"
1068
1087
  className="tf-coupon-code"
1069
- title="Copy"
1088
+ title={t.copy}
1070
1089
  onClick={() => {
1071
1090
  try {
1072
1091
  navigator.clipboard.writeText(p.code);
@@ -1075,7 +1094,7 @@ export function CouponCard(p: CouponCardProps) {
1075
1094
  }
1076
1095
  toast &&
1077
1096
  toast({
1078
- text: "Copied",
1097
+ text: t.copied,
1079
1098
  });
1080
1099
  p.onCopy && p.onCopy();
1081
1100
  }}
@@ -1091,8 +1110,8 @@ export function CouponCard(p: CouponCardProps) {
1091
1110
  max={p.limit || Math.max(used, 1)}
1092
1111
  text={
1093
1112
  p.limit
1094
- ? `${formatNumber(used)} of ${formatNumber(p.limit)} used`
1095
- : `${formatNumber(used)} used`
1113
+ ? t.usedOf(formatNumber(used), formatNumber(p.limit))
1114
+ : t.used(formatNumber(used))
1096
1115
  }
1097
1116
  tone="sky"
1098
1117
  warn={101}
@@ -1100,9 +1119,9 @@ export function CouponCard(p: CouponCardProps) {
1100
1119
  />
1101
1120
  <span className="tf-coupon-foot">
1102
1121
  {[
1103
- p.until && `Valid until ${p.until}`,
1104
- p.min != null && `from ${formatMoney(p.min, p.currency)}`,
1105
- p.revenue != null && `${formatMoney(p.revenue, p.currency)} revenue`,
1122
+ p.until && t.validUntil(p.until),
1123
+ p.min != null && t.fromAmount(formatMoney(p.min, p.currency)),
1124
+ p.revenue != null && t.revenue(formatMoney(p.revenue, p.currency)),
1106
1125
  ]
1107
1126
  .filter(Boolean)
1108
1127
  .join(" · ")}
@@ -1111,7 +1130,7 @@ export function CouponCard(p: CouponCardProps) {
1111
1130
  {p.menu && (
1112
1131
  <Popover
1113
1132
  width={200}
1114
- trigger={(s) => <IconButton icon="more" size="sm" label="More" onClick={s.toggle} />}
1133
+ trigger={(s) => <IconButton icon="more" size="sm" label={t.more} onClick={s.toggle} />}
1115
1134
  >
1116
1135
  {() => <Menu items={p.menu || []} />}
1117
1136
  </Popover>
@@ -1122,6 +1141,7 @@ export function CouponCard(p: CouponCardProps) {
1122
1141
 
1123
1142
  /* ── Locale, workspace and translations ── */
1124
1143
  export function LocaleField(p: LocaleFieldProps) {
1144
+ const t = useStrings(ADMIN_STRINGS);
1125
1145
  const locs = p.locales || [],
1126
1146
  v = useControlled(p.value, p.defaultValue || {}, p.onChange),
1127
1147
  cur = React.useState(p.active || (locs[0] && locs[0].code)),
@@ -1137,7 +1157,7 @@ export function LocaleField(p: LocaleFieldProps) {
1137
1157
  return (
1138
1158
  <FormField id={id} label={p.label} hint={p.hint} error={p.error}>
1139
1159
  <div className="tf-locfield">
1140
- <div className="tf-locfield-tabs" role="tablist" aria-label="Languages">
1160
+ <div className="tf-locfield-tabs" role="tablist" aria-label={t.languages}>
1141
1161
  {locs.map((l) => {
1142
1162
  const has = (v[0][l.code] || "").trim();
1143
1163
  return (
@@ -1153,11 +1173,11 @@ export function LocaleField(p: LocaleFieldProps) {
1153
1173
  >
1154
1174
  {l.flag && <span aria-hidden>{l.flag}</span>}
1155
1175
  {l.label || l.code.toUpperCase()}
1156
- {!has && <span className="tf-locfield-miss" title="Missing" />}
1176
+ {!has && <span className="tf-locfield-miss" title={t.missing} />}
1157
1177
  </button>
1158
1178
  );
1159
1179
  })}
1160
- <span className="tf-locfield-done">{`${done} of ${locs.length}`}</span>
1180
+ <span className="tf-locfield-done">{t.doneOf(done, locs.length)}</span>
1161
1181
  </div>
1162
1182
  {p.multiline ? (
1163
1183
  <textarea
@@ -1196,7 +1216,7 @@ export function LocaleField(p: LocaleFieldProps) {
1196
1216
  set(v[0][base] || "");
1197
1217
  }}
1198
1218
  >
1199
- {`Copy from ${(locs.find((l) => l.code === base) || {}).label || base}`}
1219
+ {t.copyFrom((locs.find((l) => l.code === base) || {}).label || base)}
1200
1220
  </Button>
1201
1221
  {p.onTranslate && (
1202
1222
  <Button
@@ -1209,7 +1229,7 @@ export function LocaleField(p: LocaleFieldProps) {
1209
1229
  });
1210
1230
  }}
1211
1231
  >
1212
- {"Translate"}
1232
+ {t.translate}
1213
1233
  </Button>
1214
1234
  )}
1215
1235
  </div>
@@ -1220,6 +1240,7 @@ export function LocaleField(p: LocaleFieldProps) {
1220
1240
  }
1221
1241
 
1222
1242
  export function LocaleSwitcher(p: LocaleSwitcherProps) {
1243
+ const t = useStrings(ADMIN_STRINGS);
1223
1244
  const lang = useControlled(
1224
1245
  p.language,
1225
1246
  p.languages && p.languages[0] && p.languages[0].code,
@@ -1238,7 +1259,7 @@ export function LocaleSwitcher(p: LocaleSwitcherProps) {
1238
1259
  aria-haspopup="dialog"
1239
1260
  aria-expanded={s.open}
1240
1261
  onClick={s.toggle}
1241
- title="Language and currency"
1262
+ title={t.languageAndCurrency}
1242
1263
  >
1243
1264
  {L0.flag ? <span>{L0.flag}</span> : <Icon name="globe" size={15} />}
1244
1265
  <span>{(L0.short || lang[0] || "").toUpperCase()}</span>
@@ -1267,7 +1288,7 @@ export function LocaleSwitcher(p: LocaleSwitcherProps) {
1267
1288
  padding: "0 8px",
1268
1289
  }}
1269
1290
  >
1270
- {"Language"}
1291
+ {t.language}
1271
1292
  </span>
1272
1293
  <div
1273
1294
  className="tf-menu"
@@ -1312,7 +1333,7 @@ export function LocaleSwitcher(p: LocaleSwitcherProps) {
1312
1333
  padding: "0 8px",
1313
1334
  }}
1314
1335
  >
1315
- {"Currency"}
1336
+ {t.currency}
1316
1337
  </span>
1317
1338
  <Segmented size="sm" block value={cur[0]} onChange={cur[1]} options={p.currencies} />
1318
1339
  </div>
@@ -1417,6 +1438,8 @@ export function WorkspaceSwitcher(p: WorkspaceSwitcherProps) {
1417
1438
 
1418
1439
  /* ── Export and 2FA ── */
1419
1440
  export function ExportDialog(p: ExportDialogProps) {
1441
+ const t = useStrings(ADMIN_STRINGS);
1442
+ const { formatNumber, formatPercent } = useFormat();
1420
1443
  const fmt = React.useState(p.format || (p.formats || ["csv"])[0]),
1421
1444
  cols = React.useState((p.columns || []).filter((c) => c.default !== false).map((c) => c.key)),
1422
1445
  range = React.useState<DateRange | null>(p.range || null),
@@ -1446,25 +1469,25 @@ export function ExportDialog(p: ExportDialogProps) {
1446
1469
  className="tf-modal tf-export"
1447
1470
  role="dialog"
1448
1471
  aria-modal={p.inline ? undefined : "true"}
1449
- aria-label={p.title || "Export"}
1472
+ aria-label={p.title || t.export}
1450
1473
  >
1451
1474
  <span className="tf-modal-close">
1452
- <IconButton icon="x" label="Close" size="sm" onClick={p.onClose} />
1475
+ <IconButton icon="x" label={t.close} size="sm" onClick={p.onClose} />
1453
1476
  </span>
1454
1477
  <div>
1455
- <h2 className="tf-modal-title">{p.title || "Export"}</h2>
1478
+ <h2 className="tf-modal-title">{p.title || t.export}</h2>
1456
1479
  <p
1457
1480
  className="tf-modal-text"
1458
1481
  style={{
1459
1482
  marginTop: 6,
1460
1483
  }}
1461
1484
  >
1462
- {p.text || `${formatNumber(p.count || 0)} rows match your filters.`}
1485
+ {p.text || t.rowsMatch(formatNumber(p.count || 0), p.count || 0)}
1463
1486
  </p>
1464
1487
  </div>
1465
1488
  {st[0] === "form" ? (
1466
1489
  <React.Fragment>
1467
- <FormField label="Format">
1490
+ <FormField label={t.format}>
1468
1491
  <Segmented
1469
1492
  block
1470
1493
  value={fmt[0]}
@@ -1477,15 +1500,15 @@ export function ExportDialog(p: ExportDialogProps) {
1477
1500
  </FormField>
1478
1501
  {p.ranges !== false && (
1479
1502
  <DatePicker
1480
- label="Period"
1503
+ label={t.period}
1481
1504
  range
1482
1505
  value={range[0]}
1483
1506
  onChange={(r) => range[1](r as DateRange | null)}
1484
- placeholder="All time"
1507
+ placeholder={t.allTime}
1485
1508
  />
1486
1509
  )}
1487
1510
  {(p.columns || []).length > 0 && (
1488
- <FormField label="Columns" hint={`${cols[0].length} selected`}>
1511
+ <FormField label={t.columns} hint={t.selectedCount(cols[0].length)}>
1489
1512
  <div className="tf-export-cols">
1490
1513
  {(p.columns || []).map((c) => {
1491
1514
  const on = cols[0].indexOf(c.key) > -1;
@@ -1518,14 +1541,14 @@ export function ExportDialog(p: ExportDialogProps) {
1518
1541
  )}
1519
1542
  <div className="tf-modal-foot">
1520
1543
  <Button variant="ghost" onClick={p.onClose}>
1521
- {"Cancel"}
1544
+ {t.cancel}
1522
1545
  </Button>
1523
1546
  <Button
1524
1547
  icon="download"
1525
1548
  disabled={!cols[0].length && (p.columns || []).length > 0}
1526
1549
  onClick={start}
1527
1550
  >
1528
- {"Export"}
1551
+ {t.export}
1529
1552
  </Button>
1530
1553
  </div>
1531
1554
  </React.Fragment>
@@ -1536,14 +1559,14 @@ export function ExportDialog(p: ExportDialogProps) {
1536
1559
  gap: 8,
1537
1560
  }}
1538
1561
  >
1539
- <Progress value={prog[0]} label="Export progress" />
1562
+ <Progress value={prog[0]} label={t.exportProgress} />
1540
1563
  <span className="tf-card-desc">
1541
- {"Preparing the file…" + " " + Math.round(prog[0]) + "%"}
1564
+ {t.preparingFile + " " + formatPercent(Math.round(prog[0]))}
1542
1565
  </span>
1543
1566
  </div>
1544
1567
  ) : (
1545
1568
  <React.Fragment>
1546
- <StatusIndicator status="ok" label={p.doneText || "Your file is ready."} />
1569
+ <StatusIndicator status="ok" label={p.doneText || t.fileReady} />
1547
1570
  <div className="tf-modal-foot">
1548
1571
  <Button
1549
1572
  variant="ghost"
@@ -1551,11 +1574,11 @@ export function ExportDialog(p: ExportDialogProps) {
1551
1574
  st[1]("form");
1552
1575
  }}
1553
1576
  >
1554
- {"New export"}
1577
+ {t.newExport}
1555
1578
  </Button>
1556
1579
  {p.download || (
1557
1580
  <Button icon="download" onClick={p.onDownload}>
1558
- {"Download"}
1581
+ {t.download}
1559
1582
  </Button>
1560
1583
  )}
1561
1584
  </div>
@@ -1576,10 +1599,11 @@ export function ExportDialog(p: ExportDialogProps) {
1576
1599
  }
1577
1600
 
1578
1601
  export function TwoFactorSetup(p: TwoFactorSetupProps) {
1602
+ const t = useStrings(ADMIN_STRINGS);
1579
1603
  const step = React.useState(p.step || 0),
1580
1604
  err = React.useState<string | null>(null),
1581
1605
  saved = React.useState(false);
1582
- const steps = ["Scan", "Confirm", "Save codes"];
1606
+ const steps = t.twoFactorSteps;
1583
1607
  return (
1584
1608
  <div className="tf-2fa">
1585
1609
  <ol className="tf-2fa-steps">
@@ -1592,7 +1616,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1592
1616
  </ol>
1593
1617
  {step[0] === 0 && (
1594
1618
  <div className="tf-2fa-scan">
1595
- <div className="tf-2fa-qr" role="img" aria-label="QR code">
1619
+ <div className="tf-2fa-qr" role="img" aria-label={t.qrCode}>
1596
1620
  {p.qr || <Icon name="grid" size={48} />}
1597
1621
  </div>
1598
1622
  <div
@@ -1608,8 +1632,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1608
1632
  margin: 0,
1609
1633
  }}
1610
1634
  >
1611
- {p.scanText ||
1612
- "Scan the code with your authenticator app. If you can't scan it, enter the key by hand."}
1635
+ {p.scanText || t.scanText}
1613
1636
  </p>
1614
1637
  <button
1615
1638
  type="button"
@@ -1620,7 +1643,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1620
1643
  } catch {
1621
1644
  /* clipboard unavailable */
1622
1645
  }
1623
- toast && toast({ text: "Copied" });
1646
+ toast && toast({ text: t.copied });
1624
1647
  }}
1625
1648
  >
1626
1649
  <span className="tf-mono">{(p.secret || "").replace(/(.{4})/g, "$1 ").trim()}</span>
@@ -1632,7 +1655,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1632
1655
  step[1](1);
1633
1656
  }}
1634
1657
  >
1635
- {"Next"}
1658
+ {t.next}
1636
1659
  </Button>
1637
1660
  </div>
1638
1661
  </div>
@@ -1651,7 +1674,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1651
1674
  margin: 0,
1652
1675
  }}
1653
1676
  >
1654
- {"Enter the 6-digit code from the app."}
1677
+ {t.enterCode}
1655
1678
  </p>
1656
1679
  <OtpInput
1657
1680
  error={!!err[0]}
@@ -1660,7 +1683,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1660
1683
  if (ok) {
1661
1684
  err[1](null);
1662
1685
  step[1](2);
1663
- } else err[1]("That code didn't work. Try the current one.");
1686
+ } else err[1](t.codeFailed);
1664
1687
  });
1665
1688
  }}
1666
1689
  />
@@ -1672,7 +1695,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1672
1695
  step[1](0);
1673
1696
  }}
1674
1697
  >
1675
- {"Back"}
1698
+ {t.back}
1676
1699
  </Button>
1677
1700
  </div>
1678
1701
  </div>
@@ -1690,8 +1713,7 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1690
1713
  margin: 0,
1691
1714
  }}
1692
1715
  >
1693
- {p.codesText ||
1694
- "Each code works once if you lose your phone. Keep them somewhere safe."}
1716
+ {p.codesText || t.codesText}
1695
1717
  </p>
1696
1718
  <div className="tf-2fa-codes">
1697
1719
  {(p.codes || []).map((c) => (
@@ -1708,14 +1730,14 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1708
1730
  } catch {
1709
1731
  /* clipboard unavailable */
1710
1732
  }
1711
- toast && toast({ text: "Copied" });
1733
+ toast && toast({ text: t.copied });
1712
1734
  }}
1713
1735
  >
1714
- {"Copy"}
1736
+ {t.copy}
1715
1737
  </Button>
1716
1738
  {p.onDownload && (
1717
1739
  <Button variant="secondary" icon="download" onClick={p.onDownload}>
1718
- {"Download"}
1740
+ {t.download}
1719
1741
  </Button>
1720
1742
  )}
1721
1743
  </div>
@@ -1725,11 +1747,11 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1725
1747
  saved[1](!saved[0]);
1726
1748
  }}
1727
1749
  >
1728
- {"I saved the recovery codes"}
1750
+ {t.savedCodes}
1729
1751
  </Checkbox>
1730
1752
  <div>
1731
1753
  <Button icon="check" disabled={!saved[0]} onClick={p.onDone}>
1732
- {"Turn on two-factor"}
1754
+ {t.turnOnTwoFactor}
1733
1755
  </Button>
1734
1756
  </div>
1735
1757
  </div>
@@ -1741,6 +1763,8 @@ export function TwoFactorSetup(p: TwoFactorSetupProps) {
1741
1763
  /* ── Media ── */
1742
1764
 
1743
1765
  export function ColorField(p: ColorFieldProps) {
1766
+ const t = useStrings(ADMIN_STRINGS);
1767
+ const { formatNumber } = useFormat();
1744
1768
  const id = useId("cf"),
1745
1769
  v = useControlled(p.value, p.defaultValue || "#000000", p.onChange);
1746
1770
  function lum(hx: string) {
@@ -1771,12 +1795,12 @@ export function ColorField(p: ColorFieldProps) {
1771
1795
  onChange={(e) => {
1772
1796
  v[1](e.target.value);
1773
1797
  }}
1774
- aria-label="Pick a colour"
1798
+ aria-label={t.pickColour}
1775
1799
  />
1776
1800
  </label>
1777
1801
  <input
1778
1802
  id={id}
1779
- aria-label={p.label ? undefined : "Pick a colour"}
1803
+ aria-label={p.label ? undefined : t.pickColour}
1780
1804
  className="tf-input tf-mono"
1781
1805
  value={v[0]}
1782
1806
  onChange={(e) => {
@@ -1787,13 +1811,13 @@ export function ColorField(p: ColorFieldProps) {
1787
1811
  {ratio != null && p.against !== false && (
1788
1812
  <span
1789
1813
  className={cx("tf-colorf-ratio", ratio >= 4.5 ? "ok" : ratio >= 3 ? "warn" : "bad")}
1790
- title={`Contrast against ${p.against || "#ffffff"}`}
1814
+ title={t.contrastAgainst(p.against || "#ffffff")}
1791
1815
  >
1792
1816
  {formatNumber(ratio, {
1793
1817
  maximumFractionDigits: 1,
1794
1818
  }) +
1795
1819
  ":1 " +
1796
- (ratio >= 4.5 ? "AA" : ratio >= 3 ? "AA large" : "Fails")}
1820
+ (ratio >= 4.5 ? t.contrastAA : ratio >= 3 ? t.contrastAALarge : t.contrastFails)}
1797
1821
  </span>
1798
1822
  )}
1799
1823
  </div>