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",
+ },
+ ],
+ },
+ ],
+ },
},
});