tf-checkout-react 2.0.0-beta.0 → 2.0.0-beta.2

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.
@@ -14,6 +14,7 @@ import React, { useState, type FC, type ReactNode } from 'react'
14
14
  import { Button } from '../ui/Button'
15
15
  import { Card } from '../ui/Card'
16
16
  import { useCheckoutContext } from '../ui/CheckoutProvider'
17
+ import { useT } from '../ui/I18nProvider'
17
18
  import { cx } from '../ui/cx'
18
19
 
19
20
  export interface ConfirmationProps {
@@ -35,6 +36,7 @@ export const Confirmation: FC<ConfirmationProps> = ({
35
36
  children,
36
37
  className,
37
38
  }) => {
39
+ const t = useT()
38
40
  const checkout = useCheckoutContext()
39
41
  const order = checkout.order
40
42
  const [copied, setCopied] = useState(false)
@@ -78,14 +80,14 @@ export const Confirmation: FC<ConfirmationProps> = ({
78
80
  <Card className={cx('tf-checkout-confirmation', className)}>
79
81
  <span className="tf-checkout-confirmation__check">{pending ? '⏳' : '✓'}</span>
80
82
  <strong className="tf-checkout-confirmation__title">
81
- {pending ? 'Payment pending' : 'Your Tickets are Confirmed!'}
83
+ {pending ? t('confirmation.pending') : t('confirmation.confirmed')}
82
84
  </strong>
83
85
  <p className="tf-checkout-note">
84
86
  {pending
85
- ? 'We’ll email your tickets as soon as the payment settles.'
87
+ ? t('confirmation.pendingBody')
86
88
  : order.alreadyPaid
87
- ? 'This order was already paid — nothing was charged again.'
88
- : 'Your tickets are available in the My Tickets section — please bring them with you to the event.'}
89
+ ? t('confirmation.alreadyPaid')
90
+ : t('confirmation.available')}
89
91
  </p>
90
92
  {order.orderHash && (
91
93
  <p className="tf-checkout-confirmation__ref">Reference: {order.orderHash}</p>
@@ -116,14 +118,14 @@ export const Confirmation: FC<ConfirmationProps> = ({
116
118
  {/* Referral / share — invite friends for cheaper (or free) tickets. */}
117
119
  {!pending && url && (
118
120
  <div className="tf-checkout-confirmation__share">
119
- <strong>Your tickets can be cheaper — or even FREE!</strong>
121
+ <strong>{t('referral.cheaperOrFree')}</strong>
120
122
  <p className="tf-checkout-note">
121
123
  Invite friends with your link. When they buy, you get rewarded.
122
124
  </p>
123
125
  <div className="tf-checkout-confirmation__share-link">
124
126
  <input className="tf-checkout-input" readOnly value={url} />
125
127
  <Button variant="secondary" onClick={copy}>
126
- {copied ? 'Copied!' : 'Copy link'}
128
+ {copied ? t('referral.copied') : t('referral.copyLink')}
127
129
  </Button>
128
130
  </div>
129
131
  <div className="tf-checkout-confirmation__socials">
@@ -4,6 +4,7 @@ import { hooks } from 'tf-checkout-shared'
4
4
 
5
5
  import { Card } from '../ui/Card'
6
6
  import { useCheckoutContext } from '../ui/CheckoutProvider'
7
+ import { useT } from '../ui/I18nProvider'
7
8
 
8
9
  export interface OrderSummaryProps {
9
10
  eventId?: string
@@ -12,6 +13,7 @@ export interface OrderSummaryProps {
12
13
 
13
14
  /** Storefront-compatible order summary shown after the checkout controls. */
14
15
  export const OrderSummary: FC<OrderSummaryProps> = ({ eventId, className }) => {
16
+ const t = useT()
15
17
  const checkout = useCheckoutContext()
16
18
  const event = hooks.useEvent(eventId)
17
19
  const breakdown = checkout.breakdown
@@ -23,13 +25,13 @@ export const OrderSummary: FC<OrderSummaryProps> = ({ eventId, className }) => {
23
25
  <Card className={['tf-checkout-order-summary', className].filter(Boolean).join(' ')}>
24
26
  {event.data?.name && (
25
27
  <div className="tf-checkout-order-summary__group">
26
- <div className="tf-checkout-order-summary__label">Event</div>
28
+ <div className="tf-checkout-order-summary__label">{t('common.event')}</div>
27
29
  <div className="tf-checkout-order-summary__event">{event.data.name}</div>
28
30
  </div>
29
31
  )}
30
32
 
31
33
  <div className="tf-checkout-order-summary__group">
32
- <div className="tf-checkout-order-summary__label">Your Tickets</div>
34
+ <div className="tf-checkout-order-summary__label">{t('tickets.yourTickets')}</div>
33
35
  {breakdown.tickets.map((ticket, index) => {
34
36
  const detail = detailed?.tickets[index]
35
37
  return (
@@ -39,7 +41,7 @@ export const OrderSummary: FC<OrderSummaryProps> = ({ eventId, className }) => {
39
41
  >
40
42
  <div className="tf-checkout-order-summary__ticket">
41
43
  {ticket.quantity} x {ticket.name} - {detail?.unit_price ?? ticket.unit}{' '}
42
- each
44
+ {t('common.each')}
43
45
  </div>
44
46
  {detail?.line_total && detail.line_total !== detail.unit_price && (
45
47
  <div className="tf-checkout-order-summary__fees">
@@ -63,7 +65,7 @@ export const OrderSummary: FC<OrderSummaryProps> = ({ eventId, className }) => {
63
65
  >
64
66
  <div className="tf-checkout-order-summary__ticket">
65
67
  {addOn.quantity} x {addOn.name}
66
- {addOn.show_price ? ` - ${addOn.unit_price} each` : ''}
68
+ {addOn.show_price ? ` - ${addOn.unit_price} ${t('common.each')}` : ''}
67
69
  </div>
68
70
  </div>
69
71
  ))}
@@ -71,7 +73,7 @@ export const OrderSummary: FC<OrderSummaryProps> = ({ eventId, className }) => {
71
73
  )}
72
74
 
73
75
  <div className="tf-checkout-order-summary__total">
74
- <div className="tf-checkout-order-summary__label">Total</div>
76
+ <div className="tf-checkout-order-summary__label">{t('common.total')}</div>
75
77
  <div className="tf-checkout-order-summary__amount">
76
78
  {detailed?.total ??
77
79
  `${breakdown.currencySymbol || `${breakdown.currencyCode} `}${Number(
@@ -24,6 +24,7 @@ import { CardInput, StripePaymentElement, XenditCardInput } from '../CardInput'
24
24
  import { Button } from '../ui/Button'
25
25
  import { Card, SectionHeader } from '../ui/Card'
26
26
  import { useCheckoutContext, useCheckoutValidation } from '../ui/CheckoutProvider'
27
+ import { useT } from '../ui/I18nProvider'
27
28
  import { Checkbox } from '../ui/Field'
28
29
  import { cx } from '../ui/cx'
29
30
  import { Loader } from '../ui/Loader'
@@ -100,6 +101,7 @@ export const Payment: FC<PaymentProps> = ({
100
101
  showTestCardHint = false,
101
102
  className,
102
103
  }) => {
104
+ const t = useT()
103
105
  const checkout = useCheckoutContext()
104
106
  const validation = useCheckoutValidation()
105
107
  const [error, setError] = useState('')
@@ -133,13 +135,31 @@ export const Payment: FC<PaymentProps> = ({
133
135
  const isFree = bd != null && Number(bd.total) === 0
134
136
  const isXendit = checkout.gateway === 'xendit'
135
137
 
138
+ // Whether the server put OXXO on the deferred Elements config. This is
139
+ // `Checkout::get_payment_method_types()`'s verdict — the event's own OXXO
140
+ // flag, the voucher-expiry window against the event date, an MX Stripe
141
+ // account, a MXN 10–10,000 total, and no manual capture. Several of those
142
+ // inputs (the Stripe account's country, per-ticket ID verification, the
143
+ // event's timezone) have no client-side equivalent, so this must never be
144
+ // re-derived here. Absent config (free carts, Xendit, Razorpay) → false,
145
+ // which is what the legacy checkout does too.
146
+ const serverMethodTypes = (
147
+ checkout.stripeElementsConfig as { paymentMethodTypes?: unknown } | undefined
148
+ )?.paymentMethodTypes
149
+ const offersOxxo =
150
+ Array.isArray(serverMethodTypes) && serverMethodTypes.includes('oxxo')
151
+
136
152
  // Build the selectable method rows from gateway + probe + snapshot.
137
153
  const options = useMemo<PaymentMethodOption[]>(() => {
138
154
  if (isFree || !methods) return []
139
155
  const out: PaymentMethodOption[] = []
140
156
  if (isXendit) {
141
157
  if (methods.xenditCard)
142
- out.push({ id: { kind: 'xenditCard' }, value: 'xenditCard', label: 'Card' })
158
+ out.push({
159
+ id: { kind: 'xenditCard' },
160
+ value: 'xenditCard',
161
+ label: t('payment.card'),
162
+ })
143
163
  for (const w of methods.xenditEWallets)
144
164
  out.push({
145
165
  id: { kind: 'xenditEWallet', walletKey: w.key },
@@ -152,7 +172,7 @@ export const Payment: FC<PaymentProps> = ({
152
172
  out.push({ id: { kind: 'razorpay' }, value: 'razorpay', label: 'Razorpay' })
153
173
  } else {
154
174
  // Stripe (or unknown → fail open to card).
155
- out.push({ id: { kind: 'card' }, value: 'card', label: 'Card' })
175
+ out.push({ id: { kind: 'card' }, value: 'card', label: t('payment.card') })
156
176
  for (const t of checkout.redirectPaymentMethods ?? []) {
157
177
  if (t === 'paypal' && methods.paypal) continue // dedupe vs PayPal Standard
158
178
  out.push({
@@ -165,11 +185,12 @@ export const Payment: FC<PaymentProps> = ({
165
185
  out.push({
166
186
  id: { kind: 'paymentPlan' },
167
187
  value: 'paymentPlan',
168
- label: 'Payment plan',
188
+ label: t('payment.paymentPlan'),
169
189
  })
170
- // OXXO cash voucher Mexico only, and never listed in redirect methods.
171
- if (checkout.breakdown?.currencyCode === 'MXN')
172
- out.push({ id: { kind: 'oxxo' }, value: 'oxxo', label: 'OXXO (pay in cash)' })
190
+ // OXXO cash voucher. `oxxo` is deliberately absent from
191
+ // SUPPORTED_REDIRECT_METHODS, so it never arrives via the loop above.
192
+ if (offersOxxo)
193
+ out.push({ id: { kind: 'oxxo' }, value: 'oxxo', label: t('payment.oxxo') })
173
194
  }
174
195
  if (methods.paypal)
175
196
  out.push({ id: { kind: 'paypal' }, value: 'paypal', label: 'PayPal' })
@@ -181,7 +202,7 @@ export const Payment: FC<PaymentProps> = ({
181
202
  checkout.gateway,
182
203
  checkout.redirectPaymentMethods,
183
204
  checkout.paymentPlan,
184
- checkout.breakdown,
205
+ offersOxxo,
185
206
  ])
186
207
 
187
208
  // Default the selection to the first row once options exist.
@@ -204,7 +225,8 @@ export const Payment: FC<PaymentProps> = ({
204
225
  * confirmer with "No card element registered". Always gate on the card.
205
226
  */
206
227
  const reuseSavedCard = Boolean(savedLast4) && useSavedCard
207
- const usesCardDetails = !isFree && (isCardSelected || (isPlanSelected && !reuseSavedCard))
228
+ const usesCardDetails =
229
+ !isFree && (isCardSelected || (isPlanSelected && !reuseSavedCard))
208
230
  // Stripe's CardElement renders its own inline "Card number is invalid" text, so
209
231
  // the pay handler can stay quiet and just scroll to it. Xendit's card form is
210
232
  // three plain inputs with no error slot — there, the message MUST be shown or
@@ -274,10 +296,10 @@ export const Payment: FC<PaymentProps> = ({
274
296
  // The plan CTA carries the brand's terminology ("Confirm Layaway"), and the
275
297
  // terms sentence quotes this same label — they must agree.
276
298
  const payLabel = isFree
277
- ? 'Complete Registration'
299
+ ? t('payment.completeRegistration')
278
300
  : isPlanSelected
279
301
  ? planConfirmLabel(checkout.paymentPlan?.configuration)
280
- : 'Submit Payment'
302
+ : t('payment.submitPayment')
281
303
  const planTerms = paymentPlanTerms(checkout.paymentPlan?.configuration)
282
304
  // 'placing' → order in flight; 'succeeded' → placed, host navigating away.
283
305
  // Both keep the CTA busy so it can't be re-clicked during the handoff.
@@ -406,9 +428,7 @@ export const Payment: FC<PaymentProps> = ({
406
428
  )}
407
429
  {showTestCardHint && (
408
430
  <p className="tf-checkout-note">
409
- {isXendit
410
- ? 'Test card: 4000 0000 0000 0002 · any future date · any CVC'
411
- : 'Test card: 4242 4242 4242 4242 · any future date · any CVC'}
431
+ {isXendit ? t('payment.testCardXendit') : t('payment.testCard')}
412
432
  </p>
413
433
  )}
414
434
  </>
@@ -438,7 +458,7 @@ export const Payment: FC<PaymentProps> = ({
438
458
  <Checkbox
439
459
  checked={useSavedCard}
440
460
  onChange={(e) => setUseSavedCard(e.target.checked)}
441
- label="Use this card"
461
+ label={t('payment.useThisCard')}
442
462
  />
443
463
  </div>
444
464
  )}
@@ -448,13 +468,13 @@ export const Payment: FC<PaymentProps> = ({
448
468
  return (
449
469
  <div className={['tf-checkout-payment-stack', className].filter(Boolean).join(' ')}>
450
470
  <Card>
451
- <SectionHeader title="Payment" />
471
+ <SectionHeader title={t('payment.title')} />
452
472
 
453
473
  {/* Payment method */}
454
474
  {!isFree && (
455
475
  <>
456
476
  {!singleCard && (
457
- <div className="tf-checkout-payment__label">Payment method</div>
477
+ <div className="tf-checkout-payment__label">{t('payment.method')}</div>
458
478
  )}
459
479
  {!methods ? (
460
480
  <div className="tf-checkout-payment__cardbox">
@@ -532,7 +552,7 @@ export const Payment: FC<PaymentProps> = ({
532
552
  loading={finalizing}
533
553
  disabled={checkout.isBusy || finalizing || (!isFree && !methods)}
534
554
  >
535
- {finalizing ? 'Placing…' : payLabel}
555
+ {finalizing ? t('payment.placing') : payLabel}
536
556
  </Button>
537
557
  </div>
538
558
  {bottomError && <p className="tf-checkout-error-text">{bottomError}</p>}
@@ -24,6 +24,7 @@ import {
24
24
  import { Button } from '../ui/Button'
25
25
  import { Card, SectionHeader } from '../ui/Card'
26
26
  import { useCheckoutContext } from '../ui/CheckoutProvider'
27
+ import { useT } from '../ui/I18nProvider'
27
28
  import { cx } from '../ui/cx'
28
29
  import { Field, FieldRow, Input } from '../ui/Field'
29
30
  import { Loader } from '../ui/Loader'
@@ -124,7 +125,7 @@ export const Tickets: FC<TicketsProps> = ({
124
125
  eventId,
125
126
  title = false,
126
127
  onContinue,
127
- buttonLabel = 'Get tickets',
128
+ buttonLabel,
128
129
  timeSlotDates,
129
130
  step,
130
131
  onSelectSeats,
@@ -133,7 +134,9 @@ export const Tickets: FC<TicketsProps> = ({
133
134
  readAttributionFromUrl = true,
134
135
  className,
135
136
  }) => {
137
+ const tr = useT()
136
138
  const checkout = useCheckoutContext()
139
+ const buttonText = buttonLabel ?? tr('tickets.getTickets')
137
140
  const ticketsQuery = hooks.useTicketsWithMeta(eventId)
138
141
  const promoMeta = ticketsQuery.data?.promo
139
142
  const showWaitingList = ticketsQuery.data?.showWaitingList ?? false
@@ -256,7 +259,9 @@ export const Tickets: FC<TicketsProps> = ({
256
259
  // Types that can still be bought with a quantity dropdown (all of them for a
257
260
  // non-seated event). When empty on a seated event, only "Select seats" shows.
258
261
  const directPurchasable = tickets.filter((t) => !seatMapRow(t))
259
- const seatCtaLabel = eventInfo?.tableMapEnabled ? 'Select on map' : 'Select seats'
262
+ const seatCtaLabel = eventInfo?.tableMapEnabled
263
+ ? tr('tickets.selectOnMap')
264
+ : tr('tickets.selectSeats')
260
265
 
261
266
  // A "Tables" divider before the first table type — only when regular tickets
262
267
  // actually precede it (a tables-first list gets no stray divider).
@@ -312,7 +317,9 @@ export const Tickets: FC<TicketsProps> = ({
312
317
  <div className="tf-checkout-tickets__group">{t.groupName}</div>
313
318
  ) : (
314
319
  t.id === firstTableId && (
315
- <div className="tf-checkout-tickets__group">Reserve tables</div>
320
+ <div className="tf-checkout-tickets__group">
321
+ {tr('tickets.reserveTables')}
322
+ </div>
316
323
  )
317
324
  )}
318
325
  <div className="tf-checkout-tickets__row-wrap">
@@ -330,7 +337,9 @@ export const Tickets: FC<TicketsProps> = ({
330
337
  setVisibleDescription((current) => (current === t.id ? null : t.id))
331
338
  }
332
339
  >
333
- {visibleDescription === t.id ? 'Hide info' : 'View info'}
340
+ {visibleDescription === t.id
341
+ ? tr('tickets.hideInfo')
342
+ : tr('tickets.viewInfo')}
334
343
  </button>
335
344
  )}
336
345
  </div>
@@ -367,17 +376,21 @@ export const Tickets: FC<TicketsProps> = ({
367
376
  {seatMapRow(t) ? null : unavailable ? (
368
377
  <span className="tf-checkout-tickets__soldout">
369
378
  {t.soldOut
370
- ? 'Sold out'
379
+ ? tr('tickets.soldOut')
371
380
  : notStarted
372
- ? 'Sales not started'
373
- : 'Sales Ended'}
381
+ ? tr('tickets.salesNotStarted')
382
+ : tr('tickets.salesEnded')}
374
383
  </span>
375
384
  ) : (
376
385
  <div className="tf-checkout-tickets__qty">
377
386
  {/* Tables pick a number of GUESTS (bounded by minGuests/maxGuests),
378
387
  not a count of tickets — the label makes that explicit, matching
379
388
  the old checkout's "GUESTS" affordance. */}
380
- {t.isTable && <span className="tf-checkout-tickets__guests">Guests</span>}
389
+ {t.isTable && (
390
+ <span className="tf-checkout-tickets__guests">
391
+ {tr('tickets.guests')}
392
+ </span>
393
+ )}
381
394
  <QuantitySelect
382
395
  value={qty[t.id] ?? 0}
383
396
  min={range.min}
@@ -447,7 +460,7 @@ export const Tickets: FC<TicketsProps> = ({
447
460
 
448
461
  {isTimeSlot && dates.length > 0 && (
449
462
  <div className="tf-checkout-tickets__currency">
450
- <span className="tf-checkout-tickets__currency-label">Date</span>
463
+ <span className="tf-checkout-tickets__currency-label">{tr('common.date')}</span>
451
464
  <select
452
465
  className="tf-checkout-select"
453
466
  value={slotDate}
@@ -467,7 +480,9 @@ export const Tickets: FC<TicketsProps> = ({
467
480
 
468
481
  {checkout.currency && checkout.currency.available.length > 1 && (
469
482
  <div className="tf-checkout-tickets__currency">
470
- <span className="tf-checkout-tickets__currency-label">Currency</span>
483
+ <span className="tf-checkout-tickets__currency-label">
484
+ {tr('common.currency')}
485
+ </span>
471
486
  <select
472
487
  className="tf-checkout-select"
473
488
  value={checkout.currency.currency}
@@ -483,7 +498,7 @@ export const Tickets: FC<TicketsProps> = ({
483
498
  )}
484
499
 
485
500
  {isTimeSlot && !slotQuery.isLoading && tickets.length === 0 && slotDate && (
486
- <p className="tf-checkout-note">No sessions available on this date.</p>
501
+ <p className="tf-checkout-note">{tr('tickets.noSessions')}</p>
487
502
  )}
488
503
 
489
504
  {isTimeSlot
@@ -511,7 +526,9 @@ export const Tickets: FC<TicketsProps> = ({
511
526
  <FieldRow>
512
527
  <Field>
513
528
  <Input
514
- placeholder={isAccess ? 'Access code' : 'Promo code'}
529
+ placeholder={
530
+ isAccess ? tr('tickets.accessCode') : tr('tickets.promoCode')
531
+ }
515
532
  value={code}
516
533
  onChange={(e) => setCode(e.target.value)}
517
534
  onKeyDown={(e) => e.key === 'Enter' && applyCode()}
@@ -551,10 +568,10 @@ export const Tickets: FC<TicketsProps> = ({
551
568
  disabled={checkout.isBusy || selected.length === 0}
552
569
  >
553
570
  {checkout.isBusy
554
- ? 'Reserving…'
571
+ ? tr('tickets.reserving')
555
572
  : selectionIsTable
556
- ? 'Reserve tables'
557
- : buttonLabel}
573
+ ? tr('tickets.reserveTables')
574
+ : buttonText}
558
575
  </Button>
559
576
  )}
560
577
 
@@ -1,6 +1,7 @@
1
1
  import React, { type Dispatch, type FC, type SetStateAction } from 'react'
2
2
 
3
3
  import { Field, FieldRow, Input, Select } from '../../ui/Field'
4
+ import { useT } from '../../ui/I18nProvider'
4
5
  import type { BillingDraft, BillingFieldError, LocationOption } from './types'
5
6
 
6
7
  interface BillingAddressFieldsProps {
@@ -19,19 +20,20 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
19
20
  fieldError,
20
21
  setBilling,
21
22
  }) => {
23
+ const t = useT()
22
24
  const setValue =
23
25
  (key: keyof BillingDraft) => (event: React.ChangeEvent<HTMLInputElement>) =>
24
26
  setBilling((current) => ({ ...current, [key]: event.target.value }))
25
27
 
26
28
  return (
27
29
  <>
28
- <div className="tf-checkout-billing__section-label">Billing address *</div>
30
+ <div className="tf-checkout-billing__section-label">{t('billing.address')}</div>
29
31
  <Field
30
- error={fieldError('Street address', 'Street address is required.')}
32
+ error={fieldError('Street address', t('billing.streetRequired'))}
31
33
  invalid={Boolean(fieldError('Street address'))}
32
34
  >
33
35
  <Input
34
- placeholder="Street address *"
36
+ placeholder={t('billing.streetPlaceholder')}
35
37
  autoComplete="street-address"
36
38
  value={billing.streetAddress ?? ''}
37
39
  required
@@ -42,11 +44,11 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
42
44
  </Field>
43
45
  <FieldRow>
44
46
  <Field
45
- error={fieldError('City', 'City is required.')}
47
+ error={fieldError('City', t('billing.cityRequired'))}
46
48
  invalid={Boolean(fieldError('City'))}
47
49
  >
48
50
  <Input
49
- placeholder="City *"
51
+ placeholder={t('billing.cityPlaceholder')}
50
52
  autoComplete="address-level2"
51
53
  value={billing.city ?? ''}
52
54
  required
@@ -56,11 +58,11 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
56
58
  />
57
59
  </Field>
58
60
  <Field
59
- error={fieldError('ZIP / postcode', 'ZIP / postcode is required.')}
61
+ error={fieldError('ZIP / postcode', t('billing.postcodeRequired'))}
60
62
  invalid={Boolean(fieldError('ZIP / postcode'))}
61
63
  >
62
64
  <Input
63
- placeholder="ZIP / Postcode *"
65
+ placeholder={t('billing.postcodePlaceholder')}
64
66
  autoComplete="postal-code"
65
67
  value={billing.zip ?? ''}
66
68
  required
@@ -72,12 +74,12 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
72
74
  </FieldRow>
73
75
  <FieldRow>
74
76
  <Field
75
- error={fieldError('Country', 'Country is required.')}
77
+ error={fieldError('Country', t('billing.countryRequired'))}
76
78
  invalid={Boolean(fieldError('Country'))}
77
79
  >
78
80
  <Select
79
81
  value={billing.countryId ?? ''}
80
- aria-label="Country"
82
+ aria-label={t('billing.countryLabel')}
81
83
  required
82
84
  invalid={Boolean(fieldError('Country'))}
83
85
  aria-invalid={Boolean(fieldError('Country'))}
@@ -94,7 +96,7 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
94
96
  }))
95
97
  }}
96
98
  >
97
- <option value="">— country * —</option>
99
+ <option value="">{t('billing.countryEmpty')}</option>
98
100
  {countries.map((country) => (
99
101
  <option key={country.id} value={country.id}>
100
102
  {country.name}
@@ -103,13 +105,13 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
103
105
  </Select>
104
106
  </Field>
105
107
  <Field
106
- error={fieldError('State / county', 'State / county is required.')}
108
+ error={fieldError('State / county', t('billing.stateRequired'))}
107
109
  invalid={Boolean(fieldError('State / county'))}
108
110
  >
109
111
  <Select
110
112
  value={billing.stateId ?? ''}
111
113
  disabled={!billing.countryId || states.length === 0}
112
- aria-label="State / region"
114
+ aria-label={t('billing.stateLabel')}
113
115
  required={states.length > 0}
114
116
  invalid={Boolean(fieldError('State / county'))}
115
117
  aria-invalid={Boolean(fieldError('State / county'))}
@@ -124,7 +126,11 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
124
126
  }))
125
127
  }}
126
128
  >
127
- <option value="">— state / county{states.length > 0 ? ' *' : ''} —</option>
129
+ <option value="">
130
+ {states.length > 0
131
+ ? t('billing.stateEmptyRequired')
132
+ : t('billing.stateEmpty')}
133
+ </option>
128
134
  {states.map((state) => (
129
135
  <option key={state.id} value={state.id}>
130
136
  {state.name}