@tribe-nest/forge 3.26.0 → 3.29.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.
Files changed (90) hide show
  1. package/package.json +1 -1
  2. package/src/client/_tests/localeHeader.spec.tsx +98 -0
  3. package/src/client/activeLocale.ts +34 -0
  4. package/src/client/createForgeClient.ts +13 -0
  5. package/src/contexts/CartContext.tsx +76 -0
  6. package/src/contexts/_tests/CartContext.spec.tsx +210 -0
  7. package/src/data/queries/useCheckouts.ts +83 -0
  8. package/src/data/queries/useWebsite.ts +7 -0
  9. package/src/i18n/CONVENTION.md +184 -0
  10. package/src/i18n/_tests/translationKeys.spec.ts +153 -0
  11. package/src/i18n/de.json +960 -0
  12. package/src/i18n/en.json +960 -0
  13. package/src/i18n/index.ts +125 -0
  14. package/src/index.ts +21 -0
  15. package/src/provider/ForgeAppProvider.tsx +2 -1
  16. package/src/provider/SiteConfigProvider.tsx +11 -0
  17. package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
  18. package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
  19. package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
  20. package/src/ui/headless/checkout/useCartResume.ts +340 -0
  21. package/src/ui/headless/checkout/useCheckout.ts +9 -2
  22. package/src/ui/headless/index.ts +18 -0
  23. package/src/ui/index.ts +2 -0
  24. package/src/ui/shell/TribeNestApp.tsx +36 -27
  25. package/src/ui/styled/AccountDashboard.tsx +197 -93
  26. package/src/ui/styled/AiAgentWidget.tsx +10 -6
  27. package/src/ui/styled/AudioPlayer.tsx +14 -6
  28. package/src/ui/styled/BlogCategory.tsx +4 -2
  29. package/src/ui/styled/BlogComments.tsx +27 -20
  30. package/src/ui/styled/BlogList.tsx +5 -3
  31. package/src/ui/styled/BlogPost.tsx +14 -11
  32. package/src/ui/styled/BundleConfirmation.tsx +32 -23
  33. package/src/ui/styled/CancellationTerms.tsx +3 -1
  34. package/src/ui/styled/Cart.tsx +32 -14
  35. package/src/ui/styled/ChatRoom.tsx +65 -20
  36. package/src/ui/styled/Checkout.tsx +151 -89
  37. package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
  38. package/src/ui/styled/CoachingBooking.tsx +71 -44
  39. package/src/ui/styled/CoachingConfirmation.tsx +56 -19
  40. package/src/ui/styled/CoachingDetail.tsx +15 -11
  41. package/src/ui/styled/CohortPage.tsx +10 -8
  42. package/src/ui/styled/ConfirmSubscription.tsx +14 -8
  43. package/src/ui/styled/ContactForm.tsx +31 -12
  44. package/src/ui/styled/CookieConsent.tsx +19 -15
  45. package/src/ui/styled/CourseAccess.tsx +54 -47
  46. package/src/ui/styled/CourseCheckout.tsx +56 -38
  47. package/src/ui/styled/CourseConfirmation.tsx +39 -17
  48. package/src/ui/styled/CourseDetail.tsx +21 -15
  49. package/src/ui/styled/CoursesGrid.tsx +4 -2
  50. package/src/ui/styled/CurrencySwitcher.tsx +4 -1
  51. package/src/ui/styled/DiscountCode.tsx +22 -18
  52. package/src/ui/styled/DonationButton.tsx +52 -17
  53. package/src/ui/styled/DonationPage.tsx +4 -4
  54. package/src/ui/styled/EmailListForm.tsx +22 -9
  55. package/src/ui/styled/EventConfirmation.tsx +37 -14
  56. package/src/ui/styled/EventCountdown.tsx +10 -6
  57. package/src/ui/styled/EventDetail.tsx +8 -6
  58. package/src/ui/styled/EventSeriesDetail.tsx +18 -13
  59. package/src/ui/styled/EventTickets.tsx +232 -108
  60. package/src/ui/styled/EventWaitlist.tsx +42 -33
  61. package/src/ui/styled/EventsList.tsx +15 -13
  62. package/src/ui/styled/ForgotPasswordForm.tsx +12 -12
  63. package/src/ui/styled/FormRenderer.tsx +12 -7
  64. package/src/ui/styled/InstallBanner.tsx +21 -18
  65. package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
  66. package/src/ui/styled/InvoicePayment.tsx +37 -16
  67. package/src/ui/styled/LeadMagnet.tsx +9 -7
  68. package/src/ui/styled/Loading.tsx +7 -1
  69. package/src/ui/styled/LoginForm.tsx +22 -14
  70. package/src/ui/styled/MembershipCheckout.tsx +32 -13
  71. package/src/ui/styled/MembershipTiers.tsx +27 -18
  72. package/src/ui/styled/OfferButton.tsx +33 -13
  73. package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
  74. package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
  75. package/src/ui/styled/PodcastEpisode.tsx +20 -9
  76. package/src/ui/styled/PodcastList.tsx +9 -3
  77. package/src/ui/styled/PodcastShow.tsx +29 -7
  78. package/src/ui/styled/PostsFeed.tsx +10 -8
  79. package/src/ui/styled/PresaleCode.tsx +8 -11
  80. package/src/ui/styled/ProductBrowseNav.tsx +21 -11
  81. package/src/ui/styled/ProductDetail.tsx +58 -28
  82. package/src/ui/styled/ReactionBar.tsx +4 -2
  83. package/src/ui/styled/ReplayList.tsx +24 -13
  84. package/src/ui/styled/ResetPasswordForm.tsx +27 -15
  85. package/src/ui/styled/ResumeCart.tsx +289 -0
  86. package/src/ui/styled/ReviewForm.tsx +52 -58
  87. package/src/ui/styled/SignupForm.tsx +29 -16
  88. package/src/ui/styled/TicketTransfer.tsx +55 -54
  89. package/src/ui/styled/UserMenu.tsx +16 -14
  90. package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
@@ -30,6 +30,7 @@ import {
30
30
  OrderStatus,
31
31
  type NotificationPreferences,
32
32
  } from "../../index";
33
+ import { useForgeT, type ForgeT } from "../../i18n";
33
34
  import { useThemeTokens } from "../theme/ForgeThemeProvider";
34
35
  import { useSiteConfig } from "../../data/queries/useWebsite";
35
36
  import { useAmountFormatter } from "../format/useFormatCurrency";
@@ -57,6 +58,21 @@ export const ACCOUNT_TABS = [
57
58
  ] as const;
58
59
  export type AccountTabKey = (typeof ACCOUNT_TABS)[number];
59
60
 
61
+ /**
62
+ * The tab strip's visible labels. The tab keys themselves are route values, so
63
+ * the copy is looked up rather than read off the key.
64
+ */
65
+ const TAB_LABEL_KEYS: Record<AccountTabKey, string> = {
66
+ membership: "forge.account_dashboard.tab_membership",
67
+ orders: "forge.account_dashboard.tab_orders",
68
+ tickets: "forge.account_dashboard.tab_tickets",
69
+ waitlist: "forge.account_dashboard.tab_waitlist",
70
+ bookings: "forge.account_dashboard.tab_bookings",
71
+ saved: "forge.account_dashboard.tab_saved",
72
+ account: "forge.account_dashboard.tab_account",
73
+ notifications: "forge.account_dashboard.tab_notifications",
74
+ };
75
+
60
76
  export interface AccountDashboardProps {
61
77
  className?: string;
62
78
  style?: React.CSSProperties;
@@ -155,6 +171,7 @@ export function AccountDashboard({
155
171
  }: AccountDashboardProps) {
156
172
  const { user, logout } = usePublicAuth();
157
173
  const { t } = useCardStyles();
174
+ const tr = useForgeT();
158
175
  const { data: siteConfig } = useSiteConfig();
159
176
 
160
177
  const fmtAmount = useAmountFormatter(formatAmount);
@@ -175,14 +192,16 @@ export function AccountDashboard({
175
192
  <div className={className} style={{ maxWidth: 900, margin: "0 auto", color: t.text, ...style }}>
176
193
  <div style={{ marginBottom: 24, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16 }}>
177
194
  <div>
178
- <h1 style={{ fontSize: 28, fontWeight: 800, fontFamily: t.headingFontFamily }}>My Account</h1>
195
+ <h1 style={{ fontSize: 28, fontWeight: 800, fontFamily: t.headingFontFamily }}>
196
+ {tr("forge.account_dashboard.title")}
197
+ </h1>
179
198
  <p style={{ opacity: 0.7 }}>{user.email}</p>
180
199
  </div>
181
200
  <button
182
201
  onClick={() => logout()}
183
202
  style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "transparent", color: t.text, border: `1px solid ${t.primary}40`, cursor: "pointer" }}
184
203
  >
185
- Log out
204
+ {tr("forge.account_dashboard.log_out")}
186
205
  </button>
187
206
  </div>
188
207
 
@@ -202,7 +221,7 @@ export function AccountDashboard({
202
221
  fontWeight: 600,
203
222
  }}
204
223
  >
205
- {tabKey}
224
+ {tr(TAB_LABEL_KEYS[tabKey])}
206
225
  </button>
207
226
  ))}
208
227
  </div>
@@ -224,6 +243,7 @@ export function AccountDashboard({
224
243
  function MembershipTab({ ctx }: { ctx: TabContext }) {
225
244
  const { user } = usePublicAuth();
226
245
  const { t, card, button, ghostButton } = useCardStyles();
246
+ const tr = useForgeT();
227
247
  const { formatAmount, formatDate, currency, onNavigateMembership } = ctx;
228
248
  const cancelMembership = useCancelMembership();
229
249
  const [isCancelling, setIsCancelling] = useState(false);
@@ -252,7 +272,7 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
252
272
  window.location.reload();
253
273
  } catch (error) {
254
274
  const detail = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
255
- setCancelError(detail || "We couldn't cancel your membership. Please try again or contact us.");
275
+ setCancelError(detail || tr("forge.account_dashboard.membership_cancel_error"));
256
276
  } finally {
257
277
  setIsCancelling(false);
258
278
  }
@@ -265,13 +285,15 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
265
285
  window.location.href = url;
266
286
  } catch (error) {
267
287
  const detail = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
268
- setBillingError(detail || "We couldn't open the billing page. Please contact us.");
288
+ setBillingError(detail || tr("forge.account_dashboard.billing_portal_error"));
269
289
  }
270
290
  };
271
291
 
272
292
  return (
273
293
  <div style={card}>
274
- <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>Current Membership</h2>
294
+ <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
295
+ {tr("forge.account_dashboard.membership_title")}
296
+ </h2>
275
297
  {membership ? (
276
298
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
277
299
  <div>
@@ -291,7 +313,10 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
291
313
  )}
292
314
  {!!membership.subscriptionAmount && (
293
315
  <p style={{ fontSize: 14, marginTop: 4 }}>
294
- {formatAmount(membership.subscriptionAmount, membership.subscriptionCurrency || currency)} / {membership.billingCycle}
316
+ {tr("forge.account_dashboard.membership_price", {
317
+ amount: formatAmount(membership.subscriptionAmount, membership.subscriptionCurrency || currency),
318
+ cycle: membership.billingCycle,
319
+ })}
295
320
  </p>
296
321
  )}
297
322
  </div>
@@ -307,7 +332,9 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
307
332
  )}
308
333
  {membership.endDate && (
309
334
  <p style={{ fontSize: 14, opacity: 0.8 }}>
310
- {access.billingState === "active" ? "Renews" : "Ends"} on {formatDate(membership.endDate)}
335
+ {access.billingState === "active"
336
+ ? tr("forge.account_dashboard.membership_renews_on", { date: formatDate(membership.endDate) })
337
+ : tr("forge.account_dashboard.membership_ends_on", { date: formatDate(membership.endDate) })}
311
338
  </p>
312
339
  )}
313
340
  {billingError && <p style={{ fontSize: 13, opacity: 0.9 }}>{billingError}</p>}
@@ -318,13 +345,15 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
318
345
  )}
319
346
  </div>
320
347
  ) : (
321
- <p style={{ opacity: 0.8 }}>You don&apos;t have an active membership.</p>
348
+ <p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.membership_empty")}</p>
322
349
  )}
323
350
 
324
351
  <div style={{ display: "flex", gap: 12, justifyContent: "flex-end", marginTop: 24 }}>
325
352
  {message.showUpdatePayment && (
326
353
  <button onClick={onUpdatePayment} disabled={openBillingPortal.isPending} style={button}>
327
- {openBillingPortal.isPending ? "Opening…" : "Update payment method"}
354
+ {openBillingPortal.isPending
355
+ ? tr("forge.account_dashboard.billing_portal_opening")
356
+ : tr("forge.account_dashboard.update_payment_method")}
328
357
  </button>
329
358
  )}
330
359
  {/* Cancelling stays available for anyone who still HAS access — a
@@ -333,11 +362,13 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
333
362
  the only honest label would be "Cancel again". */}
334
363
  {access.hasAccess && !access.cancelAtPeriodEnd && (
335
364
  <button onClick={onCancel} disabled={isCancelling} style={ghostButton}>
336
- {isCancelling ? "Cancelling…" : "Cancel"}
365
+ {isCancelling ? tr("forge.account_dashboard.cancelling") : tr("forge.account_dashboard.cancel")}
337
366
  </button>
338
367
  )}
339
368
  <button onClick={() => onNavigateMembership?.()} style={button}>
340
- {membership ? "Change" : "Join"}
369
+ {membership
370
+ ? tr("forge.account_dashboard.membership_change")
371
+ : tr("forge.account_dashboard.membership_join")}
341
372
  </button>
342
373
  </div>
343
374
  </div>
@@ -369,6 +400,7 @@ function MembershipTab({ ctx }: { ctx: TabContext }) {
369
400
  function TicketsTab({ ctx }: { ctx: TabContext }) {
370
401
  const { user } = usePublicAuth();
371
402
  const { t, card } = useCardStyles();
403
+ const tr = useForgeT();
372
404
  const { formatAmount, formatDate, currency } = ctx;
373
405
  const { data, isLoading } = useMyTickets(user?.id);
374
406
  const cancelTicket = useCancelMyTicket();
@@ -385,7 +417,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
385
417
  } catch (err) {
386
418
  const message =
387
419
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
388
- "We could not cancel this ticket. Please try again.";
420
+ tr("forge.account_dashboard.ticket_cancel_error");
389
421
  setError(message);
390
422
  } finally {
391
423
  setPendingId(null);
@@ -394,7 +426,9 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
394
426
 
395
427
  return (
396
428
  <div style={card}>
397
- <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>My Tickets</h2>
429
+ <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
430
+ {tr("forge.account_dashboard.tickets_title")}
431
+ </h2>
398
432
  {error && (
399
433
  <div style={{ marginBottom: 16, padding: 12, borderRadius: t.cornerRadius, border: `1px solid ${t.primary}40`, fontSize: 14 }}>
400
434
  {error}
@@ -418,19 +452,26 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
418
452
  <div style={{ fontWeight: 600 }}>{ticket.eventTitle}</div>
419
453
  <div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(ticket.eventDateTime)}</div>
420
454
  </div>
421
- {cancelled && <span style={{ fontSize: 13, color: t.primary }}>Cancelled</span>}
455
+ {cancelled && (
456
+ <span style={{ fontSize: 13, color: t.primary }}>{tr("forge.account_dashboard.cancelled")}</span>
457
+ )}
422
458
  {/* Says WHY this is in the list at all. Without it a
423
459
  transferred ticket looks like a purchase the buyer cannot
424
460
  remember making, with no prices on it. */}
425
461
  {!cancelled && gifted && (
426
- <span style={{ fontSize: 13, color: t.primary }}>Sent to you</span>
462
+ <span style={{ fontSize: 13, color: t.primary }}>
463
+ {tr("forge.account_dashboard.ticket_transferred")}
464
+ </span>
427
465
  )}
428
466
  </div>
429
467
 
430
468
  {ticket.items.map((item) => (
431
469
  <div key={item.id} style={{ display: "flex", justifyContent: "space-between", fontSize: 14, padding: "4px 0" }}>
432
470
  <span>
433
- {item.ticketTitle ?? "Ticket"} × {item.quantity}
471
+ {tr("forge.account_dashboard.ticket_line", {
472
+ title: item.ticketTitle ?? tr("forge.account_dashboard.ticket_fallback_title"),
473
+ quantity: item.quantity,
474
+ })}
434
475
  </span>
435
476
  {/* A holder is not shown a price, because the server did
436
477
  not send one — `Number(null)` is 0, and a confident
@@ -443,7 +484,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
443
484
 
444
485
  {!gifted && (
445
486
  <div style={{ display: "flex", justifyContent: "space-between", fontWeight: 700, marginTop: 12, paddingTop: 12, borderTop: `1px solid ${t.primary}20` }}>
446
- <span>Total</span>
487
+ <span>{tr("forge.account_dashboard.total")}</span>
447
488
  <span style={{ color: t.primary }}>{formatAmount(Number(ticket.totalAmount), ticket.currency || currency)}</span>
448
489
  </div>
449
490
  )}
@@ -522,7 +563,9 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
522
563
  fontWeight: 600,
523
564
  }}
524
565
  >
525
- {pendingId === ticket.id ? "Cancelling…" : "Cancel ticket"}
566
+ {pendingId === ticket.id
567
+ ? tr("forge.account_dashboard.cancelling")
568
+ : tr("forge.account_dashboard.cancel_ticket")}
526
569
  </button>
527
570
  )}
528
571
  </div>
@@ -530,7 +573,7 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
530
573
  })}
531
574
  </div>
532
575
  ) : (
533
- <p style={{ fontSize: 14, opacity: 0.7 }}>You have no tickets yet.</p>
576
+ <p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.tickets_empty")}</p>
534
577
  )}
535
578
  </div>
536
579
  );
@@ -555,12 +598,15 @@ function TicketsTab({ ctx }: { ctx: TabContext }) {
555
598
  function WaitlistTab({ ctx }: { ctx: TabContext }) {
556
599
  const { user } = usePublicAuth();
557
600
  const { t, card } = useCardStyles();
601
+ const tr = useForgeT();
558
602
  const { formatDate } = ctx;
559
603
  const { places, isLoading } = useMyWaitlistPlaces(user?.id);
560
604
 
561
605
  return (
562
606
  <div style={card}>
563
- <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>My Waitlists</h2>
607
+ <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
608
+ {tr("forge.account_dashboard.waitlists_title")}
609
+ </h2>
564
610
  {isLoading ? (
565
611
  <Loading />
566
612
  ) : places.length > 0 ? (
@@ -570,7 +616,7 @@ function WaitlistTab({ ctx }: { ctx: TabContext }) {
570
616
  ))}
571
617
  </div>
572
618
  ) : (
573
- <p style={{ fontSize: 14, opacity: 0.7 }}>You&apos;re not on any waitlists.</p>
619
+ <p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.waitlists_empty")}</p>
574
620
  )}
575
621
  </div>
576
622
  );
@@ -584,6 +630,7 @@ function WaitlistPlaceRow({
584
630
  formatDate: (value: string | number | Date) => string;
585
631
  }) {
586
632
  const { t } = useCardStyles();
633
+ const tr = useForgeT();
587
634
  const leave = useLeaveEventWaitlist(place.eventId);
588
635
  const [error, setError] = useState<string | null>(null);
589
636
 
@@ -594,7 +641,7 @@ function WaitlistPlaceRow({
594
641
  } catch (err) {
595
642
  const message =
596
643
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
597
- "We could not remove you from this waitlist. Please try again.";
644
+ tr("forge.account_dashboard.waitlist_leave_error");
598
645
  setError(message);
599
646
  }
600
647
  };
@@ -611,22 +658,35 @@ function WaitlistPlaceRow({
611
658
  >
612
659
  <div style={{ display: "flex", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
613
660
  <div>
614
- <div style={{ fontWeight: 600 }}>{place.event?.title ?? "Event"}</div>
661
+ <div style={{ fontWeight: 600 }}>
662
+ {place.event?.title ?? tr("forge.account_dashboard.waitlist_event_fallback")}
663
+ </div>
615
664
  <div style={{ fontSize: 13, opacity: 0.7 }}>
616
- {place.ticket?.title ?? "Ticket"} · {place.quantity} {place.quantity === 1 ? "ticket" : "tickets"}
665
+ {tr(
666
+ place.quantity === 1
667
+ ? "forge.account_dashboard.waitlist_ticket_line_one"
668
+ : "forge.account_dashboard.waitlist_ticket_line_other",
669
+ {
670
+ title: place.ticket?.title ?? tr("forge.account_dashboard.waitlist_ticket_fallback"),
671
+ quantity: place.quantity,
672
+ },
673
+ )}
617
674
  </div>
618
675
  {place.event?.dateTime && (
619
676
  <div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(place.event.dateTime)}</div>
620
677
  )}
621
678
  </div>
622
679
  {place.offer.offerOpen ? (
623
- <span style={{ fontWeight: 700, color: t.primary }}>Tickets available</span>
680
+ <span style={{ fontWeight: 700, color: t.primary }}>
681
+ {tr("forge.account_dashboard.waitlist_offer_open")}
682
+ </span>
624
683
  ) : place.position != null ? (
625
684
  <span style={{ fontSize: 14, opacity: 0.85 }}>
626
- Position <strong style={{ color: t.primary }}>#{place.position}</strong>
685
+ {tr("forge.account_dashboard.waitlist_position_label")}{" "}
686
+ <strong style={{ color: t.primary }}>#{place.position}</strong>
627
687
  </span>
628
688
  ) : (
629
- <span style={{ fontSize: 14, opacity: 0.85 }}>You&apos;re on the list</span>
689
+ <span style={{ fontSize: 14, opacity: 0.85 }}>{tr("forge.account_dashboard.waitlist_on_list")}</span>
630
690
  )}
631
691
  </div>
632
692
 
@@ -640,15 +700,21 @@ function WaitlistPlaceRow({
640
700
  fontSize: 14,
641
701
  }}
642
702
  >
643
- <strong>Buy within {formatCountdown(place.offer.secondsRemaining ?? 0)}.</strong> Your offer closes{" "}
644
- {deadline.toLocaleString(undefined, {
645
- weekday: "short",
646
- day: "numeric",
647
- month: "short",
648
- hour: "numeric",
649
- minute: "2-digit",
650
- })}
651
- . Tickets are not reserved until you buy them.
703
+ <strong>
704
+ {tr("forge.account_dashboard.waitlist_offer_deadline", {
705
+ countdown: formatCountdown(place.offer.secondsRemaining ?? 0),
706
+ })}
707
+ </strong>{" "}
708
+ {tr("forge.account_dashboard.waitlist_offer_closes", {
709
+ datetime: deadline.toLocaleString(undefined, {
710
+ weekday: "short",
711
+ day: "numeric",
712
+ month: "short",
713
+ hour: "numeric",
714
+ minute: "2-digit",
715
+ }),
716
+ })}{" "}
717
+ {tr("forge.account_dashboard.waitlist_offer_not_reserved")}
652
718
  </div>
653
719
  )}
654
720
 
@@ -668,7 +734,9 @@ function WaitlistPlaceRow({
668
734
  fontWeight: 600,
669
735
  }}
670
736
  >
671
- {leave.isPending ? "Leaving…" : "Leave waitlist"}
737
+ {leave.isPending
738
+ ? tr("forge.account_dashboard.waitlist_leaving")
739
+ : tr("forge.account_dashboard.leave_waitlist")}
672
740
  </button>
673
741
  </div>
674
742
  );
@@ -700,13 +768,16 @@ function WaitlistPlaceRow({
700
768
  function BookingsTab({ ctx }: { ctx: TabContext }) {
701
769
  const { user } = usePublicAuth();
702
770
  const { t, card } = useCardStyles();
771
+ const tr = useForgeT();
703
772
  const { data, isLoading } = useMyBookings(user?.id);
704
773
 
705
774
  const bookings = data?.data ?? [];
706
775
 
707
776
  return (
708
777
  <div style={card}>
709
- <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>My Sessions</h2>
778
+ <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
779
+ {tr("forge.account_dashboard.sessions_title")}
780
+ </h2>
710
781
  {isLoading ? (
711
782
  <Loading />
712
783
  ) : bookings.length > 0 ? (
@@ -716,14 +787,14 @@ function BookingsTab({ ctx }: { ctx: TabContext }) {
716
787
  ))}
717
788
  </div>
718
789
  ) : (
719
- <p style={{ fontSize: 14, opacity: 0.7 }}>You have no sessions booked.</p>
790
+ <p style={{ fontSize: 14, opacity: 0.7 }}>{tr("forge.account_dashboard.sessions_empty")}</p>
720
791
  )}
721
792
  </div>
722
793
  );
723
794
  }
724
795
 
725
796
  /** The session's window, in the coach's timezone where one is set. */
726
- function formatSessionWindow(booking: MyBooking): string {
797
+ function formatSessionWindow(booking: MyBooking, tr: ForgeT): string {
727
798
  const zone = booking.coachingProductTimezone ?? undefined;
728
799
  const start = new Date(booking.sessionStartTime);
729
800
  const end = new Date(booking.sessionEndTime);
@@ -736,7 +807,9 @@ function formatSessionWindow(booking: MyBooking): string {
736
807
  });
737
808
  const from = start.toLocaleString(undefined, { hour: "numeric", minute: "2-digit", timeZone: zone });
738
809
  const to = end.toLocaleString(undefined, { hour: "numeric", minute: "2-digit", timeZone: zone });
739
- return `${day}, ${from} to ${to}${zone ? ` (${zone})` : ""}`;
810
+ return zone
811
+ ? tr("forge.account_dashboard.session_window_zoned", { day, start: from, end: to, timezone: zone })
812
+ : tr("forge.account_dashboard.session_window", { day, start: from, end: to });
740
813
  }
741
814
 
742
815
  /**
@@ -766,12 +839,13 @@ function BookingLocationLine({
766
839
  location: MyBooking["location"];
767
840
  primary: string;
768
841
  }) {
842
+ const tr = useForgeT();
769
843
  if (!location) return null;
770
844
 
771
845
  if (location.type === "online") {
772
846
  return (
773
847
  <div style={{ fontSize: 13, marginTop: 4 }}>
774
- <span style={{ opacity: 0.7 }}>Join: </span>
848
+ <span style={{ opacity: 0.7 }}>{tr("forge.account_dashboard.booking_join_label")}{" "}</span>
775
849
  <a
776
850
  href={location.url}
777
851
  target="_blank"
@@ -786,7 +860,7 @@ function BookingLocationLine({
786
860
 
787
861
  return (
788
862
  <div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>
789
- <span style={{ fontWeight: 600 }}>Where: </span>
863
+ <span style={{ fontWeight: 600 }}>{tr("forge.account_dashboard.booking_where_label")}{" "}</span>
790
864
  {location.address}
791
865
  </div>
792
866
  );
@@ -794,6 +868,7 @@ function BookingLocationLine({
794
868
 
795
869
  function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
796
870
  const { t } = useCardStyles();
871
+ const tr = useForgeT();
797
872
  const { formatAmount, currency } = ctx;
798
873
  const cancelBooking = useCancelMyBooking();
799
874
  const [picking, setPicking] = useState(false);
@@ -820,7 +895,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
820
895
  // wording, not ours.
821
896
  const message =
822
897
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
823
- "We could not cancel this session. Please try again.";
898
+ tr("forge.account_dashboard.booking_cancel_error");
824
899
  setError(message);
825
900
  }
826
901
  };
@@ -837,14 +912,18 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
837
912
  <div style={{ display: "flex", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
838
913
  <div>
839
914
  <div style={{ fontWeight: 600 }}>{booking.coachingProductTitle}</div>
840
- <div style={{ fontSize: 13, opacity: 0.7 }}>{formatSessionWindow(booking)}</div>
915
+ <div style={{ fontSize: 13, opacity: 0.7 }}>{formatSessionWindow(booking, tr)}</div>
841
916
  {booking.coachingProductDurationMinutes != null && (
842
- <div style={{ fontSize: 13, opacity: 0.7 }}>{booking.coachingProductDurationMinutes} minutes</div>
917
+ <div style={{ fontSize: 13, opacity: 0.7 }}>
918
+ {tr("forge.account_dashboard.booking_duration", { minutes: booking.coachingProductDurationMinutes })}
919
+ </div>
843
920
  )}
844
921
  <BookingLocationLine location={booking.location} primary={t.primary} />
845
922
  </div>
846
923
  <div style={{ textAlign: "right" }}>
847
- {cancelled && <div style={{ fontSize: 13, color: t.primary }}>Cancelled</div>}
924
+ {cancelled && (
925
+ <div style={{ fontSize: 13, color: t.primary }}>{tr("forge.account_dashboard.cancelled")}</div>
926
+ )}
848
927
  {booking.totalAmount != null && (
849
928
  <div style={{ fontWeight: 700, color: t.primary }}>
850
929
  {formatAmount(booking.totalAmount, booking.currency || currency)}
@@ -888,7 +967,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
888
967
  <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
889
968
  {booking.canReschedule && !picking && (
890
969
  <button onClick={() => setPicking(true)} style={actionButton}>
891
- Reschedule
970
+ {tr("forge.account_dashboard.reschedule")}
892
971
  </button>
893
972
  )}
894
973
  {booking.cancellation.canCancel && (
@@ -897,7 +976,9 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
897
976
  disabled={cancelBooking.isPending}
898
977
  style={{ ...actionButton, cursor: cancelBooking.isPending ? "wait" : "pointer" }}
899
978
  >
900
- {cancelBooking.isPending ? "Cancelling…" : "Cancel session"}
979
+ {cancelBooking.isPending
980
+ ? tr("forge.account_dashboard.cancelling")
981
+ : tr("forge.account_dashboard.cancel_session")}
901
982
  </button>
902
983
  )}
903
984
  </div>
@@ -919,6 +1000,7 @@ function BookingRow({ booking, ctx }: { booking: MyBooking; ctx: TabContext }) {
919
1000
  */
920
1001
  function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone: () => void }) {
921
1002
  const { t } = useCardStyles();
1003
+ const tr = useForgeT();
922
1004
  const reschedule = useRescheduleBooking();
923
1005
  const [error, setError] = useState<string | null>(null);
924
1006
  const [pendingSlotId, setPendingSlotId] = useState<string | null>(null);
@@ -943,7 +1025,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
943
1025
  } catch (err) {
944
1026
  const message =
945
1027
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
946
- "We could not move this session. Please try another time.";
1028
+ tr("forge.account_dashboard.reschedule_error");
947
1029
  setError(message);
948
1030
  } finally {
949
1031
  setPendingSlotId(null);
@@ -955,7 +1037,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
955
1037
  return (
956
1038
  <div style={{ marginTop: 12, padding: 12, borderRadius: t.cornerRadius, border: `1px solid ${t.primary}4d` }}>
957
1039
  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12 }}>
958
- <strong style={{ fontSize: 14 }}>Choose a new time</strong>
1040
+ <strong style={{ fontSize: 14 }}>{tr("forge.account_dashboard.reschedule_title")}</strong>
959
1041
  <button
960
1042
  onClick={onDone}
961
1043
  style={{
@@ -968,7 +1050,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
968
1050
  fontSize: 13,
969
1051
  }}
970
1052
  >
971
- Close
1053
+ {tr("forge.account_dashboard.close")}
972
1054
  </button>
973
1055
  </div>
974
1056
 
@@ -1001,7 +1083,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
1001
1083
  }}
1002
1084
  >
1003
1085
  {busy
1004
- ? "Moving…"
1086
+ ? tr("forge.account_dashboard.reschedule_moving")
1005
1087
  : start.toLocaleString(undefined, {
1006
1088
  weekday: "short",
1007
1089
  day: "numeric",
@@ -1016,7 +1098,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
1016
1098
  </div>
1017
1099
  ) : (
1018
1100
  <p style={{ fontSize: 13, opacity: 0.7, marginTop: 12 }}>
1019
- No other times are open in the next two weeks.
1101
+ {tr("forge.account_dashboard.reschedule_empty")}
1020
1102
  </p>
1021
1103
  )}
1022
1104
  </div>
@@ -1028,6 +1110,7 @@ function RescheduleSlotPicker({ booking, onDone }: { booking: MyBooking; onDone:
1028
1110
  function OrdersTab({ ctx }: { ctx: TabContext }) {
1029
1111
  const { user } = usePublicAuth();
1030
1112
  const { t, card } = useCardStyles();
1113
+ const tr = useForgeT();
1031
1114
  const { formatAmount, formatDate, currency } = ctx;
1032
1115
  const { data: orders, isLoading } = useUserOrders(user?.id);
1033
1116
 
@@ -1035,7 +1118,9 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
1035
1118
 
1036
1119
  return (
1037
1120
  <div style={card}>
1038
- <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>My Orders</h2>
1121
+ <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
1122
+ {tr("forge.account_dashboard.orders_title")}
1123
+ </h2>
1039
1124
  {isLoading ? (
1040
1125
  <Loading />
1041
1126
  ) : orders && orders.length > 0 ? (
@@ -1046,7 +1131,9 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
1046
1131
  <div key={order.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
1047
1132
  <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12 }}>
1048
1133
  <div>
1049
- <div style={{ fontWeight: 600 }}>Order #{order.id}</div>
1134
+ <div style={{ fontWeight: 600 }}>
1135
+ {tr("forge.account_dashboard.order_number", { order_id: order.id })}
1136
+ </div>
1050
1137
  <div style={{ fontSize: 13, opacity: 0.7 }}>{formatDate(order.createdAt)}</div>
1051
1138
  </div>
1052
1139
  <span style={{ fontSize: 13, color: t.primary }}>{formatStatus(order.status)}</span>
@@ -1055,7 +1142,7 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
1055
1142
  group.items.map((item, i) => (
1056
1143
  <div key={`${item.productVariantId}-${i}`} style={{ display: "flex", justifyContent: "space-between", fontSize: 14, padding: "4px 0" }}>
1057
1144
  <span>
1058
- {item.title} × {item.quantity}
1145
+ {tr("forge.account_dashboard.order_line", { title: item.title, quantity: item.quantity })}
1059
1146
  {/* Which version. Two versions of one release carry the
1060
1147
  same title, so without this a buyer's own order
1061
1148
  history shows what looks like the same thing bought
@@ -1068,14 +1155,14 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
1068
1155
  )),
1069
1156
  )}
1070
1157
  <div style={{ display: "flex", justifyContent: "space-between", fontWeight: 700, marginTop: 12, paddingTop: 12, borderTop: `1px solid ${t.primary}20` }}>
1071
- <span>Total</span>
1158
+ <span>{tr("forge.account_dashboard.total")}</span>
1072
1159
  <span style={{ color: t.primary }}>{formatAmount(order.totalAmount, order.currency || currency)}</span>
1073
1160
  </div>
1074
1161
  </div>
1075
1162
  ))}
1076
1163
  </div>
1077
1164
  ) : (
1078
- <p style={{ opacity: 0.8 }}>You haven&apos;t placed any orders yet.</p>
1165
+ <p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.orders_empty")}</p>
1079
1166
  )}
1080
1167
  </div>
1081
1168
  );
@@ -1086,12 +1173,15 @@ function OrdersTab({ ctx }: { ctx: TabContext }) {
1086
1173
  function SavedTab() {
1087
1174
  const { user } = usePublicAuth();
1088
1175
  const { t, card } = useCardStyles();
1176
+ const tr = useForgeT();
1089
1177
  const { data, isLoading } = useSavedPosts(user?.id);
1090
1178
  const posts = data?.data;
1091
1179
 
1092
1180
  return (
1093
1181
  <div style={card}>
1094
- <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>Saved Posts</h2>
1182
+ <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
1183
+ {tr("forge.account_dashboard.saved_title")}
1184
+ </h2>
1095
1185
  {isLoading ? (
1096
1186
  <Loading />
1097
1187
  ) : posts && posts.length > 0 ? (
@@ -1100,13 +1190,13 @@ function SavedTab() {
1100
1190
  <div key={post.id} style={{ border: `1px solid ${t.primary}20`, borderRadius: t.cornerRadius, padding: 16 }}>
1101
1191
  <p style={{ fontSize: 14, marginBottom: 8 }}>{post.caption}</p>
1102
1192
  <p style={{ fontSize: 12, opacity: 0.6 }}>
1103
- {post.likes} likes · {post.comments} comments
1193
+ {tr("forge.account_dashboard.saved_post_stats", { likes: post.likes, comments: post.comments })}
1104
1194
  </p>
1105
1195
  </div>
1106
1196
  ))}
1107
1197
  </div>
1108
1198
  ) : (
1109
- <p style={{ opacity: 0.8 }}>You haven&apos;t saved any posts yet.</p>
1199
+ <p style={{ opacity: 0.8 }}>{tr("forge.account_dashboard.saved_empty")}</p>
1110
1200
  )}
1111
1201
  </div>
1112
1202
  );
@@ -1117,6 +1207,7 @@ function SavedTab() {
1117
1207
  function AccountTab({ ctx }: { ctx: TabContext }) {
1118
1208
  const { user, refetchUser, logout } = usePublicAuth();
1119
1209
  const { t, card, button, ghostButton, input } = useCardStyles();
1210
+ const tr = useForgeT();
1120
1211
  const { formatDate } = ctx;
1121
1212
 
1122
1213
  const updateAccount = useUpdateAccount();
@@ -1144,22 +1235,22 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
1144
1235
  try {
1145
1236
  await updateAccount.mutateAsync({ firstName, lastName });
1146
1237
  await refetchUser();
1147
- setProfileMsg("Profile updated.");
1238
+ setProfileMsg(tr("forge.account_dashboard.profile_update_success"));
1148
1239
  } catch {
1149
- setProfileMsg("Failed to update profile.");
1240
+ setProfileMsg(tr("forge.account_dashboard.profile_update_error"));
1150
1241
  }
1151
1242
  };
1152
1243
 
1153
1244
  const onChangePassword = async (e: React.FormEvent) => {
1154
1245
  e.preventDefault();
1155
1246
  setPasswordMsg("");
1156
- if (newPassword.length < 8) return setPasswordMsg("New password must be at least 8 characters.");
1157
- if (newPassword !== confirmPassword) return setPasswordMsg("Passwords don't match.");
1247
+ if (newPassword.length < 8) return setPasswordMsg(tr("forge.account_dashboard.password_too_short"));
1248
+ if (newPassword !== confirmPassword) return setPasswordMsg(tr("forge.account_dashboard.password_mismatch"));
1158
1249
  try {
1159
1250
  await changePassword.mutateAsync({ currentPassword, newPassword });
1160
1251
  logout();
1161
1252
  } catch {
1162
- setPasswordMsg("Failed to update password.");
1253
+ setPasswordMsg(tr("forge.account_dashboard.password_update_error"));
1163
1254
  }
1164
1255
  };
1165
1256
 
@@ -1185,81 +1276,83 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
1185
1276
  <div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
1186
1277
  {/* Profile */}
1187
1278
  <form onSubmit={onUpdateProfile} style={card}>
1188
- <h2 style={sectionTitle}>Profile Information</h2>
1279
+ <h2 style={sectionTitle}>{tr("forge.account_dashboard.profile_section_title")}</h2>
1189
1280
  <div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
1190
1281
  <div style={{ flex: 1 }}>
1191
- <label style={label}>First name</label>
1282
+ <label style={label}>{tr("forge.account_dashboard.first_name")}</label>
1192
1283
  <input value={firstName} onChange={(e) => setFirstName(e.target.value)} style={input} />
1193
1284
  </div>
1194
1285
  <div style={{ flex: 1 }}>
1195
- <label style={label}>Last name</label>
1286
+ <label style={label}>{tr("forge.account_dashboard.last_name")}</label>
1196
1287
  <input value={lastName} onChange={(e) => setLastName(e.target.value)} style={input} />
1197
1288
  </div>
1198
1289
  </div>
1199
- <p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>Email: {user?.email} (cannot be changed)</p>
1290
+ <p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
1291
+ {tr("forge.account_dashboard.email_readonly", { email: user?.email ?? "" })}
1292
+ </p>
1200
1293
  {profileMsg && <p style={{ fontSize: 13, marginBottom: 12 }}>{profileMsg}</p>}
1201
1294
  <button type="submit" style={button}>
1202
- Update Profile
1295
+ {tr("forge.account_dashboard.update_profile")}
1203
1296
  </button>
1204
1297
  </form>
1205
1298
 
1206
1299
  {/* Password */}
1207
1300
  <form onSubmit={onChangePassword} style={card}>
1208
- <h2 style={sectionTitle}>Change Password</h2>
1301
+ <h2 style={sectionTitle}>{tr("forge.account_dashboard.password_section_title")}</h2>
1209
1302
  {passwordMsg && <p style={{ color: "#ef4444", fontSize: 13, marginBottom: 12 }}>{passwordMsg}</p>}
1210
1303
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
1211
1304
  <div>
1212
- <label style={label}>Current password</label>
1305
+ <label style={label}>{tr("forge.account_dashboard.current_password")}</label>
1213
1306
  <input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} style={input} />
1214
1307
  </div>
1215
1308
  <div style={{ display: "flex", gap: 12 }}>
1216
1309
  <div style={{ flex: 1 }}>
1217
- <label style={label}>New password</label>
1310
+ <label style={label}>{tr("forge.account_dashboard.new_password")}</label>
1218
1311
  <input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} style={input} />
1219
1312
  </div>
1220
1313
  <div style={{ flex: 1 }}>
1221
- <label style={label}>Confirm new password</label>
1314
+ <label style={label}>{tr("forge.account_dashboard.confirm_password")}</label>
1222
1315
  <input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} style={input} />
1223
1316
  </div>
1224
1317
  </div>
1225
1318
  </div>
1226
1319
  <button type="submit" style={{ ...button, marginTop: 16 }}>
1227
- Update Password
1320
+ {tr("forge.account_dashboard.update_password")}
1228
1321
  </button>
1229
1322
  </form>
1230
1323
 
1231
1324
  {/* Data rights */}
1232
1325
  <div style={card}>
1233
- <h2 style={sectionTitle}>Your Data</h2>
1234
- <p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>Export a copy of your personal data.</p>
1326
+ <h2 style={sectionTitle}>{tr("forge.account_dashboard.data_section_title")}</h2>
1327
+ <p style={{ fontSize: 13, opacity: 0.7, marginBottom: 12 }}>
1328
+ {tr("forge.account_dashboard.data_export_description")}
1329
+ </p>
1235
1330
  <button onClick={onExport} style={ghostButton}>
1236
- Export My Data
1331
+ {tr("forge.account_dashboard.export_data")}
1237
1332
  </button>
1238
1333
  </div>
1239
1334
 
1240
1335
  {/* Delete account */}
1241
1336
  <div style={{ ...card, borderColor: "#ef444533" }}>
1242
- <h2 style={{ ...sectionTitle, color: "#ef4444" }}>Delete Account</h2>
1337
+ <h2 style={{ ...sectionTitle, color: "#ef4444" }}>{tr("forge.account_dashboard.delete_section_title")}</h2>
1243
1338
  {scheduledAt ? (
1244
1339
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
1245
1340
  <p style={{ fontSize: 14 }}>
1246
- Account deletion scheduled for {formatDate(scheduledAt)}. You can cancel before then.
1341
+ {tr("forge.account_dashboard.deletion_scheduled", { date: formatDate(scheduledAt) })}
1247
1342
  </p>
1248
1343
  <button onClick={() => cancelDeletion.mutate()} style={ghostButton}>
1249
- Cancel Deletion
1344
+ {tr("forge.account_dashboard.cancel_deletion")}
1250
1345
  </button>
1251
1346
  </div>
1252
1347
  ) : confirmDelete ? (
1253
1348
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
1254
- <p style={{ fontSize: 14 }}>
1255
- Your account will be scheduled for permanent deletion after a 30-day cooling-off period.
1256
- </p>
1349
+ <p style={{ fontSize: 14 }}>{tr("forge.account_dashboard.deletion_confirm_body")}</p>
1257
1350
  <div style={{ display: "flex", gap: 12 }}>
1258
1351
  <button onClick={() => requestDeletion.mutate()} style={{ ...button, background: "#ef4444" }}>
1259
- Yes, delete my account
1352
+ {tr("forge.account_dashboard.deletion_confirm")}
1260
1353
  </button>
1261
1354
  <button onClick={() => setConfirmDelete(false)} style={ghostButton}>
1262
- Cancel
1355
+ {tr("forge.account_dashboard.cancel")}
1263
1356
  </button>
1264
1357
  </div>
1265
1358
  </div>
@@ -1268,7 +1361,7 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
1268
1361
  onClick={() => setConfirmDelete(true)}
1269
1362
  style={{ padding: "8px 14px", borderRadius: t.cornerRadius, background: "#ef444411", color: "#ef4444", border: "1px solid #ef444433", cursor: "pointer" }}
1270
1363
  >
1271
- Request Account Deletion
1364
+ {tr("forge.account_dashboard.request_deletion")}
1272
1365
  </button>
1273
1366
  )}
1274
1367
  </div>
@@ -1280,6 +1373,7 @@ function AccountTab({ ctx }: { ctx: TabContext }) {
1280
1373
 
1281
1374
  function NotificationsTab() {
1282
1375
  const { t, card } = useCardStyles();
1376
+ const tr = useForgeT();
1283
1377
  const { data } = useNotificationPreferences();
1284
1378
  const updatePreference = useUpdateNotificationPreference();
1285
1379
 
@@ -1341,10 +1435,20 @@ function NotificationsTab() {
1341
1435
 
1342
1436
  return (
1343
1437
  <div style={card}>
1344
- <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>Notification Types</h2>
1438
+ <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, fontFamily: t.headingFontFamily }}>
1439
+ {tr("forge.account_dashboard.notifications_title")}
1440
+ </h2>
1345
1441
  <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
1346
- <Row k="newPosts" title="New Posts" desc="Get notified when new posts are published" />
1347
- <Row k="chatMessages" title="Chat Messages" desc="Get notified when new chat messages arrive" />
1442
+ <Row
1443
+ k="newPosts"
1444
+ title={tr("forge.account_dashboard.notify_new_posts_title")}
1445
+ desc={tr("forge.account_dashboard.notify_new_posts_description")}
1446
+ />
1447
+ <Row
1448
+ k="chatMessages"
1449
+ title={tr("forge.account_dashboard.notify_chat_title")}
1450
+ desc={tr("forge.account_dashboard.notify_chat_description")}
1451
+ />
1348
1452
  </div>
1349
1453
  </div>
1350
1454
  );