From fd1bd52fe7e1415dfcb71e07db3069573371840d Mon Sep 17 00:00:00 2001 From: Similoluwa Abidoye Date: Fri, 24 Jul 2026 11:28:45 +0100 Subject: [PATCH 1/2] feat(frontend): add i18n support for Fiat Onramp Integration Interface Replaces the hardcoded English copy added for the SEP-24 deposit modal (title, form labels, per-step loading messages, error fallback, iframe title) with next-intl translations under a new "fiatOnramp" namespace, translated for all three supported locales (en, es, pt). The dashboard's "Buy / Deposit" trigger button is localized too since it's part of the same feature surface. Closes #1212 Co-Authored-By: Claude Sonnet 5 --- frontend/messages/en.json | 22 ++++++++- frontend/messages/es.json | 20 ++++++++ frontend/messages/pt.json | 20 ++++++++ .../app/(authenticated)/dashboard/page.tsx | 4 +- .../src/components/FiatOnrampModal.test.tsx | 47 +++++++++++++++--- frontend/src/components/FiatOnrampModal.tsx | 48 +++++++++---------- 6 files changed, 126 insertions(+), 35 deletions(-) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index dc679ea4..df99ad65 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -452,5 +452,25 @@ "dismissLabel": "Dismiss notification: {message}", "notificationLabel": "Notification: {message}", "timestampLabel": "Timestamp: {timestamp}" + }, + "fiatOnramp": { + "title": "Buy / Deposit Funds", + "triggerLabel": "Buy / Deposit", + "description": "Deposit fiat via a Stellar anchor (SEP-0024). Funds arrive as tokens directly in your connected wallet.", + "selectAsset": "Select Asset", + "amountLabel": "Amount", + "amountOptional": "(optional)", + "amountPlaceholder": "e.g. {amount} {asset}", + "anchorDomainLabel": "Anchor Domain", + "anchorDomainPlaceholder": "e.g. testanchor.stellar.org", + "continueButton": "Continue to Anchor", + "footerNote": "Secured by Stellar Network - SEP-0024 Standard", + "stepConnecting": "Connecting to anchor...", + "stepAuth": "Waiting for wallet signature...", + "stepGenerating": "Preparing your deposit form...", + "authHint": "Please sign the challenge transaction in your wallet to securely connect to {domain}.", + "genericHint": "This should only take a moment.", + "iframeTitle": "Anchor deposit form", + "genericError": "Deposit failed" } -} \ No newline at end of file +} diff --git a/frontend/messages/es.json b/frontend/messages/es.json index bfb03270..2ae33bcc 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -452,5 +452,25 @@ "dismissLabel": "Descartar notificación: {message}", "notificationLabel": "Notificación: {message}", "timestampLabel": "Marca de tiempo: {timestamp}" + }, + "fiatOnramp": { + "title": "Comprar / Depositar Fondos", + "triggerLabel": "Comprar / Depositar", + "description": "Deposita fiat a traves de un anchor de Stellar (SEP-0024). Los fondos llegan como tokens directamente a tu billetera conectada.", + "selectAsset": "Seleccionar Activo", + "amountLabel": "Monto", + "amountOptional": "(opcional)", + "amountPlaceholder": "ej. {amount} {asset}", + "anchorDomainLabel": "Dominio del Anchor", + "anchorDomainPlaceholder": "ej. testanchor.stellar.org", + "continueButton": "Continuar al Anchor", + "footerNote": "Protegido por la red Stellar - Estandar SEP-0024", + "stepConnecting": "Conectando con el anchor...", + "stepAuth": "Esperando firma de la billetera...", + "stepGenerating": "Preparando tu formulario de deposito...", + "authHint": "Firma la transaccion de desafio en tu billetera para conectarte de forma segura a {domain}.", + "genericHint": "Esto solo deberia tomar un momento.", + "iframeTitle": "Formulario de deposito del anchor", + "genericError": "El deposito fallo" } } diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index a2df3ab8..bb046475 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -452,5 +452,25 @@ "dismissLabel": "Dispensar notificacao: {message}", "notificationLabel": "Notificacao: {message}", "timestampLabel": "Carimbo de data/hora: {timestamp}" + }, + "fiatOnramp": { + "title": "Comprar / Depositar Fundos", + "triggerLabel": "Comprar / Depositar", + "description": "Deposite fiat atraves de um anchor Stellar (SEP-0024). Os fundos chegam como tokens diretamente na sua carteira conectada.", + "selectAsset": "Selecionar Ativo", + "amountLabel": "Valor", + "amountOptional": "(opcional)", + "amountPlaceholder": "ex. {amount} {asset}", + "anchorDomainLabel": "Dominio do Anchor", + "anchorDomainPlaceholder": "ex. testanchor.stellar.org", + "continueButton": "Continuar para o Anchor", + "footerNote": "Protegido pela rede Stellar - Padrao SEP-0024", + "stepConnecting": "Conectando ao anchor...", + "stepAuth": "Aguardando assinatura da carteira...", + "stepGenerating": "Preparando seu formulario de deposito...", + "authHint": "Assine a transacao de desafio na sua carteira para se conectar com seguranca a {domain}.", + "genericHint": "Isso deve levar apenas um momento.", + "iframeTitle": "Formulario de deposito do anchor", + "genericError": "Falha no deposito" } } diff --git a/frontend/src/app/(authenticated)/dashboard/page.tsx b/frontend/src/app/(authenticated)/dashboard/page.tsx index 7f8a4ae0..63e602db 100644 --- a/frontend/src/app/(authenticated)/dashboard/page.tsx +++ b/frontend/src/app/(authenticated)/dashboard/page.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState, useEffect } from "react"; +import { useTranslations } from "next-intl"; import DashboardSkeleton from "@/components/DashboardSkeleton"; import Link from "next/link"; import { @@ -15,6 +16,7 @@ import PaymentsTabs from "@/components/PaymentsTabs"; import FiatOnrampModal from "@/components/FiatOnrampModal"; export default function DashboardPage() { + const t = useTranslations("fiatOnramp"); const [isFirstKeyModalOpen, setIsFirstKeyModalOpen] = useState(false); const [isOnrampModalOpen, setIsOnrampModalOpen] = useState(false); const hydrated = useMerchantHydrated(); @@ -91,7 +93,7 @@ export default function DashboardPage() { d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> - Buy / Deposit + {t("triggerLabel")} void }) { + return render( + + + , + ); +} + // ─── framer-motion mock ─────────────────────────────────────────────────────── // jsdom has no layout engine so framer-motion's measurement APIs fail; strip // animation-only props and render plain HTML so the DOM stays inspectable. @@ -56,12 +89,12 @@ describe("FiatOnrampModal", () => { }); it("does not render when closed", () => { - render(); + renderModal({ isOpen: false, onClose: vi.fn() }); expect(screen.queryByText("Buy / Deposit Funds")).not.toBeInTheDocument(); }); it("renders the asset selection form when open", () => { - render(); + renderModal({ isOpen: true, onClose: vi.fn() }); expect(screen.getByText("Buy / Deposit Funds")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /USDC/ })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /SRT/ })).toBeInTheDocument(); @@ -69,7 +102,7 @@ describe("FiatOnrampModal", () => { }); it("lets the user switch the selected asset", () => { - render(); + renderModal({ isOpen: true, onClose: vi.fn() }); const usdcButton = screen.getByRole("button", { name: /USDC/ }); const srtButton = screen.getByRole("button", { name: /SRT/ }); @@ -83,7 +116,7 @@ describe("FiatOnrampModal", () => { }); it("walks through connecting, auth, and generating loading states before showing the interactive iframe", async () => { - render(); + renderModal({ isOpen: true, onClose: vi.fn() }); fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" })); @@ -112,7 +145,7 @@ describe("FiatOnrampModal", () => { }); it("passes the entered amount through to initiateDeposit", async () => { - render(); + renderModal({ isOpen: true, onClose: vi.fn() }); fireEvent.change(screen.getByLabelText(/Amount/), { target: { value: "250" } }); fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" })); @@ -131,7 +164,7 @@ describe("FiatOnrampModal", () => { it("shows an inline error and returns to the select step when the wallet is unavailable", async () => { mockGetFreighterPublicKey.mockRejectedValue(new Error("Freighter is not installed")); - render(); + renderModal({ isOpen: true, onClose: vi.fn() }); fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" })); await waitFor(() => { @@ -144,7 +177,7 @@ describe("FiatOnrampModal", () => { it("resets state and calls onClose when closed", () => { const onClose = vi.fn(); - render(); + renderModal({ isOpen: true, onClose }); fireEvent.click(screen.getByTestId("modal-close")); diff --git a/frontend/src/components/FiatOnrampModal.tsx b/frontend/src/components/FiatOnrampModal.tsx index 6fded897..e4441e25 100644 --- a/frontend/src/components/FiatOnrampModal.tsx +++ b/frontend/src/components/FiatOnrampModal.tsx @@ -2,6 +2,7 @@ import { useCallback, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; +import { useTranslations } from "next-intl"; import Skeleton from "react-loading-skeleton"; import "react-loading-skeleton/dist/skeleton.css"; import { @@ -33,14 +34,16 @@ const SUPPORTED_ASSETS = [ ]; type Step = "SELECT" | "CONNECTING" | "AUTH" | "GENERATING" | "INTERACTIVE"; +type BusyStep = Exclude; -const STEP_MESSAGE: Record, string> = { - CONNECTING: "Connecting to anchor…", - AUTH: "Waiting for wallet signature…", - GENERATING: "Preparing your deposit form…", +const STEP_MESSAGE_KEY: Record = { + CONNECTING: "stepConnecting", + AUTH: "stepAuth", + GENERATING: "stepGenerating", }; export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProps) { + const t = useTranslations("fiatOnramp"); const [step, setStep] = useState("SELECT"); const [amount, setAmount] = useState(""); const [anchorDomain, setAnchorDomain] = useState(DEFAULT_ANCHOR); @@ -96,15 +99,15 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp setInteractiveUrl(url); setStep("INTERACTIVE"); } catch (err) { - const message = err instanceof Error ? err.message : "Deposit failed"; + const message = err instanceof Error ? err.message : t("genericError"); toast.error(message); setError(message); setStep("SELECT"); } - }, [anchorDomain, amount, selectedAsset]); + }, [anchorDomain, amount, selectedAsset, t]); return ( - + {step === "SELECT" && ( -

