Phase 6: Translate User Pages - exchange, trades, invites, profile

- Expand exchange.json with all exchange page strings (page, steps, detailsStep, bookingStep, confirmationStep, priceDisplay)
- Create trades.json translation files for es, en, ca
- Create invites.json translation files for es, en, ca
- Create profile.json translation files for es, en, ca
- Translate exchange page and all components (ExchangeDetailsStep, BookingStep, ConfirmationStep, StepIndicator, PriceDisplay)
- Translate trades page (titles, sections, buttons, status labels)
- Translate invites page (titles, sections, status badges, copy button)
- Translate profile page (form labels, hints, placeholders, messages)
- Update IntlProvider to load all new namespaces
- All frontend tests passing
This commit is contained in:
counterweight 2025-12-25 22:19:13 +01:00
parent 7dd13292a0
commit 246553c402
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
22 changed files with 559 additions and 115 deletions

View file

@ -6,6 +6,7 @@ import { components } from "../../generated/api";
import { formatDate, formatTime } from "../../utils/date";
import { formatEur } from "../../utils/exchange";
import { bannerStyles } from "../../styles/shared";
import { useTranslation } from "../../hooks/useTranslation";
type BookableSlot = components["schemas"]["BookableSlot"];
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
@ -215,14 +216,15 @@ export function BookingStep({
onSlotSelect,
onBackToDetails,
}: BookingStepProps) {
const t = useTranslation("exchange");
return (
<>
{/* Trade Summary Card */}
<div style={styles.summaryCard}>
<div style={styles.summaryHeader}>
<span style={styles.summaryTitle}>Your Exchange</span>
<span style={styles.summaryTitle}>{t("bookingStep.yourExchange")}</span>
<button onClick={onBackToDetails} style={styles.editButton}>
Edit
{t("bookingStep.edit")}
</button>
</div>
<div style={styles.summaryDetails}>
@ -232,7 +234,7 @@ export function BookingStep({
color: direction === "buy" ? "#4ade80" : "#f87171",
}}
>
{direction === "buy" ? "Buy" : "Sell"} BTC
{direction === "buy" ? t("bookingStep.buy") : t("bookingStep.sell")} BTC
</span>
<span style={styles.summaryDivider}></span>
<span>{formatEur(eurAmount)}</span>
@ -242,15 +244,17 @@ export function BookingStep({
</span>
<span style={styles.summaryDivider}></span>
<span style={styles.summaryPaymentMethod}>
{direction === "buy" ? "Receive via " : "Send via "}
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
{direction === "buy" ? t("bookingStep.receiveVia") : t("bookingStep.sendVia")}{" "}
{bitcoinTransferMethod === "onchain"
? t("transferMethod.onchain")
: t("transferMethod.lightning")}
</span>
</div>
</div>
{/* Date Selection */}
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Select a Date</h2>
<h2 style={styles.sectionTitle}>{t("bookingStep.selectDate")}</h2>
<div style={styles.dateGrid}>
{dates.map((date) => {
const dateStr = formatDate(date);
@ -291,15 +295,13 @@ export function BookingStep({
{/* Warning for existing trade on selected date */}
{existingTradeOnSelectedDate && (
<div style={bannerStyles.errorBanner}>
<div>
You already have a trade booked on this day. You can only book one trade per day.
</div>
<div>{t("bookingStep.existingTradeWarning")}</div>
<div style={styles.errorLink}>
<a
href={`/trades/${existingTradeOnSelectedDate.public_id}`}
style={styles.errorLinkAnchor}
>
View your existing trade
{t("bookingStep.viewExistingTrade")}
</a>
</div>
</div>
@ -309,7 +311,7 @@ export function BookingStep({
{selectedDate && !existingTradeOnSelectedDate && (
<div style={styles.section}>
<h2 style={styles.sectionTitle}>
Available Slots for{" "}
{t("bookingStep.availableSlots")}{" "}
{selectedDate.toLocaleDateString("en-US", {
weekday: "long",
month: "long",
@ -318,9 +320,9 @@ export function BookingStep({
</h2>
{isLoadingSlots ? (
<div style={styles.emptyState}>Loading slots...</div>
<div style={styles.emptyState}>{t("bookingStep.loadingSlots")}</div>
) : availableSlots.length === 0 ? (
<div style={styles.emptyState}>No available slots for this date</div>
<div style={styles.emptyState}>{t("bookingStep.noSlots")}</div>
) : (
<div style={styles.slotGrid}>
{availableSlots.map((slot) => {

View file

@ -6,6 +6,7 @@ import { components } from "../../generated/api";
import { formatTime } from "../../utils/date";
import { formatEur } from "../../utils/exchange";
import { buttonStyles } from "../../styles/shared";
import { useTranslation } from "../../hooks/useTranslation";
type BookableSlot = components["schemas"]["BookableSlot"];
type Direction = "buy" | "sell";
@ -154,14 +155,15 @@ export function ConfirmationStep({
onConfirm,
onBack,
}: ConfirmationStepProps) {
const t = useTranslation("exchange");
return (
<>
{/* Compressed Booking Summary */}
<div style={styles.compressedBookingCard}>
<div style={styles.compressedBookingHeader}>
<span style={styles.compressedBookingTitle}>Appointment</span>
<span style={styles.compressedBookingTitle}>{t("confirmationStep.appointment")}</span>
<button onClick={onBack} style={styles.editButton}>
Edit
{t("confirmationStep.edit")}
</button>
</div>
<div style={styles.compressedBookingDetails}>
@ -181,44 +183,48 @@ export function ConfirmationStep({
{/* Confirmation Card */}
<div style={styles.confirmCard}>
<h3 style={styles.confirmTitle}>Confirm Trade</h3>
<h3 style={styles.confirmTitle}>{t("confirmationStep.confirmTrade")}</h3>
<div style={styles.confirmDetails}>
<div style={styles.confirmRow}>
<span style={styles.confirmLabel}>Time:</span>
<span style={styles.confirmLabel}>{t("confirmationStep.time")}</span>
<span style={styles.confirmValue}>
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
</span>
</div>
<div style={styles.confirmRow}>
<span style={styles.confirmLabel}>Direction:</span>
<span style={styles.confirmLabel}>{t("confirmationStep.direction")}</span>
<span
style={{
...styles.confirmValue,
color: direction === "buy" ? "#4ade80" : "#f87171",
}}
>
{direction === "buy" ? "Buy BTC" : "Sell BTC"}
{direction === "buy" ? t("direction.buyShort") : t("direction.sellShort")}
</span>
</div>
<div style={styles.confirmRow}>
<span style={styles.confirmLabel}>EUR:</span>
<span style={styles.confirmLabel}>{t("confirmationStep.eur")}</span>
<span style={styles.confirmValue}>{formatEur(eurAmount)}</span>
</div>
<div style={styles.confirmRow}>
<span style={styles.confirmLabel}>BTC:</span>
<span style={styles.confirmLabel}>{t("confirmationStep.btc")}</span>
<span style={{ ...styles.confirmValue, ...styles.satsValue }}>
<SatsDisplay sats={satsAmount} />
</span>
</div>
<div style={styles.confirmRow}>
<span style={styles.confirmLabel}>Rate:</span>
<span style={styles.confirmLabel}>{t("confirmationStep.rate")}</span>
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
</div>
<div style={styles.confirmRow}>
<span style={styles.confirmLabel}>Payment:</span>
<span style={styles.confirmLabel}>{t("confirmationStep.payment")}</span>
<span style={styles.confirmValue}>
{direction === "buy" ? "Receive via " : "Send via "}
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
{direction === "buy"
? t("confirmationStep.receiveVia")
: t("confirmationStep.sendVia")}{" "}
{bitcoinTransferMethod === "onchain"
? t("transferMethod.onchain")
: t("transferMethod.lightning")}
</span>
</div>
</div>
@ -237,13 +243,15 @@ export function ConfirmationStep({
}}
>
{isBooking
? "Booking..."
? t("confirmationStep.booking")
: isPriceStale
? "Price Stale"
: `Confirm ${direction === "buy" ? "Buy" : "Sell"}`}
? t("confirmationStep.priceStale")
: direction === "buy"
? t("confirmationStep.confirmBuy")
: t("confirmationStep.confirmSell")}
</button>
<button onClick={onBack} disabled={isBooking} style={styles.cancelButton}>
Back
{t("confirmationStep.back")}
</button>
</div>
</div>

View file

@ -5,6 +5,7 @@ import { SatsDisplay } from "../../components/SatsDisplay";
import { formatEur } from "../../utils/exchange";
import { buttonStyles } from "../../styles/shared";
import constants from "../../../../shared/constants.json";
import { useTranslation } from "../../hooks/useTranslation";
const { lightningMaxEur: LIGHTNING_MAX_EUR } = constants.exchange;
@ -225,6 +226,7 @@ export function ExchangeDetailsStep({
hasPrice,
onContinue,
}: ExchangeDetailsStepProps) {
const t = useTranslation("exchange");
const isLightningDisabled = eurAmount > LIGHTNING_MAX_EUR * 100;
const handleAmountChange = (value: number) => {
@ -263,7 +265,7 @@ export function ExchangeDetailsStep({
...(direction === "buy" ? styles.directionBtnBuyActive : {}),
}}
>
Buy BTC
{t("direction.buyShort")}
</button>
<button
onClick={() => onDirectionChange("sell")}
@ -272,14 +274,15 @@ export function ExchangeDetailsStep({
...(direction === "sell" ? styles.directionBtnSellActive : {}),
}}
>
Sell BTC
{t("direction.sellShort")}
</button>
</div>
{/* Payment Method Selector */}
<div style={styles.paymentMethodSection}>
<div style={styles.paymentMethodLabel}>
Payment Method <span style={styles.required}>*</span>
{t("detailsStep.paymentMethod")}{" "}
<span style={styles.required}>{t("detailsStep.required")}</span>
</div>
<div style={styles.paymentMethodRow}>
<button
@ -290,7 +293,7 @@ export function ExchangeDetailsStep({
}}
>
<span style={styles.paymentMethodIcon}>🔗</span>
<span>Onchain</span>
<span>{t("transferMethod.onchain")}</span>
</button>
<button
onClick={() => onBitcoinTransferMethodChange("lightning")}
@ -302,12 +305,12 @@ export function ExchangeDetailsStep({
}}
>
<span style={styles.paymentMethodIcon}></span>
<span>Lightning</span>
<span>{t("transferMethod.lightning")}</span>
</button>
</div>
{isLightningDisabled && (
<div style={styles.thresholdMessage}>
Lightning payments are only available for amounts up to {LIGHTNING_MAX_EUR}
{t("detailsStep.lightningThreshold", { max: LIGHTNING_MAX_EUR })}
</div>
)}
</div>
@ -315,7 +318,7 @@ export function ExchangeDetailsStep({
{/* Amount Section */}
<div style={styles.amountSection}>
<div style={styles.amountHeader}>
<span style={styles.amountLabel}>Amount (EUR)</span>
<span style={styles.amountLabel}>{t("detailsStep.amount")}</span>
<div style={styles.amountInputWrapper}>
<span style={styles.amountCurrency}></span>
<input
@ -345,18 +348,24 @@ export function ExchangeDetailsStep({
<div style={styles.tradeSummary}>
{direction === "buy" ? (
<p style={styles.summaryText}>
You buy{" "}
{t("detailsStep.summaryBuy").split("{sats}")[0].trim()}{" "}
<strong style={styles.satsValue}>
<SatsDisplay sats={satsAmount} />
</strong>
, you sell <strong>{formatEur(eurAmount)}</strong>
{", "}
{t("detailsStep.summaryBuy").split("{sats}")[1]?.split("{eur}")[0]?.trim()}{" "}
<strong>{formatEur(eurAmount)}</strong>
</p>
) : (
<p style={styles.summaryText}>
You buy <strong>{formatEur(eurAmount)}</strong>, you sell{" "}
{t("detailsStep.summarySell").split("{sats}")[0]?.split("{eur}")[0]?.trim()}{" "}
<strong>{formatEur(eurAmount)}</strong>
{", "}
{t("detailsStep.summarySell").split("{sats}")[0]?.split("{eur}")[1]?.trim()}{" "}
<strong style={styles.satsValue}>
<SatsDisplay sats={satsAmount} />
</strong>
{t("detailsStep.summarySell").split("{sats}")[1]?.trim()}
</p>
)}
</div>
@ -370,7 +379,7 @@ export function ExchangeDetailsStep({
...(isPriceStale || !hasPrice ? buttonStyles.buttonDisabled : {}),
}}
>
Continue to Booking
{t("detailsStep.continueToBooking")}
</button>
</div>
);

View file

@ -2,6 +2,7 @@
import { CSSProperties } from "react";
import { components } from "../../generated/api";
import { useTranslation } from "../../hooks/useTranslation";
type ExchangePriceResponse = components["schemas"]["ExchangePriceResponse"];
@ -94,6 +95,7 @@ export function PriceDisplay({
direction,
agreedPrice,
}: PriceDisplayProps) {
const t = useTranslation("exchange");
const marketPrice = priceData?.price?.market_price ?? 0;
const premiumPercent = priceData?.price?.premium_percentage ?? 5;
const isPriceStale = priceData?.price?.is_stale ?? false;
@ -101,16 +103,16 @@ export function PriceDisplay({
return (
<div style={styles.priceCard}>
{isLoading && !priceData ? (
<div style={styles.priceLoading}>Loading price...</div>
<div style={styles.priceLoading}>{t("priceDisplay.loading")}</div>
) : error && !priceData?.price ? (
<div style={styles.priceError}>{error}</div>
) : (
<>
<div style={styles.priceRow}>
<span style={styles.priceLabel}>Market:</span>
<span style={styles.priceLabel}>{t("priceDisplay.market")}</span>
<span style={styles.priceValue}>{formatPrice(marketPrice)}</span>
<span style={styles.priceDivider}></span>
<span style={styles.priceLabel}>Our price:</span>
<span style={styles.priceLabel}>{t("priceDisplay.ourPrice")}</span>
<span style={styles.priceValue}>{formatPrice(agreedPrice)}</span>
<span style={styles.premiumBadge}>
{direction === "buy" ? "+" : "-"}
@ -119,8 +121,8 @@ export function PriceDisplay({
</div>
{lastUpdate && (
<div style={styles.priceTimestamp}>
Updated {lastUpdate.toLocaleTimeString()}
{isPriceStale && <span style={styles.staleWarning}> (stale)</span>}
{t("priceDisplay.updated")} {lastUpdate.toLocaleTimeString()}
{isPriceStale && <span style={styles.staleWarning}> {t("priceDisplay.stale")}</span>}
</div>
)}
</>

View file

@ -1,6 +1,7 @@
"use client";
import { CSSProperties } from "react";
import { useTranslation } from "../../hooks/useTranslation";
type WizardStep = "details" | "booking" | "confirmation";
@ -58,6 +59,7 @@ const styles: Record<string, CSSProperties> = {
* Shows which step the user is currently on and which steps are completed.
*/
export function StepIndicator({ currentStep }: StepIndicatorProps) {
const t = useTranslation("exchange");
return (
<div style={styles.stepIndicator}>
<div
@ -67,7 +69,7 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
}}
>
<span style={styles.stepNumber}>1</span>
<span style={styles.stepLabel}>Exchange Details</span>
<span style={styles.stepLabel}>{t("steps.details")}</span>
</div>
<div style={styles.stepDivider} />
<div
@ -81,7 +83,7 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
}}
>
<span style={styles.stepNumber}>2</span>
<span style={styles.stepLabel}>Book Appointment</span>
<span style={styles.stepLabel}>{t("steps.booking")}</span>
</div>
<div style={styles.stepDivider} />
<div
@ -91,7 +93,7 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
}}
>
<span style={styles.stepNumber}>3</span>
<span style={styles.stepLabel}>Confirm</span>
<span style={styles.stepLabel}>{t("steps.confirm")}</span>
</div>
</div>
);