From 7dd13292a0aa94dbfb52ebae50d98b638564fd17 Mon Sep 17 00:00:00 2001 From: counterweight Date: Thu, 25 Dec 2025 22:14:04 +0100 Subject: [PATCH] Phase 5: Translate Auth Pages - login and signup - Create auth.json translation files for es, en, ca - Translate login page: title, subtitle, form labels, buttons, footer - Translate signup page: invite code step and account creation step - Translate signup/[code] redirect page - Update IntlProvider to load auth namespace - Update test expectations to match Spanish translations (default language) - All frontend and e2e tests passing --- frontend/app/components/IntlProvider.tsx | 9 +++-- frontend/app/login/page.test.tsx | 10 ++--- frontend/app/login/page.tsx | 22 ++++++----- frontend/app/signup/[code]/page.tsx | 4 +- frontend/app/signup/page.test.tsx | 8 ++-- frontend/app/signup/page.tsx | 50 ++++++++++++------------ frontend/locales/ca/auth.json | 44 +++++++++++++++++++++ frontend/locales/en/auth.json | 44 +++++++++++++++++++++ frontend/locales/es/auth.json | 44 +++++++++++++++++++++ 9 files changed, 188 insertions(+), 47 deletions(-) create mode 100644 frontend/locales/ca/auth.json create mode 100644 frontend/locales/en/auth.json create mode 100644 frontend/locales/es/auth.json diff --git a/frontend/app/components/IntlProvider.tsx b/frontend/app/components/IntlProvider.tsx index 912a9da..aea34e5 100644 --- a/frontend/app/components/IntlProvider.tsx +++ b/frontend/app/components/IntlProvider.tsx @@ -14,11 +14,14 @@ 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"; +import esAuth from "../../locales/es/auth.json"; +import enAuth from "../../locales/en/auth.json"; +import caAuth from "../../locales/ca/auth.json"; const messages = { - es: { common: esCommon, navigation: esNavigation, exchange: esExchange }, - en: { common: enCommon, navigation: enNavigation, exchange: enExchange }, - ca: { common: caCommon, navigation: caNavigation, exchange: caExchange }, + es: { common: esCommon, navigation: esNavigation, exchange: esExchange, auth: esAuth }, + en: { common: enCommon, navigation: enNavigation, exchange: enExchange, auth: enAuth }, + ca: { common: caCommon, navigation: caNavigation, exchange: caExchange, auth: caAuth }, }; interface IntlProviderProps { diff --git a/frontend/app/login/page.test.tsx b/frontend/app/login/page.test.tsx index 9d1c9ba..2d51738 100644 --- a/frontend/app/login/page.test.tsx +++ b/frontend/app/login/page.test.tsx @@ -19,21 +19,21 @@ afterEach(() => cleanup()); test("renders login form with title", () => { renderWithProviders(); - expect(screen.getByText("Welcome back")).toBeDefined(); + expect(screen.getByText("Bienvenido de nuevo")).toBeDefined(); }); test("renders email and password inputs", () => { renderWithProviders(); - expect(screen.getByLabelText("Email")).toBeDefined(); - expect(screen.getByLabelText("Password")).toBeDefined(); + expect(screen.getByLabelText("Correo electrónico")).toBeDefined(); + expect(screen.getByLabelText("Contraseña")).toBeDefined(); }); test("renders sign in button", () => { renderWithProviders(); - expect(screen.getByRole("button", { name: "Sign in" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Iniciar sesión" })).toBeDefined(); }); test("renders link to signup", () => { renderWithProviders(); - expect(screen.getByText("Sign up")).toBeDefined(); + expect(screen.getByText("Regístrate")).toBeDefined(); }); diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx index bb8d0ba..19a3d06 100644 --- a/frontend/app/login/page.tsx +++ b/frontend/app/login/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { useAuth } from "../auth-context"; import { authFormStyles as styles } from "../styles/auth-form"; import { LanguageSelector } from "../components/LanguageSelector"; +import { useTranslation } from "../hooks/useTranslation"; export default function LoginPage() { const [email, setEmail] = useState(""); @@ -13,6 +14,7 @@ export default function LoginPage() { const [isSubmitting, setIsSubmitting] = useState(false); const { login } = useAuth(); const router = useRouter(); + const t = useTranslation("auth"); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -23,7 +25,7 @@ export default function LoginPage() { await login(email, password); router.push("/"); } catch (err) { - setError(err instanceof Error ? err.message : "Login failed"); + setError(err instanceof Error ? err.message : t("login.loginFailed")); } finally { setIsSubmitting(false); } @@ -37,8 +39,8 @@ export default function LoginPage() {
-

Welcome back

-

Sign in to your account

+

{t("login.title")}

+

{t("login.subtitle")}

@@ -46,7 +48,7 @@ export default function LoginPage() {
setEmail(e.target.value)} style={styles.input} - placeholder="you@example.com" + placeholder={t("login.emailPlaceholder")} required />
setPassword(e.target.value)} style={styles.input} - placeholder="••••••••" + placeholder={t("login.passwordPlaceholder")} required />
@@ -82,14 +84,14 @@ export default function LoginPage() { }} disabled={isSubmitting} > - {isSubmitting ? "Signing in..." : "Sign in"} + {isSubmitting ? t("login.signingIn") : t("login.signIn")}

- Don't have an account?{" "} + {t("login.noAccount")}{" "} - Sign up + {t("login.signUp")}

diff --git a/frontend/app/signup/[code]/page.tsx b/frontend/app/signup/[code]/page.tsx index 0505e56..53738f8 100644 --- a/frontend/app/signup/[code]/page.tsx +++ b/frontend/app/signup/[code]/page.tsx @@ -4,12 +4,14 @@ import { useEffect } from "react"; import { useRouter, useParams } from "next/navigation"; import { useAuth } from "../../auth-context"; import { LanguageSelector } from "../../components/LanguageSelector"; +import { useTranslation } from "../../hooks/useTranslation"; export default function SignupWithCodePage() { const params = useParams(); const router = useRouter(); const { user, isLoading } = useAuth(); const code = params.code as string; + const t = useTranslation("auth"); useEffect(() => { // Wait for auth check to complete before redirecting @@ -40,7 +42,7 @@ export default function SignupWithCodePage() {
- Redirecting... + {t("signup.redirecting")} ); } diff --git a/frontend/app/signup/page.test.tsx b/frontend/app/signup/page.test.tsx index f4aec2a..7eb09e0 100644 --- a/frontend/app/signup/page.test.tsx +++ b/frontend/app/signup/page.test.tsx @@ -21,20 +21,20 @@ afterEach(() => cleanup()); test("renders signup form with title", () => { renderWithProviders(); // Step 1 shows "Join with Invite" title (invite code entry) - expect(screen.getByRole("heading", { name: "Join with Invite" })).toBeDefined(); + expect(screen.getByRole("heading", { name: "Únete con Invitación" })).toBeDefined(); }); test("renders invite code input", () => { renderWithProviders(); - expect(screen.getByLabelText("Invite Code")).toBeDefined(); + expect(screen.getByLabelText("Código de Invitación")).toBeDefined(); }); test("renders continue button", () => { renderWithProviders(); - expect(screen.getByRole("button", { name: "Continue" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Continuar" })).toBeDefined(); }); test("renders link to login", () => { renderWithProviders(); - expect(screen.getByText("Sign in")).toBeDefined(); + expect(screen.getByText("Iniciar sesión")).toBeDefined(); }); diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx index e9c4717..829e6ce 100644 --- a/frontend/app/signup/page.tsx +++ b/frontend/app/signup/page.tsx @@ -6,6 +6,7 @@ import { useAuth } from "../auth-context"; import { invitesApi } from "../api"; import { authFormStyles as styles } from "../styles/auth-form"; import { LanguageSelector } from "../components/LanguageSelector"; +import { useTranslation } from "../hooks/useTranslation"; function SignupContent() { const searchParams = useSearchParams(); @@ -25,6 +26,7 @@ function SignupContent() { const { user, register } = useAuth(); const router = useRouter(); + const t = useTranslation("auth"); // Redirect if already logged in useEffect(() => { @@ -51,11 +53,11 @@ function SignupContent() { setInviteError(""); } else { setInviteValid(false); - setInviteError(response.error || "Invalid invite code"); + setInviteError(response.error || t("signup.invalidInviteCode")); } } catch { setInviteValid(false); - setInviteError("Failed to verify invite code"); + setInviteError(t("signup.failedToVerify")); } finally { setIsCheckingInvite(false); } @@ -78,12 +80,12 @@ function SignupContent() { setError(""); if (password !== confirmPassword) { - setError("Passwords do not match"); + setError(t("signup.passwordsDoNotMatch")); return; } if (password.length < 6) { - setError("Password must be at least 6 characters"); + setError(t("signup.passwordTooShort")); return; } @@ -93,7 +95,7 @@ function SignupContent() { await register(email, password, inviteCode.trim()); router.push("/"); } catch (err) { - setError(err instanceof Error ? err.message : "Registration failed"); + setError(err instanceof Error ? err.message : t("signup.registrationFailed")); } finally { setIsSubmitting(false); } @@ -114,7 +116,7 @@ function SignupContent() {
- Checking invite code... + {t("signup.checkingInviteCode")}
@@ -132,8 +134,8 @@ function SignupContent() {
-

Join with Invite

-

Enter your invite code to get started

+

{t("signup.title")}

+

{t("signup.subtitle")}

@@ -141,7 +143,7 @@ function SignupContent() {
@@ -165,7 +167,7 @@ function SignupContent() { display: "block", }} > - Ask your inviter for this code + {t("signup.inviteHint")}
@@ -177,14 +179,14 @@ function SignupContent() { }} disabled={isCheckingInvite || !inviteCode.trim()} > - {isCheckingInvite ? "Checking..." : "Continue"} + {isCheckingInvite ? t("signup.checking") : t("signup.continue")}

- Already have an account?{" "} + {t("signup.alreadyHaveAccount")}{" "} - Sign in + {t("signup.signIn")}

@@ -202,9 +204,9 @@ function SignupContent() {
-

Create account

+

{t("signup.createAccountTitle")}

- Using invite:{" "} + {t("signup.createAccountSubtitle")}{" "} setEmail(e.target.value)} style={styles.input} - placeholder="you@example.com" + placeholder={t("signup.emailPlaceholder")} required autoFocus /> @@ -239,7 +241,7 @@ function SignupContent() {

setPassword(e.target.value)} style={styles.input} - placeholder="••••••••" + placeholder={t("signup.passwordPlaceholder")} required />
setConfirmPassword(e.target.value)} style={styles.input} - placeholder="••••••••" + placeholder={t("signup.confirmPasswordPlaceholder")} required />
@@ -275,7 +277,7 @@ function SignupContent() { }} disabled={isSubmitting} > - {isSubmitting ? "Creating account..." : "Create account"} + {isSubmitting ? t("signup.creatingAccount") : t("signup.createAccount")} @@ -293,7 +295,7 @@ function SignupContent() { padding: 0, }} > - Use a different invite code + {t("signup.useDifferentCode")}

diff --git a/frontend/locales/ca/auth.json b/frontend/locales/ca/auth.json new file mode 100644 index 0000000..7e4ac0a --- /dev/null +++ b/frontend/locales/ca/auth.json @@ -0,0 +1,44 @@ +{ + "login": { + "title": "Benvingut de nou", + "subtitle": "Inicia sessió al teu compte", + "email": "Correu electrònic", + "password": "Contrasenya", + "emailPlaceholder": "tu@exemple.com", + "passwordPlaceholder": "••••••••", + "signIn": "Iniciar sessió", + "signingIn": "Iniciant sessió...", + "noAccount": "No tens un compte?", + "signUp": "Registra't", + "loginFailed": "Error en iniciar sessió" + }, + "signup": { + "title": "Uneix-te amb Invitació", + "subtitle": "Introdueix el teu codi d'invitació per començar", + "inviteCode": "Codi d'Invitació", + "inviteCodePlaceholder": "paraula-paraula-00", + "inviteHint": "Demana aquest codi al teu convidant", + "checking": "Comprovant...", + "continue": "Continuar", + "checkingInviteCode": "Comprovant codi d'invitació...", + "createAccount": "Crear compte", + "createAccountTitle": "Crear compte", + "createAccountSubtitle": "Utilitzant invitació:", + "email": "Correu electrònic", + "emailPlaceholder": "tu@exemple.com", + "password": "Contrasenya", + "passwordPlaceholder": "••••••••", + "confirmPassword": "Confirmar Contrasenya", + "confirmPasswordPlaceholder": "••••••••", + "creatingAccount": "Creant compte...", + "alreadyHaveAccount": "Ja tens un compte?", + "signIn": "Iniciar sessió", + "useDifferentCode": "Utilitzar un codi d'invitació diferent", + "redirecting": "Redirigint...", + "passwordsDoNotMatch": "Les contrasenyes no coincideixen", + "passwordTooShort": "La contrasenya ha de tenir almenys 6 caràcters", + "invalidInviteCode": "Codi d'invitació invàlid", + "failedToVerify": "Error en verificar codi d'invitació", + "registrationFailed": "Error en registrar-se" + } +} diff --git a/frontend/locales/en/auth.json b/frontend/locales/en/auth.json new file mode 100644 index 0000000..fd5b9cd --- /dev/null +++ b/frontend/locales/en/auth.json @@ -0,0 +1,44 @@ +{ + "login": { + "title": "Welcome back", + "subtitle": "Sign in to your account", + "email": "Email", + "password": "Password", + "emailPlaceholder": "you@example.com", + "passwordPlaceholder": "••••••••", + "signIn": "Sign in", + "signingIn": "Signing in...", + "noAccount": "Don't have an account?", + "signUp": "Sign up", + "loginFailed": "Login failed" + }, + "signup": { + "title": "Join with Invite", + "subtitle": "Enter your invite code to get started", + "inviteCode": "Invite Code", + "inviteCodePlaceholder": "word-word-00", + "inviteHint": "Ask your inviter for this code", + "checking": "Checking...", + "continue": "Continue", + "checkingInviteCode": "Checking invite code...", + "createAccount": "Create account", + "createAccountTitle": "Create account", + "createAccountSubtitle": "Using invite:", + "email": "Email", + "emailPlaceholder": "you@example.com", + "password": "Password", + "passwordPlaceholder": "••••••••", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "••••••••", + "creatingAccount": "Creating account...", + "alreadyHaveAccount": "Already have an account?", + "signIn": "Sign in", + "useDifferentCode": "Use a different invite code", + "redirecting": "Redirecting...", + "passwordsDoNotMatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "invalidInviteCode": "Invalid invite code", + "failedToVerify": "Failed to verify invite code", + "registrationFailed": "Registration failed" + } +} diff --git a/frontend/locales/es/auth.json b/frontend/locales/es/auth.json new file mode 100644 index 0000000..39be272 --- /dev/null +++ b/frontend/locales/es/auth.json @@ -0,0 +1,44 @@ +{ + "login": { + "title": "Bienvenido de nuevo", + "subtitle": "Inicia sesión en tu cuenta", + "email": "Correo electrónico", + "password": "Contraseña", + "emailPlaceholder": "tu@ejemplo.com", + "passwordPlaceholder": "••••••••", + "signIn": "Iniciar sesión", + "signingIn": "Iniciando sesión...", + "noAccount": "¿No tienes una cuenta?", + "signUp": "Regístrate", + "loginFailed": "Error al iniciar sesión" + }, + "signup": { + "title": "Únete con Invitación", + "subtitle": "Ingresa tu código de invitación para comenzar", + "inviteCode": "Código de Invitación", + "inviteCodePlaceholder": "palabra-palabra-00", + "inviteHint": "Pide este código a tu invitador", + "checking": "Verificando...", + "continue": "Continuar", + "checkingInviteCode": "Verificando código de invitación...", + "createAccount": "Crear cuenta", + "createAccountTitle": "Crear cuenta", + "createAccountSubtitle": "Usando invitación:", + "email": "Correo electrónico", + "emailPlaceholder": "tu@ejemplo.com", + "password": "Contraseña", + "passwordPlaceholder": "••••••••", + "confirmPassword": "Confirmar Contraseña", + "confirmPasswordPlaceholder": "••••••••", + "creatingAccount": "Creando cuenta...", + "alreadyHaveAccount": "¿Ya tienes una cuenta?", + "signIn": "Iniciar sesión", + "useDifferentCode": "Usar un código de invitación diferente", + "redirecting": "Redirigiendo...", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "passwordTooShort": "La contraseña debe tener al menos 6 caracteres", + "invalidInviteCode": "Código de invitación inválido", + "failedToVerify": "Error al verificar código de invitación", + "registrationFailed": "Error al registrarse" + } +}