- Deposit fiat via a Stellar anchor (SEP-0024). Funds arrive as tokens directly - in your connected wallet. -

+

{t("description")}

{error && (
{SUPPORTED_ASSETS.map((asset) => ( @@ -158,7 +158,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
setAmount(e.target.value)} disabled={isBusy} className="w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white focus:outline-none focus:ring-1 focus:ring-mint disabled:cursor-not-allowed disabled:opacity-50" - placeholder={`e.g. 100 ${selectedAsset.code}`} + placeholder={t("amountPlaceholder", { amount: "100", asset: selectedAsset.code })} />
setAnchorDomain(e.target.value)} disabled={isBusy} className="w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white focus:outline-none focus:ring-1 focus:ring-mint disabled:cursor-not-allowed disabled:opacity-50" - placeholder="e.g. testanchor.stellar.org" + placeholder={t("anchorDomainPlaceholder")} />
@@ -196,10 +196,10 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp {isBusy ? ( <> - {STEP_MESSAGE[step as Exclude]} + {t(STEP_MESSAGE_KEY[step as BusyStep])} ) : ( - "Continue to Anchor" + t("continueButton") )} @@ -222,11 +222,9 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
-

{STEP_MESSAGE[step]}

+

{t(STEP_MESSAGE_KEY[step as BusyStep])}

- {step === "AUTH" - ? `Please sign the challenge transaction in your wallet to securely connect to ${anchorDomain}.` - : "This should only take a moment."} + {step === "AUTH" ? t("authHint", { domain: anchorDomain }) : t("genericHint")}

)} @@ -249,7 +247,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp )}