From f3a724cb09e33609219bc6575bae0f1f8c37e195 Mon Sep 17 00:00:00 2001 From: whiteghost0001 <197908238+whiteghost0001@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:04:01 +0000 Subject: [PATCH] simple: fix #96 Co-authored-by: Simple Agent --- src/components/EditPriceModal.test.tsx | 148 ++++++++++-------- src/components/EditPriceModal.tsx | 40 ++--- .../TransferOwnershipModal.test.tsx | 16 +- src/components/TransferOwnershipModal.tsx | 38 +++-- src/i18n/locales/en.json | 35 +++++ src/i18n/locales/es.json | 26 ++- 6 files changed, 195 insertions(+), 108 deletions(-) diff --git a/src/components/EditPriceModal.test.tsx b/src/components/EditPriceModal.test.tsx index 8352360..aba7ba4 100644 --- a/src/components/EditPriceModal.test.tsx +++ b/src/components/EditPriceModal.test.tsx @@ -3,11 +3,27 @@ import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { EditPriceModal } from "./EditPriceModal.js"; import { useEditPrice } from "../hooks/useEditPrice.js"; +import { I18nProvider } from "../i18n/I18nProvider.js"; +import { en } from "../i18n/locales/en.js"; vi.mock("../hooks/useEditPrice.js", () => ({ useEditPrice: vi.fn(), })); +function renderModal(props: { + resourceId: string; + currentPrice: string; + apiKey: string; + onClose: () => void; + onConfirmed: (price: string) => void; +}) { + return render( + + + , + ); +} + describe("EditPriceModal", () => { const onClose = vi.fn(); const onConfirmed = vi.fn(); @@ -25,38 +41,38 @@ describe("EditPriceModal", () => { }); it("renders with labeled price input and current value", () => { - render( - , - ); - - expect(screen.getByRole("heading", { name: /edit price/i })).toBeInTheDocument(); - const input = screen.getByLabelText(/new price/i); + renderModal({ + resourceId: "res-1", + currentPrice: "2.50", + apiKey: "key-1", + onClose, + onConfirmed, + }); + + expect( + screen.getByRole("heading", { name: en.editPrice.title }), + ).toBeInTheDocument(); + const input = screen.getByLabelText(en.editPrice.newPriceLabel); expect(input).toHaveValue(2.5); - expect(screen.getByRole("button", { name: /update price/i })).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: en.editPrice.submit }), + ).toBeInTheDocument(); }); it("submits new price on happy path", async () => { const user = userEvent.setup(); - render( - , - ); - - const input = screen.getByLabelText(/new price/i); + renderModal({ + resourceId: "res-1", + currentPrice: "2.50", + apiKey: "key-1", + onClose, + onConfirmed, + }); + + const input = screen.getByLabelText(en.editPrice.newPriceLabel); await user.clear(input); await user.type(input, "3.00"); - await user.click(screen.getByRole("button", { name: /update price/i })); + await user.click(screen.getByRole("button", { name: en.editPrice.submit })); expect(mockEditPrice).toHaveBeenCalledWith("3"); }); @@ -70,18 +86,20 @@ describe("EditPriceModal", () => { editPrice: mockEditPrice, }); - render( - , - ); - - expect(screen.getByText(/waiting for wallet signature/i)).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /working/i })).toBeDisabled(); + renderModal({ + resourceId: "res-1", + currentPrice: "2.50", + apiKey: "key-1", + onClose, + onConfirmed, + }); + + expect( + screen.getByText(en.editPrice.status.signing), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: en.editPrice.working }), + ).toBeDisabled(); }); it("shows error state", () => { @@ -93,15 +111,13 @@ describe("EditPriceModal", () => { editPrice: mockEditPrice, }); - render( - , - ); + renderModal({ + resourceId: "res-1", + currentPrice: "2.50", + apiKey: "key-1", + onClose, + onConfirmed, + }); expect(screen.getByText("Wallet rejected signing")).toBeInTheDocument(); }); @@ -115,35 +131,33 @@ describe("EditPriceModal", () => { editPrice: mockEditPrice, }); - render( - , - ); + renderModal({ + resourceId: "res-1", + currentPrice: "2.50", + apiKey: "key-1", + onClose, + onConfirmed, + }); await waitFor(() => { expect(onConfirmed).toHaveBeenCalledWith("4.00"); }); - expect(screen.getByText(/price updated to 4\.00 usdc/i)).toBeInTheDocument(); + expect( + screen.getByText(en.editPrice.status.confirmed.replace("{price}", "4.00")), + ).toBeInTheDocument(); }); it("closes when cancel is clicked", async () => { const user = userEvent.setup(); - render( - , - ); - - await user.click(screen.getByRole("button", { name: /cancel/i })); + renderModal({ + resourceId: "res-1", + currentPrice: "2.50", + apiKey: "key-1", + onClose, + onConfirmed, + }); + + await user.click(screen.getByRole("button", { name: en.editPrice.cancel })); expect(onClose).toHaveBeenCalled(); }); }); diff --git a/src/components/EditPriceModal.tsx b/src/components/EditPriceModal.tsx index 707a2d3..b434ec0 100644 --- a/src/components/EditPriceModal.tsx +++ b/src/components/EditPriceModal.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { useEditPrice } from "../hooks/useEditPrice.js"; import { useDialogA11y } from "../hooks/useDialogA11y.js"; +import { useTranslation } from "../i18n/index.js"; interface Props { resourceId: string; @@ -15,26 +16,29 @@ const PRICE_PATTERN = /^\d+(\.\d{1,7})?$/; const MAX_STROOPS = 9223372036854775807n; const STROOPS_PER_UNIT = BigInt(10000000); -function validatePrice(value: string): string | null { +type Translate = (key: string, vars?: Record) => string; + +function validatePrice(value: string, t: Translate): string | null { const v = value.trim(); - if (!PRICE_PATTERN.test(v)) return "Enter a price with up to 7 decimal places."; + if (!PRICE_PATTERN.test(v)) return t("editPrice.errors.invalidFormat"); const [whole, frac = ""] = v.split("."); const stroops = BigInt(whole) * STROOPS_PER_UNIT + BigInt((frac + "0000000").slice(0, 7)); - if (stroops <= BigInt(0)) return "Price must be greater than 0."; - if (stroops > MAX_STROOPS) return "Price is too large."; + if (stroops <= BigInt(0)) return t("editPrice.errors.notPositive"); + if (stroops > MAX_STROOPS) return t("editPrice.errors.tooLarge"); return null; } -const STATUS_LABELS: Record = { +const STATUS_KEYS: Record = { idle: "", - preparing: "Building transaction…", - signing: "Waiting for wallet signature…", - submitting: "Submitting to Stellar…", - confirmed: "Price updated!", + preparing: "editPrice.status.preparing", + signing: "editPrice.status.signing", + submitting: "editPrice.status.submitting", + confirmed: "editPrice.status.confirmed", error: "", }; export function EditPriceModal({ resourceId, currentPrice, apiKey, onClose, onConfirmed }: Props) { + const { t } = useTranslation(); const [price, setPrice] = useState(currentPrice); const [priceError, setPriceError] = useState(null); const { status, newPrice, error, networkWarning, editPrice } = useEditPrice(resourceId, apiKey); @@ -50,7 +54,7 @@ export function EditPriceModal({ resourceId, currentPrice, apiKey, onClose, onCo function handleSubmit(e: React.FormEvent) { e.preventDefault(); - const problem = validatePrice(price); + const problem = validatePrice(price, t); setPriceError(problem); if (problem) return; editPrice(price.trim()); @@ -67,13 +71,13 @@ export function EditPriceModal({ resourceId, currentPrice, apiKey, onClose, onCo className="w-full max-w-none overflow-y-auto rounded-none bg-surface-raised p-4 shadow-xl outline-none sm:max-w-sm sm:rounded-2xl sm:p-6" >

- Edit Price + {t("editPrice.title")}

- {STATUS_LABELS[status] && ( -

{STATUS_LABELS[status]}

+ {STATUS_KEYS[status] && ( +

{t(STATUS_KEYS[status])}

)} {networkWarning && (

- ⚠️ {networkWarning} + {t("editPrice.networkWarning", { warning: networkWarning })}

)} @@ -106,7 +110,7 @@ export function EditPriceModal({ resourceId, currentPrice, apiKey, onClose, onCo {status === "confirmed" && (

- Price updated to {newPrice} USDC on-chain and in the database. + {t("editPrice.success", { price: newPrice ?? "" })}

)} @@ -117,7 +121,7 @@ export function EditPriceModal({ resourceId, currentPrice, apiKey, onClose, onCo disabled={busy} className="w-full rounded-lg px-4 py-3 text-sm text-fg-muted hover:bg-surface-hover disabled:opacity-50 sm:w-auto sm:py-2" > - {status === "confirmed" ? "Close" : "Cancel"} + {status === "confirmed" ? t("common.close") : t("common.cancel")} {status !== "confirmed" && ( )} diff --git a/src/components/TransferOwnershipModal.test.tsx b/src/components/TransferOwnershipModal.test.tsx index 156ec28..9d8406c 100644 --- a/src/components/TransferOwnershipModal.test.tsx +++ b/src/components/TransferOwnershipModal.test.tsx @@ -3,11 +3,15 @@ import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { TransferOwnershipModal } from "./TransferOwnershipModal.js"; import { useTransferOwnership } from "../hooks/useTransferOwnership.js"; +import { I18nProvider } from "../i18n/I18nProvider.js"; vi.mock("../hooks/useTransferOwnership.js", () => ({ useTransferOwnership: vi.fn(), })); +const renderWithI18n = (ui: React.ReactElement) => + render({ui}); + describe("TransferOwnershipModal", () => { const onClose = vi.fn(); const onConfirmed = vi.fn(); @@ -25,7 +29,7 @@ describe("TransferOwnershipModal", () => { }); it("renders with labeled owner address input", () => { - render( + renderWithI18n( { it("submits new owner on happy path", async () => { const user = userEvent.setup(); - render( + renderWithI18n( { transferOwnership: mockTransfer, }); - render( + renderWithI18n( { transferOwnership: mockTransfer, }); - render( + renderWithI18n( { transferOwnership: mockTransfer, }); - render( + renderWithI18n( { it("closes when cancel is clicked", async () => { const user = userEvent.setup(); - render( + renderWithI18n( void; } -const STATUS_LABELS: Record = { +const STATUS_KEYS: Record = { idle: "", - preparing: "Building transaction…", - signing: "Waiting for wallet signature…", - submitting: "Submitting to Stellar…", - confirmed: "Ownership transferred!", + preparing: "transferOwnership.status.preparing", + signing: "transferOwnership.status.signing", + submitting: "transferOwnership.status.submitting", + confirmed: "transferOwnership.status.confirmed", error: "", }; export function TransferOwnershipModal({ resourceId, resourceTitle, apiKey, onClose, onConfirmed }: Props) { + const { t } = useTranslation(); const [newCreator, setNewCreator] = useState(""); const [confirming, setConfirming] = useState(false); const { status, newOwner, error, networkWarning, transferOwnership } = useTransferOwnership( @@ -61,13 +63,13 @@ export function TransferOwnershipModal({ resourceId, resourceTitle, apiKey, onCl className="w-full max-w-none overflow-y-auto rounded-none bg-surface-raised p-4 shadow-xl outline-none sm:max-w-sm sm:rounded-2xl sm:p-6" >

- Transfer Ownership + {t("transferOwnership.title")}

{recipient.length > 0 && !recipientValid && (

- Enter a valid Stellar public key: 56 characters starting with G. + {t("transferOwnership.invalidKey")}

)}
@@ -90,19 +92,19 @@ export function TransferOwnershipModal({ resourceId, resourceTitle, apiKey, onCl {confirming && status === "idle" && (

- Transfer {resourceTitle} to this address? This cannot be undone. + {t("transferOwnership.confirmPrompt", { title: resourceTitle })}

{recipient}

)} - {STATUS_LABELS[status] && ( -

{STATUS_LABELS[status]}

+ {STATUS_KEYS[status] && ( +

{t(STATUS_KEYS[status])}

)} {networkWarning && (

- ⚠️ {networkWarning} + {t("transferOwnership.networkWarning", { warning: networkWarning })}

)} @@ -110,7 +112,7 @@ export function TransferOwnershipModal({ resourceId, resourceTitle, apiKey, onCl {status === "confirmed" && newOwner && (

- Ownership transferred to {newOwner}. + {t("transferOwnership.success", { newOwner })}

)} @@ -121,7 +123,7 @@ export function TransferOwnershipModal({ resourceId, resourceTitle, apiKey, onCl disabled={busy} className="w-full rounded-lg px-4 py-3 text-sm text-fg-muted hover:bg-surface-hover disabled:opacity-50 sm:w-auto sm:py-2" > - {status === "confirmed" ? "Close" : "Cancel"} + {status === "confirmed" ? t("common.close") : t("common.cancel")} {confirming && status !== "confirmed" && !busy && ( )} {status !== "confirmed" && ( @@ -138,7 +140,11 @@ export function TransferOwnershipModal({ resourceId, resourceTitle, apiKey, onCl disabled={busy || !recipientValid} className="w-full rounded-lg bg-accent px-4 py-3 text-sm font-medium text-white hover:bg-accent-hover disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto sm:py-2" > - {busy ? "Working…" : confirming ? "Confirm transfer" : "Review transfer"} + {busy + ? t("common.working") + : confirming + ? t("transferOwnership.confirmTransfer") + : t("transferOwnership.reviewTransfer")} )} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e9d2491..0c0904b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -346,6 +346,41 @@ "edit_price": "Edit price", "transfer": "Transfer" }, + "transfer_ownership": { + "title": "Transfer Ownership", + "new_owner_label": "New owner address", + "new_owner_placeholder": "G…", + "invalid_address": "Enter a valid Stellar address: 56 characters starting with G.", + "confirm_prompt": "Transfer \"{{title}}\" to this address? This action cannot be undone.", + "status_preparing": "Preparing transaction…", + "status_signing": "Waiting for signature…", + "status_submitting": "Submitting to blockchain…", + "status_confirmed": "Transfer confirmed", + "network_warning": "Warning:", + "success": "Ownership transferred to {{owner}}.", + "close": "Close", + "cancel": "Cancel", + "back": "Back", + "working": "Working…", + "confirm_transfer": "Confirm transfer", + "review_transfer": "Review transfer" + }, + "edit_price": { + "title": "Edit Price", + "new_price_label": "New price (USDC)", + "invalid_format": "Enter a valid price amount.", + "must_be_positive": "Price must be greater than 0.", + "too_large": "Price is too large.", + "status_preparing": "Preparing transaction…", + "status_signing": "Waiting for signature…", + "status_submitting": "Submitting to blockchain…", + "status_confirmed": "Price updated", + "success": "Price updated to {{price}} USDC.", + "close": "Close", + "cancel": "Cancel", + "working": "Working…", + "update_price": "Update price" + }, "preview": { "title": "Resource Preview", "close": "Close preview", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index f579da7..4f56a7e 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -344,7 +344,31 @@ "view_registration_transaction": "Ver transacción de registro", "register": "Registrar", "edit_price": "Editar precio", - "transfer": "Transferir" + "transfer": "Transferir", + "transfer_ownership_title": "Transferir propiedad", + "transfer_ownership_description": "Transfiera «{{title}}» a otra billetera de Stellar. El nuevo propietario podrá gestionar y registrar este recurso.", + "transfer_ownership_current_owner": "Propietario actual: {{owner}}", + "transfer_ownership_recipient_label": "Dirección del destinatario", + "transfer_ownership_recipient_placeholder": "G…", + "transfer_ownership_invalid_address": "Introduzca una dirección de Stellar válida: 56 caracteres que empiezan por G.", + "transfer_ownership_same_address": "La dirección del destinatario debe ser diferente de la del propietario actual.", + "transfer_ownership_submitting": "Transfiriendo…", + "transfer_ownership_submit": "Transferir propiedad", + "transfer_ownership_success": "Propiedad transferida correctamente.", + "transfer_ownership_error": "No se pudo transferir la propiedad. Revise su conexión e inténtelo de nuevo.", + "transfer_ownership_cancel": "Cancelar", + "transfer_ownership_close": "Cerrar", + "edit_price_title": "Editar precio", + "edit_price_description": "Actualice el precio de «{{title}}». El precio actual es {{price}} USDC.", + "edit_price_label": "Nuevo precio (USDC)", + "edit_price_placeholder": "0.50", + "edit_price_invalid": "Introduzca un precio válido mayor que cero.", + "edit_price_submitting": "Guardando…", + "edit_price_submit": "Guardar precio", + "edit_price_success": "Precio actualizado correctamente.", + "edit_price_error": "No se pudo actualizar el precio. Revise su conexión e inténtelo de nuevo.", + "edit_price_cancel": "Cancelar", + "edit_price_close": "Cerrar" }, "preview": { "title": "Vista previa del recurso",