From a5a1a2c1ad6a4359ea359b455826c289cbfec4fc Mon Sep 17 00:00:00 2001 From: counterweight Date: Thu, 25 Dec 2025 22:06:39 +0100 Subject: [PATCH] Phase 4: Translate Shared Components - common, navigation, status labels - Translate LoadingState and EmptyState components (common namespace) - Translate Header navigation labels (navigation namespace) - Translate StatusBadge trade status labels (exchange namespace) - Create navigation.json translation files for es, en, ca - Create exchange.json translation files for status/direction/transfer labels - Update IntlProvider to load navigation and exchange namespaces - Update frontend tests to expect Spanish translations (default language) - Configure Playwright to use English language for e2e tests via storageState - Fix test expectations to match translated strings All frontend and e2e tests passing. --- frontend/app/components/EmptyState.tsx | 7 +++++- frontend/app/components/Header.tsx | 31 +++++++++++++++--------- frontend/app/components/IntlProvider.tsx | 12 ++++++--- frontend/app/components/LoadingState.tsx | 10 +++++--- frontend/app/components/StatusBadge.tsx | 22 +++++++++++++---- frontend/app/profile/page.test.tsx | 6 ++--- frontend/e2e/helpers/setup.ts | 12 +++++++++ frontend/locales/ca/exchange.json | 17 +++++++++++++ frontend/locales/ca/navigation.json | 11 +++++++++ frontend/locales/en/exchange.json | 17 +++++++++++++ frontend/locales/en/navigation.json | 11 +++++++++ frontend/locales/es/exchange.json | 17 +++++++++++++ frontend/locales/es/navigation.json | 11 +++++++++ frontend/playwright.config.ts | 16 ++++++++++++ 14 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 frontend/e2e/helpers/setup.ts create mode 100644 frontend/locales/ca/exchange.json create mode 100644 frontend/locales/ca/navigation.json create mode 100644 frontend/locales/en/exchange.json create mode 100644 frontend/locales/en/navigation.json create mode 100644 frontend/locales/es/exchange.json create mode 100644 frontend/locales/es/navigation.json diff --git a/frontend/app/components/EmptyState.tsx b/frontend/app/components/EmptyState.tsx index 44cf5af..31c2490 100644 --- a/frontend/app/components/EmptyState.tsx +++ b/frontend/app/components/EmptyState.tsx @@ -1,4 +1,7 @@ +"use client"; + import { utilityStyles } from "../styles/shared"; +import { useTranslation } from "../hooks/useTranslation"; interface EmptyStateProps { /** Message to display when empty */ @@ -18,8 +21,10 @@ interface EmptyStateProps { * Displays a message when there's no data, or a loading state. */ export function EmptyState({ message, hint, isLoading, action, style }: EmptyStateProps) { + const t = useTranslation("common"); + if (isLoading) { - return
Loading...
; + return
{t("loading")}
; } return ( diff --git a/frontend/app/components/Header.tsx b/frontend/app/components/Header.tsx index ef59d0a..6929e64 100644 --- a/frontend/app/components/Header.tsx +++ b/frontend/app/components/Header.tsx @@ -5,6 +5,7 @@ import { useAuth } from "../auth-context"; import { sharedStyles } from "../styles/shared"; import constants from "../../../shared/constants.json"; import { LanguageSelector } from "./LanguageSelector"; +import { useTranslation } from "../hooks/useTranslation"; const { ADMIN, REGULAR } = constants.roles; @@ -24,29 +25,35 @@ interface HeaderProps { interface NavItem { id: PageId; - label: string; + labelKey: string; href: string; regularOnly?: boolean; adminOnly?: boolean; } const REGULAR_NAV_ITEMS: NavItem[] = [ - { id: "exchange", label: "Exchange", href: "/exchange", regularOnly: true }, - { id: "trades", label: "My Trades", href: "/trades", regularOnly: true }, - { id: "invites", label: "My Invites", href: "/invites", regularOnly: true }, - { id: "profile", label: "My Profile", href: "/profile", regularOnly: true }, + { id: "exchange", labelKey: "exchange", href: "/exchange", regularOnly: true }, + { id: "trades", labelKey: "myTrades", href: "/trades", regularOnly: true }, + { id: "invites", labelKey: "myInvites", href: "/invites", regularOnly: true }, + { id: "profile", labelKey: "myProfile", href: "/profile", regularOnly: true }, ]; const ADMIN_NAV_ITEMS: NavItem[] = [ - { id: "admin-trades", label: "Trades", href: "/admin/trades", adminOnly: true }, - { id: "admin-availability", label: "Availability", href: "/admin/availability", adminOnly: true }, - { id: "admin-invites", label: "Invites", href: "/admin/invites", adminOnly: true }, - { id: "admin-price-history", label: "Prices", href: "/admin/price-history", adminOnly: true }, + { id: "admin-trades", labelKey: "trades", href: "/admin/trades", adminOnly: true }, + { + id: "admin-availability", + labelKey: "availability", + href: "/admin/availability", + adminOnly: true, + }, + { id: "admin-invites", labelKey: "invites", href: "/admin/invites", adminOnly: true }, + { id: "admin-price-history", labelKey: "prices", href: "/admin/price-history", adminOnly: true }, ]; export function Header({ currentPage }: HeaderProps) { const { user, logout, hasRole } = useAuth(); const router = useRouter(); + const t = useTranslation("navigation"); const isRegularUser = hasRole(REGULAR); const isAdminUser = hasRole(ADMIN); @@ -71,10 +78,10 @@ export function Header({ currentPage }: HeaderProps) { {index > 0 && } {item.id === currentPage ? ( - {item.label} + {t(item.labelKey)} ) : ( - {item.label} + {t(item.labelKey)} )} @@ -84,7 +91,7 @@ export function Header({ currentPage }: HeaderProps) { {user.email} diff --git a/frontend/app/components/IntlProvider.tsx b/frontend/app/components/IntlProvider.tsx index e573941..912a9da 100644 --- a/frontend/app/components/IntlProvider.tsx +++ b/frontend/app/components/IntlProvider.tsx @@ -8,11 +8,17 @@ import { useLanguage } from "../hooks/useLanguage"; import esCommon from "../../locales/es/common.json"; import enCommon from "../../locales/en/common.json"; import caCommon from "../../locales/ca/common.json"; +import esNavigation from "../../locales/es/navigation.json"; +import enNavigation from "../../locales/en/navigation.json"; +import caNavigation from "../../locales/ca/navigation.json"; +import esExchange from "../../locales/es/exchange.json"; +import enExchange from "../../locales/en/exchange.json"; +import caExchange from "../../locales/ca/exchange.json"; const messages = { - es: { common: esCommon }, - en: { common: enCommon }, - ca: { common: caCommon }, + es: { common: esCommon, navigation: esNavigation, exchange: esExchange }, + en: { common: enCommon, navigation: enNavigation, exchange: enExchange }, + ca: { common: caCommon, navigation: caNavigation, exchange: caExchange }, }; interface IntlProviderProps { diff --git a/frontend/app/components/LoadingState.tsx b/frontend/app/components/LoadingState.tsx index ff92367..af78e0a 100644 --- a/frontend/app/components/LoadingState.tsx +++ b/frontend/app/components/LoadingState.tsx @@ -1,9 +1,10 @@ "use client"; import { layoutStyles } from "../styles/shared"; +import { useTranslation } from "../hooks/useTranslation"; interface LoadingStateProps { - /** Custom loading message (default: "Loading...") */ + /** Custom loading message (default: uses translation) */ message?: string; } @@ -11,10 +12,13 @@ interface LoadingStateProps { * Standard loading state component. * Displays a centered loading message with consistent styling. */ -export function LoadingState({ message = "Loading..." }: LoadingStateProps) { +export function LoadingState({ message }: LoadingStateProps) { + const t = useTranslation("common"); + const displayMessage = message || t("loading"); + return (
-
{message}
+
{displayMessage}
); } diff --git a/frontend/app/components/StatusBadge.tsx b/frontend/app/components/StatusBadge.tsx index 4b50d4c..e232453 100644 --- a/frontend/app/components/StatusBadge.tsx +++ b/frontend/app/components/StatusBadge.tsx @@ -1,5 +1,8 @@ +"use client"; + import { badgeStyles } from "../styles/shared"; import { getTradeStatusDisplay } from "../utils/exchange"; +import { useTranslation } from "../hooks/useTranslation"; type StatusBadgeVariant = "success" | "error" | "ready"; @@ -14,11 +17,20 @@ interface StatusBadgeProps { style?: React.CSSProperties; } +const STATUS_KEY_MAP: Record = { + booked: "pending", + completed: "completed", + cancelled_by_user: "userCancelled", + cancelled_by_admin: "adminCancelled", + no_show: "noShow", +}; + /** * Standardized status badge component. * Can be used with a variant prop for simple badges, or tradeStatus prop for trade-specific styling. */ export function StatusBadge({ children, variant, tradeStatus, style }: StatusBadgeProps) { + const t = useTranslation("exchange"); let badgeStyle: React.CSSProperties = { ...badgeStyles.badge }; if (tradeStatus) { @@ -44,9 +56,9 @@ export function StatusBadge({ children, variant, tradeStatus, style }: StatusBad } } - return ( - - {tradeStatus ? getTradeStatusDisplay(tradeStatus).text : children} - - ); + const displayText = tradeStatus + ? t(`status.${STATUS_KEY_MAP[tradeStatus] || tradeStatus}`) + : children; + + return {displayText}; } diff --git a/frontend/app/profile/page.test.tsx b/frontend/app/profile/page.test.tsx index ae1d4e5..aeb0ee8 100644 --- a/frontend/app/profile/page.test.tsx +++ b/frontend/app/profile/page.test.tsx @@ -115,7 +115,7 @@ describe("ProfilePage - Display", () => { mockGetProfile.mockImplementation(() => new Promise(() => {})); renderWithProviders(); - expect(screen.getByText("Loading...")).toBeDefined(); + expect(screen.getByText("Cargando...")).toBeDefined(); }); test("renders profile page title", async () => { @@ -211,7 +211,7 @@ describe("ProfilePage - Navigation", () => { renderWithProviders(); await waitFor(() => { expect(screen.getByText("Exchange")).toBeDefined(); - expect(screen.getByText("My Trades")).toBeDefined(); + expect(screen.getByText("Mis Operaciones")).toBeDefined(); }); }); @@ -284,7 +284,7 @@ describe("ProfilePage - Loading State", () => { renderWithProviders(); - expect(screen.getByText("Loading...")).toBeDefined(); + expect(screen.getByText("Cargando...")).toBeDefined(); }); }); diff --git a/frontend/e2e/helpers/setup.ts b/frontend/e2e/helpers/setup.ts new file mode 100644 index 0000000..8174eb7 --- /dev/null +++ b/frontend/e2e/helpers/setup.ts @@ -0,0 +1,12 @@ +import { Page } from "@playwright/test"; + +/** + * Set language to English for e2e tests. + * E2E tests should only test in English according to requirements. + * Call this in beforeEach hooks in test files. + */ +export async function setEnglishLanguage(page: Page) { + await page.addInitScript(() => { + window.localStorage.setItem("arbret-locale", "en"); + }); +} diff --git a/frontend/locales/ca/exchange.json b/frontend/locales/ca/exchange.json new file mode 100644 index 0000000..2362763 --- /dev/null +++ b/frontend/locales/ca/exchange.json @@ -0,0 +1,17 @@ +{ + "status": { + "pending": "Pendent", + "completed": "Completada", + "userCancelled": "Cancel·lada per Usuari", + "adminCancelled": "Cancel·lada per Admin", + "noShow": "No Present" + }, + "direction": { + "buy": "COMPRAR BTC", + "sell": "VENDRE BTC" + }, + "transferMethod": { + "onchain": "Onchain", + "lightning": "Lightning" + } +} diff --git a/frontend/locales/ca/navigation.json b/frontend/locales/ca/navigation.json new file mode 100644 index 0000000..db6e10d --- /dev/null +++ b/frontend/locales/ca/navigation.json @@ -0,0 +1,11 @@ +{ + "exchange": "Exchange", + "myTrades": "Les Meves Operacions", + "myInvites": "Les Meves Invitacions", + "myProfile": "El Meu Perfil", + "signOut": "Tancar sessió", + "trades": "Operacions", + "availability": "Disponibilitat", + "invites": "Invitacions", + "prices": "Preus" +} diff --git a/frontend/locales/en/exchange.json b/frontend/locales/en/exchange.json new file mode 100644 index 0000000..13297c6 --- /dev/null +++ b/frontend/locales/en/exchange.json @@ -0,0 +1,17 @@ +{ + "status": { + "pending": "Pending", + "completed": "Completed", + "userCancelled": "User Cancelled", + "adminCancelled": "Admin Cancelled", + "noShow": "No Show" + }, + "direction": { + "buy": "BUY BTC", + "sell": "SELL BTC" + }, + "transferMethod": { + "onchain": "Onchain", + "lightning": "Lightning" + } +} diff --git a/frontend/locales/en/navigation.json b/frontend/locales/en/navigation.json new file mode 100644 index 0000000..441b294 --- /dev/null +++ b/frontend/locales/en/navigation.json @@ -0,0 +1,11 @@ +{ + "exchange": "Exchange", + "myTrades": "My Trades", + "myInvites": "My Invites", + "myProfile": "My Profile", + "signOut": "Sign out", + "trades": "Trades", + "availability": "Availability", + "invites": "Invites", + "prices": "Prices" +} diff --git a/frontend/locales/es/exchange.json b/frontend/locales/es/exchange.json new file mode 100644 index 0000000..f755651 --- /dev/null +++ b/frontend/locales/es/exchange.json @@ -0,0 +1,17 @@ +{ + "status": { + "pending": "Pendiente", + "completed": "Completada", + "userCancelled": "Cancelada por Usuario", + "adminCancelled": "Cancelada por Admin", + "noShow": "No Presente" + }, + "direction": { + "buy": "COMPRAR BTC", + "sell": "VENDER BTC" + }, + "transferMethod": { + "onchain": "Onchain", + "lightning": "Lightning" + } +} diff --git a/frontend/locales/es/navigation.json b/frontend/locales/es/navigation.json new file mode 100644 index 0000000..d85ff25 --- /dev/null +++ b/frontend/locales/es/navigation.json @@ -0,0 +1,11 @@ +{ + "exchange": "Exchange", + "myTrades": "Mis Operaciones", + "myInvites": "Mis Invitaciones", + "myProfile": "Mi Perfil", + "signOut": "Cerrar sesión", + "trades": "Operaciones", + "availability": "Disponibilidad", + "invites": "Invitaciones", + "prices": "Precios" +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index ebc1a25..daf2340 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -20,5 +20,21 @@ export default defineConfig({ // Reduce screenshot/recording overhead screenshot: "only-on-failure", trace: "retain-on-failure", + // Set language to English for all e2e tests via localStorage + // E2E tests should only test in English according to requirements + storageState: { + cookies: [], + origins: [ + { + origin: "http://localhost:3000", + localStorage: [ + { + name: "arbret-locale", + value: "en", + }, + ], + }, + ], + }, }, });