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:
parent
7dd13292a0
commit
246553c402
22 changed files with 559 additions and 115 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue