Skip to content

Commit fd1bd52

Browse files
Abidoyesimzeclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 1ebfe9e commit fd1bd52

6 files changed

Lines changed: 126 additions & 35 deletions

File tree

frontend/messages/en.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,5 +452,25 @@
452452
"dismissLabel": "Dismiss notification: {message}",
453453
"notificationLabel": "Notification: {message}",
454454
"timestampLabel": "Timestamp: {timestamp}"
455+
},
456+
"fiatOnramp": {
457+
"title": "Buy / Deposit Funds",
458+
"triggerLabel": "Buy / Deposit",
459+
"description": "Deposit fiat via a Stellar anchor (SEP-0024). Funds arrive as tokens directly in your connected wallet.",
460+
"selectAsset": "Select Asset",
461+
"amountLabel": "Amount",
462+
"amountOptional": "(optional)",
463+
"amountPlaceholder": "e.g. {amount} {asset}",
464+
"anchorDomainLabel": "Anchor Domain",
465+
"anchorDomainPlaceholder": "e.g. testanchor.stellar.org",
466+
"continueButton": "Continue to Anchor",
467+
"footerNote": "Secured by Stellar Network - SEP-0024 Standard",
468+
"stepConnecting": "Connecting to anchor...",
469+
"stepAuth": "Waiting for wallet signature...",
470+
"stepGenerating": "Preparing your deposit form...",
471+
"authHint": "Please sign the challenge transaction in your wallet to securely connect to {domain}.",
472+
"genericHint": "This should only take a moment.",
473+
"iframeTitle": "Anchor deposit form",
474+
"genericError": "Deposit failed"
455475
}
456-
}
476+
}

frontend/messages/es.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,5 +452,25 @@
452452
"dismissLabel": "Descartar notificación: {message}",
453453
"notificationLabel": "Notificación: {message}",
454454
"timestampLabel": "Marca de tiempo: {timestamp}"
455+
},
456+
"fiatOnramp": {
457+
"title": "Comprar / Depositar Fondos",
458+
"triggerLabel": "Comprar / Depositar",
459+
"description": "Deposita fiat a traves de un anchor de Stellar (SEP-0024). Los fondos llegan como tokens directamente a tu billetera conectada.",
460+
"selectAsset": "Seleccionar Activo",
461+
"amountLabel": "Monto",
462+
"amountOptional": "(opcional)",
463+
"amountPlaceholder": "ej. {amount} {asset}",
464+
"anchorDomainLabel": "Dominio del Anchor",
465+
"anchorDomainPlaceholder": "ej. testanchor.stellar.org",
466+
"continueButton": "Continuar al Anchor",
467+
"footerNote": "Protegido por la red Stellar - Estandar SEP-0024",
468+
"stepConnecting": "Conectando con el anchor...",
469+
"stepAuth": "Esperando firma de la billetera...",
470+
"stepGenerating": "Preparando tu formulario de deposito...",
471+
"authHint": "Firma la transaccion de desafio en tu billetera para conectarte de forma segura a {domain}.",
472+
"genericHint": "Esto solo deberia tomar un momento.",
473+
"iframeTitle": "Formulario de deposito del anchor",
474+
"genericError": "El deposito fallo"
455475
}
456476
}

frontend/messages/pt.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,5 +452,25 @@
452452
"dismissLabel": "Dispensar notificacao: {message}",
453453
"notificationLabel": "Notificacao: {message}",
454454
"timestampLabel": "Carimbo de data/hora: {timestamp}"
455+
},
456+
"fiatOnramp": {
457+
"title": "Comprar / Depositar Fundos",
458+
"triggerLabel": "Comprar / Depositar",
459+
"description": "Deposite fiat atraves de um anchor Stellar (SEP-0024). Os fundos chegam como tokens diretamente na sua carteira conectada.",
460+
"selectAsset": "Selecionar Ativo",
461+
"amountLabel": "Valor",
462+
"amountOptional": "(opcional)",
463+
"amountPlaceholder": "ex. {amount} {asset}",
464+
"anchorDomainLabel": "Dominio do Anchor",
465+
"anchorDomainPlaceholder": "ex. testanchor.stellar.org",
466+
"continueButton": "Continuar para o Anchor",
467+
"footerNote": "Protegido pela rede Stellar - Padrao SEP-0024",
468+
"stepConnecting": "Conectando ao anchor...",
469+
"stepAuth": "Aguardando assinatura da carteira...",
470+
"stepGenerating": "Preparando seu formulario de deposito...",
471+
"authHint": "Assine a transacao de desafio na sua carteira para se conectar com seguranca a {domain}.",
472+
"genericHint": "Isso deve levar apenas um momento.",
473+
"iframeTitle": "Formulario de deposito do anchor",
474+
"genericError": "Falha no deposito"
455475
}
456476
}

frontend/src/app/(authenticated)/dashboard/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import React, { useState, useEffect } from "react";
4+
import { useTranslations } from "next-intl";
45
import DashboardSkeleton from "@/components/DashboardSkeleton";
56
import Link from "next/link";
67
import {
@@ -15,6 +16,7 @@ import PaymentsTabs from "@/components/PaymentsTabs";
1516
import FiatOnrampModal from "@/components/FiatOnrampModal";
1617

1718
export default function DashboardPage() {
19+
const t = useTranslations("fiatOnramp");
1820
const [isFirstKeyModalOpen, setIsFirstKeyModalOpen] = useState(false);
1921
const [isOnrampModalOpen, setIsOnrampModalOpen] = useState(false);
2022
const hydrated = useMerchantHydrated();
@@ -91,7 +93,7 @@ export default function DashboardPage() {
9193
d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
9294
/>
9395
</svg>
94-
Buy / Deposit
96+
{t("triggerLabel")}
9597
</button>
9698

9799
<Link

frontend/src/components/FiatOnrampModal.test.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,42 @@ import React from "react";
22
import { describe, it, expect, vi, beforeEach } from "vitest";
33
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
44
import "@testing-library/jest-dom/vitest";
5+
import { NextIntlClientProvider } from "next-intl";
56

67
import FiatOnrampModal from "./FiatOnrampModal";
78

9+
const messages = {
10+
fiatOnramp: {
11+
title: "Buy / Deposit Funds",
12+
triggerLabel: "Buy / Deposit",
13+
description:
14+
"Deposit fiat via a Stellar anchor (SEP-0024). Funds arrive as tokens directly in your connected wallet.",
15+
selectAsset: "Select Asset",
16+
amountLabel: "Amount",
17+
amountOptional: "(optional)",
18+
amountPlaceholder: "e.g. {amount} {asset}",
19+
anchorDomainLabel: "Anchor Domain",
20+
anchorDomainPlaceholder: "e.g. testanchor.stellar.org",
21+
continueButton: "Continue to Anchor",
22+
footerNote: "Secured by Stellar Network - SEP-0024 Standard",
23+
stepConnecting: "Connecting to anchor...",
24+
stepAuth: "Waiting for wallet signature...",
25+
stepGenerating: "Preparing your deposit form...",
26+
authHint: "Please sign the challenge transaction in your wallet to securely connect to {domain}.",
27+
genericHint: "This should only take a moment.",
28+
iframeTitle: "Anchor deposit form",
29+
genericError: "Deposit failed",
30+
},
31+
};
32+
33+
function renderModal(props: { isOpen: boolean; onClose: () => void }) {
34+
return render(
35+
<NextIntlClientProvider locale="en" messages={messages}>
36+
<FiatOnrampModal {...props} />
37+
</NextIntlClientProvider>,
38+
);
39+
}
40+
841
// ─── framer-motion mock ───────────────────────────────────────────────────────
942
// jsdom has no layout engine so framer-motion's measurement APIs fail; strip
1043
// animation-only props and render plain HTML so the DOM stays inspectable.
@@ -56,20 +89,20 @@ describe("FiatOnrampModal", () => {
5689
});
5790

5891
it("does not render when closed", () => {
59-
render(<FiatOnrampModal isOpen={false} onClose={vi.fn()} />);
92+
renderModal({ isOpen: false, onClose: vi.fn() });
6093
expect(screen.queryByText("Buy / Deposit Funds")).not.toBeInTheDocument();
6194
});
6295

6396
it("renders the asset selection form when open", () => {
64-
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
97+
renderModal({ isOpen: true, onClose: vi.fn() });
6598
expect(screen.getByText("Buy / Deposit Funds")).toBeInTheDocument();
6699
expect(screen.getByRole("button", { name: /USDC/ })).toBeInTheDocument();
67100
expect(screen.getByRole("button", { name: /SRT/ })).toBeInTheDocument();
68101
expect(screen.getByRole("button", { name: "Continue to Anchor" })).toBeInTheDocument();
69102
});
70103

71104
it("lets the user switch the selected asset", () => {
72-
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
105+
renderModal({ isOpen: true, onClose: vi.fn() });
73106
const usdcButton = screen.getByRole("button", { name: /USDC/ });
74107
const srtButton = screen.getByRole("button", { name: /SRT/ });
75108

@@ -83,7 +116,7 @@ describe("FiatOnrampModal", () => {
83116
});
84117

85118
it("walks through connecting, auth, and generating loading states before showing the interactive iframe", async () => {
86-
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
119+
renderModal({ isOpen: true, onClose: vi.fn() });
87120

88121
fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" }));
89122

@@ -112,7 +145,7 @@ describe("FiatOnrampModal", () => {
112145
});
113146

114147
it("passes the entered amount through to initiateDeposit", async () => {
115-
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
148+
renderModal({ isOpen: true, onClose: vi.fn() });
116149

117150
fireEvent.change(screen.getByLabelText(/Amount/), { target: { value: "250" } });
118151
fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" }));
@@ -131,7 +164,7 @@ describe("FiatOnrampModal", () => {
131164
it("shows an inline error and returns to the select step when the wallet is unavailable", async () => {
132165
mockGetFreighterPublicKey.mockRejectedValue(new Error("Freighter is not installed"));
133166

134-
render(<FiatOnrampModal isOpen onClose={vi.fn()} />);
167+
renderModal({ isOpen: true, onClose: vi.fn() });
135168
fireEvent.click(screen.getByRole("button", { name: "Continue to Anchor" }));
136169

137170
await waitFor(() => {
@@ -144,7 +177,7 @@ describe("FiatOnrampModal", () => {
144177

145178
it("resets state and calls onClose when closed", () => {
146179
const onClose = vi.fn();
147-
render(<FiatOnrampModal isOpen onClose={onClose} />);
180+
renderModal({ isOpen: true, onClose });
148181

149182
fireEvent.click(screen.getByTestId("modal-close"));
150183

frontend/src/components/FiatOnrampModal.tsx

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useCallback, useState } from "react";
44
import { AnimatePresence, motion } from "framer-motion";
5+
import { useTranslations } from "next-intl";
56
import Skeleton from "react-loading-skeleton";
67
import "react-loading-skeleton/dist/skeleton.css";
78
import {
@@ -33,14 +34,16 @@ const SUPPORTED_ASSETS = [
3334
];
3435

3536
type Step = "SELECT" | "CONNECTING" | "AUTH" | "GENERATING" | "INTERACTIVE";
37+
type BusyStep = Exclude<Step, "SELECT" | "INTERACTIVE">;
3638

37-
const STEP_MESSAGE: Record<Exclude<Step, "SELECT" | "INTERACTIVE">, string> = {
38-
CONNECTING: "Connecting to anchor…",
39-
AUTH: "Waiting for wallet signature…",
40-
GENERATING: "Preparing your deposit form…",
39+
const STEP_MESSAGE_KEY: Record<BusyStep, "stepConnecting" | "stepAuth" | "stepGenerating"> = {
40+
CONNECTING: "stepConnecting",
41+
AUTH: "stepAuth",
42+
GENERATING: "stepGenerating",
4143
};
4244

4345
export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProps) {
46+
const t = useTranslations("fiatOnramp");
4447
const [step, setStep] = useState<Step>("SELECT");
4548
const [amount, setAmount] = useState("");
4649
const [anchorDomain, setAnchorDomain] = useState(DEFAULT_ANCHOR);
@@ -96,15 +99,15 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
9699
setInteractiveUrl(url);
97100
setStep("INTERACTIVE");
98101
} catch (err) {
99-
const message = err instanceof Error ? err.message : "Deposit failed";
102+
const message = err instanceof Error ? err.message : t("genericError");
100103
toast.error(message);
101104
setError(message);
102105
setStep("SELECT");
103106
}
104-
}, [anchorDomain, amount, selectedAsset]);
107+
}, [anchorDomain, amount, selectedAsset, t]);
105108

106109
return (
107-
<Modal isOpen={isOpen} onClose={handleClose} title="Buy / Deposit Funds">
110+
<Modal isOpen={isOpen} onClose={handleClose} title={t("title")}>
108111
<AnimatePresence mode="wait">
109112
{step === "SELECT" && (
110113
<motion.div
@@ -115,10 +118,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
115118
transition={{ duration: 0.18 }}
116119
className="flex flex-col gap-6"
117120
>
118-
<p className="text-sm text-slate-400">
119-
Deposit fiat via a Stellar anchor (SEP-0024). Funds arrive as tokens directly
120-
in your connected wallet.
121-
</p>
121+
<p className="text-sm text-slate-400">{t("description")}</p>
122122

123123
{error && (
124124
<div
@@ -131,7 +131,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
131131

132132
<div className="space-y-2">
133133
<label className="text-xs font-semibold uppercase tracking-wider text-slate-500">
134-
Select Asset
134+
{t("selectAsset")}
135135
</label>
136136
<div className="grid grid-cols-2 gap-4">
137137
{SUPPORTED_ASSETS.map((asset) => (
@@ -158,7 +158,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
158158

159159
<div className="space-y-2">
160160
<label htmlFor="onramp-amount" className="text-xs font-semibold uppercase tracking-wider text-slate-500">
161-
Amount <span className="normal-case text-slate-600">(optional)</span>
161+
{t("amountLabel")} <span className="normal-case text-slate-600">{t("amountOptional")}</span>
162162
</label>
163163
<input
164164
id="onramp-amount"
@@ -168,13 +168,13 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
168168
onChange={(e) => setAmount(e.target.value)}
169169
disabled={isBusy}
170170
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"
171-
placeholder={`e.g. 100 ${selectedAsset.code}`}
171+
placeholder={t("amountPlaceholder", { amount: "100", asset: selectedAsset.code })}
172172
/>
173173
</div>
174174

175175
<div className="space-y-2">
176176
<label htmlFor="onramp-anchor" className="text-xs font-semibold uppercase tracking-wider text-slate-500">
177-
Anchor Domain
177+
{t("anchorDomainLabel")}
178178
</label>
179179
<input
180180
id="onramp-anchor"
@@ -183,7 +183,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
183183
onChange={(e) => setAnchorDomain(e.target.value)}
184184
disabled={isBusy}
185185
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"
186-
placeholder="e.g. testanchor.stellar.org"
186+
placeholder={t("anchorDomainPlaceholder")}
187187
/>
188188
</div>
189189

@@ -196,10 +196,10 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
196196
{isBusy ? (
197197
<>
198198
<Spinner size="sm" className="text-black" />
199-
{STEP_MESSAGE[step as Exclude<Step, "SELECT" | "INTERACTIVE">]}
199+
{t(STEP_MESSAGE_KEY[step as BusyStep])}
200200
</>
201201
) : (
202-
"Continue to Anchor"
202+
t("continueButton")
203203
)}
204204
</button>
205205
</motion.div>
@@ -222,11 +222,9 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
222222
<Spinner size="lg" className="text-mint" />
223223
</div>
224224
</div>
225-
<h3 className="text-lg font-bold text-white">{STEP_MESSAGE[step]}</h3>
225+
<h3 className="text-lg font-bold text-white">{t(STEP_MESSAGE_KEY[step as BusyStep])}</h3>
226226
<p className="mt-2 text-sm text-slate-400">
227-
{step === "AUTH"
228-
? `Please sign the challenge transaction in your wallet to securely connect to ${anchorDomain}.`
229-
: "This should only take a moment."}
227+
{step === "AUTH" ? t("authHint", { domain: anchorDomain }) : t("genericHint")}
230228
</p>
231229
</motion.div>
232230
)}
@@ -249,7 +247,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
249247
)}
250248
<iframe
251249
src={interactiveUrl}
252-
title="Anchor deposit form"
250+
title={t("iframeTitle")}
253251
className="h-full w-full"
254252
onLoad={() => setIframeLoading(false)}
255253
/>
@@ -258,9 +256,7 @@ export default function FiatOnrampModal({ isOpen, onClose }: FiatOnrampModalProp
258256
</AnimatePresence>
259257

260258
{step !== "INTERACTIVE" && (
261-
<p className="mt-6 text-center text-xs text-slate-500">
262-
Secured by Stellar Network • SEP-0024 Standard
263-
</p>
259+
<p className="mt-6 text-center text-xs text-slate-500">{t("footerNote")}</p>
264260
)}
265261
</Modal>
266262
);

0 commit comments

Comments
 (0)