@tribe-nest/forge 3.26.0 → 3.31.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 (129) hide show
  1. package/package.json +6 -3
  2. package/src/_tests/publishedResolvability.spec.ts +184 -0
  3. package/src/_tests/specsRunWorkspaceSource.spec.ts +116 -0
  4. package/src/_tests/workspaceAliases.ts +40 -0
  5. package/src/client/_tests/localeHeader.spec.tsx +98 -0
  6. package/src/client/activeLocale.ts +34 -0
  7. package/src/client/createForgeClient.ts +13 -0
  8. package/src/contexts/CartContext.tsx +76 -0
  9. package/src/contexts/PublicAuthContext.tsx +34 -5
  10. package/src/contexts/_tests/CartContext.spec.tsx +210 -0
  11. package/src/contexts/_tests/PublicAuthRefetch.spec.tsx +147 -0
  12. package/src/data/queries/useBroadcasts.ts +151 -0
  13. package/src/data/queries/useCheckouts.ts +83 -0
  14. package/src/data/queries/useMyBookings.ts +9 -1
  15. package/src/data/queries/useWebsite.ts +7 -0
  16. package/src/i18n/CONVENTION.md +184 -0
  17. package/src/i18n/_tests/translationKeys.spec.ts +153 -0
  18. package/src/i18n/de.json +1019 -0
  19. package/src/i18n/en.json +1019 -0
  20. package/src/i18n/index.ts +125 -0
  21. package/src/index.ts +21 -0
  22. package/src/provider/ForgeAppProvider.tsx +2 -1
  23. package/src/provider/SiteConfigProvider.tsx +11 -0
  24. package/src/ui/format/_tests/membershipPwyw.spec.ts +185 -0
  25. package/src/ui/format/_tests/pwyw.spec.ts +65 -8
  26. package/src/ui/format/membershipPwyw.ts +164 -0
  27. package/src/ui/format/pwyw.ts +37 -0
  28. package/src/ui/headless/broadcast/_tests/broadcastState.spec.ts +235 -0
  29. package/src/ui/headless/broadcast/broadcastState.ts +158 -0
  30. package/src/ui/headless/broadcast/useBroadcastWatch.ts +174 -21
  31. package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
  32. package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
  33. package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
  34. package/src/ui/headless/checkout/useCartResume.ts +340 -0
  35. package/src/ui/headless/checkout/useCheckout.ts +9 -2
  36. package/src/ui/headless/event/useEventCheckout.ts +8 -13
  37. package/src/ui/headless/index.ts +32 -0
  38. package/src/ui/headless/membership/useMembershipCheckout.ts +160 -32
  39. package/src/ui/index.ts +38 -0
  40. package/src/ui/media/CallHelpHint.tsx +87 -0
  41. package/src/ui/media/CallStage.tsx +542 -0
  42. package/src/ui/media/_tests/CallStage.spec.tsx +685 -0
  43. package/src/ui/media/_tests/bookingSession.spec.tsx +179 -0
  44. package/src/ui/media/_tests/callState.spec.ts +452 -0
  45. package/src/ui/media/_tests/fakeNode.ts +178 -0
  46. package/src/ui/media/bookingSession.tsx +194 -0
  47. package/src/ui/media/callState.ts +341 -0
  48. package/src/ui/media/index.ts +135 -0
  49. package/src/ui/shell/TribeNestApp.tsx +36 -27
  50. package/src/ui/styled/AccountDashboard.tsx +289 -96
  51. package/src/ui/styled/AiAgentWidget.tsx +10 -6
  52. package/src/ui/styled/AudioPlayer.tsx +14 -6
  53. package/src/ui/styled/BlogCategory.tsx +4 -2
  54. package/src/ui/styled/BlogComments.tsx +27 -20
  55. package/src/ui/styled/BlogList.tsx +5 -3
  56. package/src/ui/styled/BlogPost.tsx +14 -11
  57. package/src/ui/styled/BroadcastWatch.tsx +107 -0
  58. package/src/ui/styled/BundleConfirmation.tsx +32 -23
  59. package/src/ui/styled/CancellationTerms.tsx +3 -1
  60. package/src/ui/styled/Cart.tsx +32 -14
  61. package/src/ui/styled/ChatRoom.tsx +65 -20
  62. package/src/ui/styled/Checkout.tsx +151 -89
  63. package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
  64. package/src/ui/styled/CoachingBooking.tsx +71 -44
  65. package/src/ui/styled/CoachingConfirmation.tsx +56 -19
  66. package/src/ui/styled/CoachingDetail.tsx +15 -11
  67. package/src/ui/styled/CohortPage.tsx +10 -8
  68. package/src/ui/styled/ConfirmSubscription.tsx +14 -8
  69. package/src/ui/styled/ContactForm.tsx +31 -12
  70. package/src/ui/styled/CookieConsent.tsx +19 -15
  71. package/src/ui/styled/CourseAccess.tsx +54 -47
  72. package/src/ui/styled/CourseCheckout.tsx +56 -38
  73. package/src/ui/styled/CourseConfirmation.tsx +39 -17
  74. package/src/ui/styled/CourseDetail.tsx +21 -15
  75. package/src/ui/styled/CoursesGrid.tsx +4 -2
  76. package/src/ui/styled/CurrencySwitcher.tsx +4 -1
  77. package/src/ui/styled/DiscountCode.tsx +22 -18
  78. package/src/ui/styled/DonationButton.tsx +52 -17
  79. package/src/ui/styled/DonationPage.tsx +4 -4
  80. package/src/ui/styled/EmailListForm.tsx +22 -9
  81. package/src/ui/styled/EventConfirmation.tsx +37 -14
  82. package/src/ui/styled/EventCountdown.tsx +10 -6
  83. package/src/ui/styled/EventDetail.tsx +8 -6
  84. package/src/ui/styled/EventSeriesDetail.tsx +18 -13
  85. package/src/ui/styled/EventTickets.tsx +232 -108
  86. package/src/ui/styled/EventWaitlist.tsx +42 -33
  87. package/src/ui/styled/EventsList.tsx +15 -13
  88. package/src/ui/styled/ForgotPasswordForm.tsx +17 -12
  89. package/src/ui/styled/FormRenderer.tsx +12 -7
  90. package/src/ui/styled/InstallBanner.tsx +21 -18
  91. package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
  92. package/src/ui/styled/InvoicePayment.tsx +37 -16
  93. package/src/ui/styled/LeadMagnet.tsx +9 -7
  94. package/src/ui/styled/LiveBroadcastList.tsx +171 -0
  95. package/src/ui/styled/Loading.tsx +7 -1
  96. package/src/ui/styled/LoginForm.tsx +32 -14
  97. package/src/ui/styled/MembershipCheckout.tsx +341 -49
  98. package/src/ui/styled/MembershipTiers.tsx +34 -18
  99. package/src/ui/styled/OfferButton.tsx +33 -13
  100. package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
  101. package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
  102. package/src/ui/styled/PodcastEpisode.tsx +20 -9
  103. package/src/ui/styled/PodcastList.tsx +9 -3
  104. package/src/ui/styled/PodcastShow.tsx +29 -7
  105. package/src/ui/styled/PostsFeed.tsx +10 -8
  106. package/src/ui/styled/PresaleCode.tsx +8 -11
  107. package/src/ui/styled/ProductBrowseNav.tsx +21 -11
  108. package/src/ui/styled/ProductDetail.tsx +58 -28
  109. package/src/ui/styled/ReactionBar.tsx +4 -2
  110. package/src/ui/styled/ReplayList.tsx +24 -13
  111. package/src/ui/styled/ResetPasswordForm.tsx +32 -15
  112. package/src/ui/styled/ResumeCart.tsx +289 -0
  113. package/src/ui/styled/ReviewForm.tsx +52 -58
  114. package/src/ui/styled/SignupForm.tsx +34 -16
  115. package/src/ui/styled/TicketTransfer.tsx +55 -54
  116. package/src/ui/styled/UserMenu.tsx +16 -14
  117. package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +134 -0
  118. package/src/ui/styled/_tests/BroadcastPassValidation.spec.tsx +125 -0
  119. package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +364 -0
  120. package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
  121. package/src/ui/styled/broadcast/BroadcastPassValidation.tsx +187 -0
  122. package/src/ui/styled/broadcast/BroadcastPlayer.tsx +536 -0
  123. package/src/ui/styled/broadcast/BroadcastTicketPurchase.tsx +74 -0
  124. package/src/ui/styled/broadcast/EndedBroadcast.tsx +103 -0
  125. package/src/ui/styled/community/CommunityComposer.tsx +182 -3
  126. package/src/ui/styled/community/CommunityFeed.tsx +36 -51
  127. package/src/ui/styled/community/CommunityPostDetail.tsx +16 -2
  128. package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +281 -0
  129. package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +175 -0
@@ -4,6 +4,7 @@ import { CreditCard, MapPin, Package, User } from "lucide-react";
4
4
  import { useCheckout, CHECKOUT_STAGE_ORDER, type CheckoutStage } from "../headless/checkout/useCheckout";
5
5
  import type { SelectedShippingRate } from "../headless/checkout/useCheckout";
6
6
  import type { ShippingRate, ShippingGroup } from "../../data/queries/useShipping";
7
+ import { useForgeT } from "../../i18n";
7
8
  import { useForgeTheme } from "../theme/ForgeThemeProvider";
8
9
  import { readableTextOn } from "../theme/contrast";
9
10
  import { useFormatCurrency } from "../format/useFormatCurrency";
@@ -64,6 +65,8 @@ export function Checkout({
64
65
  style,
65
66
  }: CheckoutProps) {
66
67
  const theme = useForgeTheme();
68
+ // `tr`, not `t`: a ticket line below binds `t` as its own map callback param.
69
+ const tr = useForgeT();
67
70
  const { formatCurrency } = useFormatCurrency();
68
71
  // Store display mode (site config) — drives the pre-quote "incl. tax" caption.
69
72
  // Once the payment starts, the authoritative taxQuote from start-payment wins.
@@ -187,12 +190,12 @@ export function Checkout({
187
190
  >
188
191
  <div className="text-center py-12">
189
192
  <h1 className="text-2xl font-bold mb-4" style={{ color: theme.colors.text }}>
190
- Your cart is empty
193
+ {tr("forge.checkout.empty_title")}
191
194
  </h1>
192
195
  <p className="mb-6" style={{ color: `${theme.colors.text}${alpha(0.7)}` }}>
193
- Add some items to your cart before checking out.
196
+ {tr("forge.checkout.empty_body")}
194
197
  </p>
195
- <PrimaryButton text="Continue Shopping" onClick={() => go(storePath)} />
198
+ <PrimaryButton text={tr("forge.checkout.continue_shopping")} onClick={() => go(storePath)} />
196
199
  </div>
197
200
  </div>
198
201
  );
@@ -200,14 +203,14 @@ export function Checkout({
200
203
 
201
204
  const stages = c.hasPhysicalProduct
202
205
  ? [
203
- { key: "userDetails" as const, label: "User", icon: User },
204
- { key: "shippingAddress" as const, label: "Address", icon: MapPin },
205
- { key: "shippingRates" as const, label: "Shipping", icon: Package },
206
- { key: "payment" as const, label: "Payment", icon: CreditCard },
206
+ { key: "userDetails" as const, label: tr("forge.checkout.stage_user"), icon: User },
207
+ { key: "shippingAddress" as const, label: tr("forge.checkout.stage_address"), icon: MapPin },
208
+ { key: "shippingRates" as const, label: tr("forge.checkout.stage_shipping"), icon: Package },
209
+ { key: "payment" as const, label: tr("forge.checkout.stage_payment"), icon: CreditCard },
207
210
  ]
208
211
  : [
209
- { key: "userDetails" as const, label: "User", icon: User },
210
- { key: "payment" as const, label: "Payment", icon: CreditCard },
212
+ { key: "userDetails" as const, label: tr("forge.checkout.stage_user"), icon: User },
213
+ { key: "payment" as const, label: tr("forge.checkout.stage_payment"), icon: CreditCard },
211
214
  ];
212
215
 
213
216
  const currentIdx = stageIdx(c.currentStage);
@@ -272,29 +275,35 @@ export function Checkout({
272
275
  >
273
276
  <div className="pb-6" style={{ borderTop: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
274
277
  <h3 className="text-lg font-semibold mb-4" style={{ color: theme.colors.text }}>
275
- Already have an account?
278
+ {tr("forge.checkout.have_account")}
276
279
  </h3>
277
- <PrimaryButton text="Login" onClick={handleLogin} fullWidth />
280
+ <PrimaryButton text={tr("forge.checkout.login")} onClick={handleLogin} fullWidth />
278
281
  </div>
279
282
  <h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
280
- Or
283
+ {tr("forge.checkout.or")}
281
284
  </h2>
282
285
 
283
286
  <div className="mb-6">
284
287
  <h3 className="text-lg font-semibold mb-4" style={{ color: theme.colors.text }}>
285
- Continue as Guest
288
+ {tr("forge.checkout.guest_title")}
286
289
  </h3>
287
290
  <form onSubmit={guestForm.handleSubmit(c.submitGuest)} className="space-y-4">
288
291
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
289
292
  <div>
290
293
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
291
- First Name
294
+ {tr("forge.checkout.first_name")}
292
295
  </label>
293
296
  <Controller
294
297
  control={guestForm.control}
295
298
  name="firstName"
296
- rules={{ required: "First name is required" }}
297
- render={({ field }) => <input {...field} placeholder="First Name" style={inputStyle} />}
299
+ rules={{ required: tr("forge.checkout.first_name_required") }}
300
+ render={({ field }) => (
301
+ <input
302
+ {...field}
303
+ placeholder={tr("forge.checkout.first_name_placeholder")}
304
+ style={inputStyle}
305
+ />
306
+ )}
298
307
  />
299
308
  {guestForm.formState.errors.firstName && (
300
309
  <p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.firstName.message}</p>
@@ -302,13 +311,19 @@ export function Checkout({
302
311
  </div>
303
312
  <div>
304
313
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
305
- Last Name
314
+ {tr("forge.checkout.last_name")}
306
315
  </label>
307
316
  <Controller
308
317
  control={guestForm.control}
309
318
  name="lastName"
310
- rules={{ required: "Last name is required" }}
311
- render={({ field }) => <input {...field} placeholder="Last Name" style={inputStyle} />}
319
+ rules={{ required: tr("forge.checkout.last_name_required") }}
320
+ render={({ field }) => (
321
+ <input
322
+ {...field}
323
+ placeholder={tr("forge.checkout.last_name_placeholder")}
324
+ style={inputStyle}
325
+ />
326
+ )}
312
327
  />
313
328
  {guestForm.formState.errors.lastName && (
314
329
  <p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.lastName.message}</p>
@@ -317,16 +332,26 @@ export function Checkout({
317
332
  </div>
318
333
  <div>
319
334
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
320
- Email
335
+ {tr("forge.checkout.email")}
321
336
  </label>
322
337
  <Controller
323
338
  control={guestForm.control}
324
339
  name="email"
325
340
  rules={{
326
- required: "Email is required",
327
- pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message: "Invalid email address" },
341
+ required: tr("forge.checkout.email_required"),
342
+ pattern: {
343
+ value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
344
+ message: tr("forge.checkout.email_invalid"),
345
+ },
328
346
  }}
329
- render={({ field }) => <input {...field} type="email" placeholder="Email" style={inputStyle} />}
347
+ render={({ field }) => (
348
+ <input
349
+ {...field}
350
+ type="email"
351
+ placeholder={tr("forge.checkout.email_placeholder")}
352
+ style={inputStyle}
353
+ />
354
+ )}
330
355
  />
331
356
  {guestForm.formState.errors.email && (
332
357
  <p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.email.message}</p>
@@ -342,10 +367,10 @@ export function Checkout({
342
367
  style={{ accentColor: theme.colors.primary }}
343
368
  />
344
369
  <label htmlFor="joinEmailList" className="text-sm" style={{ color: theme.colors.text }}>
345
- Sign up to receive updates
370
+ {tr("forge.checkout.join_email_list")}
346
371
  </label>
347
372
  </div>
348
- <PrimaryButton text="Continue as Guest" type="submit" fullWidth />
373
+ <PrimaryButton text={tr("forge.checkout.guest_submit")} type="submit" fullWidth />
349
374
  </form>
350
375
  </div>
351
376
  </div>
@@ -362,18 +387,20 @@ export function Checkout({
362
387
  }}
363
388
  >
364
389
  <h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
365
- Shipping Address
390
+ {tr("forge.checkout.shipping_address_title")}
366
391
  </h2>
367
392
  <form onSubmit={shippingForm.handleSubmit(c.submitAddress)} className="space-y-4">
368
393
  <div>
369
394
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
370
- Address Line 1
395
+ {tr("forge.checkout.address1_label")}
371
396
  </label>
372
397
  <Controller
373
398
  control={shippingForm.control}
374
399
  name="address1"
375
- rules={{ required: "Address is required" }}
376
- render={({ field }) => <input {...field} placeholder="12 address avenue, Bankstown" style={inputStyle} />}
400
+ rules={{ required: tr("forge.checkout.address_required") }}
401
+ render={({ field }) => (
402
+ <input {...field} placeholder={tr("forge.checkout.address1_placeholder")} style={inputStyle} />
403
+ )}
377
404
  />
378
405
  {shippingForm.formState.errors.address1 && (
379
406
  <p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.address1.message}</p>
@@ -383,13 +410,15 @@ export function Checkout({
383
410
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
384
411
  <div>
385
412
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
386
- City
413
+ {tr("forge.checkout.city_label")}
387
414
  </label>
388
415
  <Controller
389
416
  control={shippingForm.control}
390
417
  name="city"
391
- rules={{ required: "City is required" }}
392
- render={({ field }) => <input {...field} placeholder="Sydney" style={inputStyle} />}
418
+ rules={{ required: tr("forge.checkout.city_required") }}
419
+ render={({ field }) => (
420
+ <input {...field} placeholder={tr("forge.checkout.city_placeholder")} style={inputStyle} />
421
+ )}
393
422
  />
394
423
  {shippingForm.formState.errors.city && (
395
424
  <p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.city.message}</p>
@@ -398,13 +427,15 @@ export function Checkout({
398
427
 
399
428
  <div>
400
429
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
401
- Postal Code
430
+ {tr("forge.checkout.zip_label")}
402
431
  </label>
403
432
  <Controller
404
433
  control={shippingForm.control}
405
434
  name="zip"
406
- rules={{ required: "Postal code is required" }}
407
- render={({ field }) => <input {...field} placeholder="2200" style={inputStyle} />}
435
+ rules={{ required: tr("forge.checkout.zip_required") }}
436
+ render={({ field }) => (
437
+ <input {...field} placeholder={tr("forge.checkout.zip_placeholder")} style={inputStyle} />
438
+ )}
408
439
  />
409
440
  {shippingForm.formState.errors.zip && (
410
441
  <p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.zip.message}</p>
@@ -415,15 +446,15 @@ export function Checkout({
415
446
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
416
447
  <div>
417
448
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
418
- Country
449
+ {tr("forge.checkout.country_label")}
419
450
  </label>
420
451
  <Controller
421
452
  control={shippingForm.control}
422
453
  name="countryCode"
423
- rules={{ required: "Country is required" }}
454
+ rules={{ required: tr("forge.checkout.country_required") }}
424
455
  render={({ field }) => (
425
456
  <select {...field} style={inputStyle}>
426
- <option value="">Select Country</option>
457
+ <option value="">{tr("forge.checkout.country_select")}</option>
427
458
  {c.countries.map((country) => (
428
459
  <option key={country.code} value={country.code}>
429
460
  {country.name}
@@ -440,15 +471,15 @@ export function Checkout({
440
471
  {selectedCountry?.states && selectedCountry.states.length > 0 && (
441
472
  <div>
442
473
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
443
- State/Province
474
+ {tr("forge.checkout.state_label")}
444
475
  </label>
445
476
  <Controller
446
477
  control={shippingForm.control}
447
478
  name="stateCode"
448
- rules={{ required: "State/Province is required" }}
479
+ rules={{ required: tr("forge.checkout.state_required") }}
449
480
  render={({ field }) => (
450
481
  <select {...field} style={inputStyle}>
451
- <option value="">Select State/Province</option>
482
+ <option value="">{tr("forge.checkout.state_select")}</option>
452
483
  {selectedCountry.states!.map((state) => (
453
484
  <option key={state.code} value={state.code}>
454
485
  {state.name}
@@ -466,13 +497,15 @@ export function Checkout({
466
497
 
467
498
  <div>
468
499
  <label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
469
- Phone Number
500
+ {tr("forge.checkout.phone_label")}
470
501
  </label>
471
502
  <Controller
472
503
  control={shippingForm.control}
473
504
  name="phone"
474
- rules={{ required: "Phone number is required" }}
475
- render={({ field }) => <input {...field} placeholder="Phone number" style={inputStyle} />}
505
+ rules={{ required: tr("forge.checkout.phone_required") }}
506
+ render={({ field }) => (
507
+ <input {...field} placeholder={tr("forge.checkout.phone_placeholder")} style={inputStyle} />
508
+ )}
476
509
  />
477
510
  {shippingForm.formState.errors.phone && (
478
511
  <p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.phone.message}</p>
@@ -480,9 +513,18 @@ export function Checkout({
480
513
  </div>
481
514
 
482
515
  <div className="flex gap-4 pt-4">
483
- {!c.user && <SecondaryButton text="Back" onClick={() => c.setCurrentStage("userDetails")} />}
516
+ {!c.user && (
517
+ <SecondaryButton
518
+ text={tr("forge.checkout.back")}
519
+ onClick={() => c.setCurrentStage("userDetails")}
520
+ />
521
+ )}
484
522
  <PrimaryButton
485
- text={c.isFetchingRates ? "Fetching shipping rates..." : "Continue to Shipping"}
523
+ text={
524
+ c.isFetchingRates
525
+ ? tr("forge.checkout.fetching_rates")
526
+ : tr("forge.checkout.continue_to_shipping")
527
+ }
486
528
  type="submit"
487
529
  fullWidth
488
530
  disabled={c.isFetchingRates}
@@ -503,7 +545,7 @@ export function Checkout({
503
545
  }}
504
546
  >
505
547
  <h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
506
- Shipping Options
548
+ {tr("forge.checkout.shipping_options_title")}
507
549
  </h2>
508
550
 
509
551
  <ShippingRateSelector
@@ -514,9 +556,12 @@ export function Checkout({
514
556
  />
515
557
 
516
558
  <div className="flex gap-4 pt-6">
517
- <SecondaryButton text="Back" onClick={() => c.setCurrentStage("shippingAddress")} />
559
+ <SecondaryButton
560
+ text={tr("forge.checkout.back")}
561
+ onClick={() => c.setCurrentStage("shippingAddress")}
562
+ />
518
563
  <PrimaryButton
519
- text="Continue to Payment"
564
+ text={tr("forge.checkout.continue_to_payment")}
520
565
  onClick={c.confirmRates}
521
566
  fullWidth
522
567
  disabled={c.shippingGroups.some((g) => g.selectable && !c.selections[g.id])}
@@ -550,12 +595,17 @@ export function Checkout({
550
595
  }}
551
596
  >
552
597
  <h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
553
- Payment Information
598
+ {tr("forge.checkout.payment_title")}
554
599
  </h2>
555
600
  <p className="text-sm mb-8" style={{ color: `${theme.colors.text}${alpha(0.7)}` }}>
556
- This is a free checkout. You will not be charged anything.
601
+ {tr("forge.checkout.free_checkout_body")}
557
602
  </p>
558
- <PrimaryButton text="Complete Checkout" onClick={handleFreeCheckout} fullWidth disabled={c.isFreeCheckoutLoading} />
603
+ <PrimaryButton
604
+ text={tr("forge.checkout.complete_checkout")}
605
+ onClick={handleFreeCheckout}
606
+ fullWidth
607
+ disabled={c.isFreeCheckoutLoading}
608
+ />
559
609
  </div>
560
610
  )}
561
611
  </div>
@@ -571,13 +621,13 @@ export function Checkout({
571
621
  }}
572
622
  >
573
623
  <h3 className="text-xl font-bold mb-4" style={{ color: theme.colors.text }}>
574
- Order Summary
624
+ {tr("forge.checkout.summary_title")}
575
625
  </h3>
576
626
 
577
627
  {/* Customer */}
578
628
  <div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
579
629
  <h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
580
- Customer
630
+ {tr("forge.checkout.summary_customer")}
581
631
  </h4>
582
632
  {c.user ? (
583
633
  <div className="text-sm">
@@ -595,7 +645,7 @@ export function Checkout({
595
645
  </div>
596
646
  ) : (
597
647
  <p className="text-sm" style={{ color: `${theme.colors.text}${alpha(0.5)}` }}>
598
- Guest checkout
648
+ {tr("forge.checkout.guest_checkout")}
599
649
  </p>
600
650
  )}
601
651
  </div>
@@ -604,7 +654,7 @@ export function Checkout({
604
654
  {c.hasPhysicalProduct && c.shippingData && (
605
655
  <div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
606
656
  <h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
607
- Shipping Address
657
+ {tr("forge.checkout.summary_shipping_address")}
608
658
  </h4>
609
659
  <div className="text-sm">
610
660
  <p style={{ color: theme.colors.text }}>{c.shippingData.address1}</p>
@@ -621,7 +671,7 @@ export function Checkout({
621
671
  {/* Cart Items */}
622
672
  <div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
623
673
  <h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
624
- Items
674
+ {tr("forge.checkout.summary_items")}
625
675
  </h4>
626
676
  <div className="space-y-2">
627
677
  {/* Ticket lines first: they are what the bundle is FOR, and the
@@ -636,13 +686,13 @@ export function Checkout({
636
686
  <div key={`${t.eventId}-${ticketId}`} className="flex justify-between text-sm" data-testid="summary-ticket-line">
637
687
  <div className="flex-1">
638
688
  <p className="font-medium" style={{ color: theme.colors.text }}>
639
- {meta?.title ?? "Ticket"}
689
+ {meta?.title ?? tr("forge.checkout.ticket_fallback")}
640
690
  </p>
641
691
  <p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
642
692
  {t.eventTitle}
643
693
  </p>
644
694
  <p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
645
- Qty: {qty}
695
+ {tr("forge.checkout.qty", { count: qty })}
646
696
  </p>
647
697
  </div>
648
698
  <p className="font-medium" style={{ color: theme.colors.text }}>
@@ -667,11 +717,11 @@ export function Checkout({
667
717
  <CartLineOptions item={item} accent={theme.colors.primary} size="small" />
668
718
  {item.isGift && (
669
719
  <p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
670
- Gift for {item.recipientName}
720
+ {tr("forge.checkout.gift_for", { recipient_name: item.recipientName ?? "" })}
671
721
  </p>
672
722
  )}
673
723
  <p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
674
- Qty: {item.quantity}
724
+ {tr("forge.checkout.qty", { count: item.quantity })}
675
725
  </p>
676
726
  </div>
677
727
  <p className="font-medium" style={{ color: theme.colors.text }}>
@@ -698,25 +748,25 @@ export function Checkout({
698
748
  style={{ color: theme.colors.primary }}
699
749
  onClick={() => c.setShowCouponInput(true)}
700
750
  >
701
- Have a coupon code?
751
+ {tr("forge.checkout.coupon_prompt")}
702
752
  </button>
703
753
  )}
704
754
  {c.showCouponInput && !c.appliedCoupon && c.isCouponEditable && (
705
755
  <div className="flex gap-2 items-end">
706
756
  <div className="flex-1">
707
757
  <p className="text-sm mb-1" style={{ color: theme.colors.text }}>
708
- Coupon Code
758
+ {tr("forge.checkout.coupon_label")}
709
759
  </p>
710
760
  <input
711
- aria-label="Coupon code"
712
- placeholder="Enter coupon code"
761
+ aria-label={tr("forge.checkout.aria_coupon_code")}
762
+ placeholder={tr("forge.checkout.coupon_placeholder")}
713
763
  style={inputStyle}
714
764
  value={c.couponCode}
715
765
  onChange={(e) => c.setCouponCode(e.target.value.toUpperCase())}
716
766
  />
717
767
  </div>
718
768
  <PrimaryButton
719
- text={c.isApplyingCoupon ? "..." : "Apply"}
769
+ text={c.isApplyingCoupon ? "..." : tr("forge.checkout.coupon_apply")}
720
770
  onClick={c.applyCoupon}
721
771
  disabled={c.isApplyingCoupon || !c.couponCode.trim()}
722
772
  />
@@ -726,7 +776,7 @@ export function Checkout({
726
776
  code is priced live, so promising "at payment" would be wrong. */}
727
777
  {c.isBundle && !c.checkoutId && c.showCouponInput && !c.appliedCoupon && c.isCouponEditable && (
728
778
  <p className="text-xs mt-1" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
729
- Your code is applied when you reach payment.
779
+ {tr("forge.checkout.coupon_deferred_note")}
730
780
  </p>
731
781
  )}
732
782
  {c.couponError && <p className="text-sm text-red-500 mt-1">{c.couponError}</p>}
@@ -737,19 +787,25 @@ export function Checkout({
737
787
  >
738
788
  <div>
739
789
  <p className="text-sm font-semibold" style={{ color: theme.colors.primary }}>
740
- Coupon applied: {c.appliedCoupon.code}
790
+ {tr("forge.checkout.coupon_applied", { code: c.appliedCoupon.code })}
741
791
  </p>
742
792
  {/* A bundle reports the cash that came off and the code it
743
793
  came off under, but not the rate — so say the cash,
744
794
  never a made-up "0% off". */}
745
795
  <p className="text-xs" style={{ color: theme.colors.text }}>
746
796
  {c.appliedCoupon.discountType === "percentage"
747
- ? `${c.appliedCoupon.discountValue}% off`
797
+ ? // `String(...)` keeps a null rate rendering exactly as the old template
798
+ // literal did, rather than quietly turning it into a made-up "0".
799
+ tr("forge.checkout.coupon_percent_off", { percent: String(c.appliedCoupon.discountValue) })
748
800
  : c.appliedCoupon.discountValue != null
749
- ? `${formatCurrency(c.appliedCoupon.discountValue)} off`
801
+ ? tr("forge.checkout.coupon_amount_off", {
802
+ amount: formatCurrency(c.appliedCoupon.discountValue),
803
+ })
750
804
  : c.appliedCoupon.discountAmount != null
751
- ? `${formatCurrency(c.appliedCoupon.discountAmount)} off`
752
- : "Applied at payment"}
805
+ ? tr("forge.checkout.coupon_amount_off", {
806
+ amount: formatCurrency(c.appliedCoupon.discountAmount),
807
+ })
808
+ : tr("forge.checkout.coupon_applied_at_payment")}
753
809
  </p>
754
810
  </div>
755
811
  {c.isCouponEditable && (
@@ -760,7 +816,7 @@ export function Checkout({
760
816
  onClick={c.removeCoupon}
761
817
  disabled={c.isApplyingCoupon}
762
818
  >
763
- Remove
819
+ {tr("forge.checkout.coupon_remove")}
764
820
  </button>
765
821
  )}
766
822
  </div>
@@ -771,18 +827,18 @@ export function Checkout({
771
827
  {/* Totals */}
772
828
  <div className="space-y-2">
773
829
  <div className="flex justify-between">
774
- <span style={{ color: theme.colors.text }}>Subtotal</span>
830
+ <span style={{ color: theme.colors.text }}>{tr("forge.checkout.subtotal")}</span>
775
831
  <span style={{ color: theme.colors.text }}>{formatCurrency(c.subTotal)}</span>
776
832
  </div>
777
833
  {c.discountAmount > 0 && (
778
834
  <div className="flex justify-between">
779
- <span style={{ color: theme.colors.text }}>Discount</span>
835
+ <span style={{ color: theme.colors.text }}>{tr("forge.checkout.discount")}</span>
780
836
  <span style={{ color: theme.colors.primary }}>-{formatCurrency(c.discountAmount)}</span>
781
837
  </div>
782
838
  )}
783
839
  {c.shippingCost > 0 && (
784
840
  <div className="flex justify-between">
785
- <span style={{ color: theme.colors.text }}>Shipping</span>
841
+ <span style={{ color: theme.colors.text }}>{tr("forge.checkout.shipping")}</span>
786
842
  <span style={{ color: theme.colors.text }}>{formatCurrency(c.shippingCost)}</span>
787
843
  </div>
788
844
  )}
@@ -791,7 +847,7 @@ export function Checkout({
791
847
  jumped past the subtotal with no explanation. */}
792
848
  {c.bookingFeeCost > 0 && (
793
849
  <div className="flex justify-between" data-testid="summary-booking-fee">
794
- <span style={{ color: theme.colors.text }}>Booking fee</span>
850
+ <span style={{ color: theme.colors.text }}>{tr("forge.checkout.booking_fee")}</span>
795
851
  <span style={{ color: theme.colors.text }}>{formatCurrency(c.bookingFeeCost)}</span>
796
852
  </div>
797
853
  )}
@@ -799,7 +855,7 @@ export function Checkout({
799
855
  quote is known (added on top of the total). */}
800
856
  {taxSummary?.mode === "exclusive" && (
801
857
  <div className="flex justify-between">
802
- <span style={{ color: theme.colors.text }}>Tax</span>
858
+ <span style={{ color: theme.colors.text }}>{tr("forge.checkout.tax")}</span>
803
859
  <span style={{ color: theme.colors.text }}>{taxSummary.formattedTax}</span>
804
860
  </div>
805
861
  )}
@@ -807,7 +863,7 @@ export function Checkout({
807
863
  className="flex justify-between font-bold text-lg pt-2"
808
864
  style={{ borderTop: `1px solid ${theme.colors.primary}${alpha(0.2)}`, color: theme.colors.primary }}
809
865
  >
810
- <span>Total</span>
866
+ <span>{tr("forge.checkout.total")}</span>
811
867
  <span>
812
868
  {c.chargedTotal
813
869
  ? formatCurrency(c.chargedTotal.amount, c.chargedTotal.currency)
@@ -824,7 +880,7 @@ export function Checkout({
824
880
  !taxSummary &&
825
881
  pricesIncludeTax && (
826
882
  <p className="text-xs text-right" style={{ color: `${theme.colors.text}${alpha(0.6)}`, margin: 0 }}>
827
- incl. tax
883
+ {tr("forge.checkout.incl_tax")}
828
884
  </p>
829
885
  )
830
886
  )}
@@ -857,6 +913,7 @@ function PaymentSection({
857
913
  onBack?: () => void;
858
914
  }) {
859
915
  const theme = useForgeTheme();
916
+ const tr = useForgeT();
860
917
  const renderPay = usePaymentRenderer();
861
918
  const { paymentSecret, chargedAmount, chargedCurrency, returnUrl, isPaystack, isCreatingOrder, startError } = checkout;
862
919
  const { canOpenPaystack, openPaystackCheckout } = checkout;
@@ -878,7 +935,7 @@ function PaymentSection({
878
935
  borderRadius: `${theme.cornerRadius}px`,
879
936
  }}
880
937
  >
881
- Back
938
+ {tr("forge.checkout.back")}
882
939
  </button>
883
940
  ) : null;
884
941
 
@@ -894,10 +951,10 @@ function PaymentSection({
894
951
  }}
895
952
  >
896
953
  <h2 className="text-2xl font-bold mb-2" style={{ color: theme.colors.text }}>
897
- Payment
954
+ {tr("forge.checkout.payment_heading")}
898
955
  </h2>
899
956
  <p className="text-lg mb-4" style={{ color: theme.colors.text }}>
900
- Payment is not configured
957
+ {tr("forge.checkout.payment_not_configured")}
901
958
  </p>
902
959
  {backButton}
903
960
  </div>
@@ -914,7 +971,7 @@ function PaymentSection({
914
971
  }}
915
972
  >
916
973
  <h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
917
- Payment Information
974
+ {tr("forge.checkout.payment_title")}
918
975
  </h2>
919
976
 
920
977
  {startError && (
@@ -932,13 +989,15 @@ function PaymentSection({
932
989
  expiredMessage={holdExpiredError || undefined}
933
990
  onRetry={retryHold}
934
991
  isRetrying={isRetryingHold}
935
- secondaryAction={onBack ? { label: "Change your order", onClick: onBack } : undefined}
992
+ secondaryAction={onBack ? { label: tr("forge.checkout.hold_change_order"), onClick: onBack } : undefined}
936
993
  />
937
994
 
938
995
  {/* Suppressed once the server has refused: there is no intent coming, so a
939
996
  "Preparing payment…" spinner would sit there forever under a notice
940
997
  that has already explained what happened. */}
941
- {isCreatingOrder && !hasCharge && !isHoldExpired && <Loading label="Preparing payment…" size={22} />}
998
+ {isCreatingOrder && !hasCharge && !isHoldExpired && (
999
+ <Loading label={tr("forge.checkout.preparing_payment")} size={22} />
1000
+ )}
942
1001
 
943
1002
  {isPaystack
944
1003
  ? canOpenPaystack && (
@@ -985,6 +1044,7 @@ function ShippingRateSelector({
985
1044
  formatCurrency: (amount: number, currencyCode?: string) => string;
986
1045
  }) {
987
1046
  const theme = useForgeTheme();
1047
+ const tr = useForgeT();
988
1048
  if (groups.length === 0) return null;
989
1049
 
990
1050
  return (
@@ -1011,7 +1071,7 @@ function ShippingRateSelector({
1011
1071
  </span>
1012
1072
  {group.rates[0].estimatedDays != null && (
1013
1073
  <span className="text-xs ml-2" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
1014
- ({group.rates[0].estimatedDays} days)
1074
+ {tr("forge.checkout.rate_days", { days: group.rates[0].estimatedDays })}
1015
1075
  </span>
1016
1076
  )}
1017
1077
  </div>
@@ -1024,7 +1084,7 @@ function ShippingRateSelector({
1024
1084
  <div className="space-y-2">
1025
1085
  {group.rates.length === 0 && (
1026
1086
  <p className="text-sm" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
1027
- No shipping options available for this address.
1087
+ {tr("forge.checkout.no_shipping_options")}
1028
1088
  </p>
1029
1089
  )}
1030
1090
  {group.rates.map((rate) => {
@@ -1053,7 +1113,9 @@ function ShippingRateSelector({
1053
1113
  </span>
1054
1114
  {rate.estimatedDays && (
1055
1115
  <p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
1056
- Estimated {rate.estimatedDays} {rate.estimatedDays === 1 ? "day" : "days"}
1116
+ {rate.estimatedDays === 1
1117
+ ? tr("forge.checkout.rate_estimated_day", { days: rate.estimatedDays })
1118
+ : tr("forge.checkout.rate_estimated_days", { days: rate.estimatedDays })}
1057
1119
  </p>
1058
1120
  )}
1059
1121
  </div>