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
This commit is contained in:
parent
a5a1a2c1ad
commit
7dd13292a0
9 changed files with 188 additions and 47 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -19,21 +19,21 @@ afterEach(() => cleanup());
|
|||
|
||||
test("renders login form with title", () => {
|
||||
renderWithProviders(<LoginPage />);
|
||||
expect(screen.getByText("Welcome back")).toBeDefined();
|
||||
expect(screen.getByText("Bienvenido de nuevo")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders email and password inputs", () => {
|
||||
renderWithProviders(<LoginPage />);
|
||||
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(<LoginPage />);
|
||||
expect(screen.getByRole("button", { name: "Sign in" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Iniciar sesión" })).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders link to signup", () => {
|
||||
renderWithProviders(<LoginPage />);
|
||||
expect(screen.getByText("Sign up")).toBeDefined();
|
||||
expect(screen.getByText("Regístrate")).toBeDefined();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div style={styles.container}>
|
||||
<div style={styles.card}>
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>Welcome back</h1>
|
||||
<p style={styles.subtitle}>Sign in to your account</p>
|
||||
<h1 style={styles.title}>{t("login.title")}</h1>
|
||||
<p style={styles.subtitle}>{t("login.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={styles.form}>
|
||||
|
|
@ -46,7 +48,7 @@ export default function LoginPage() {
|
|||
|
||||
<div style={styles.field}>
|
||||
<label htmlFor="email" style={styles.label}>
|
||||
Email
|
||||
{t("login.email")}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
|
|
@ -54,14 +56,14 @@ export default function LoginPage() {
|
|||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="you@example.com"
|
||||
placeholder={t("login.emailPlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={styles.field}>
|
||||
<label htmlFor="password" style={styles.label}>
|
||||
Password
|
||||
{t("login.password")}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
|
|
@ -69,7 +71,7 @@ export default function LoginPage() {
|
|||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="••••••••"
|
||||
placeholder={t("login.passwordPlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -82,14 +84,14 @@ export default function LoginPage() {
|
|||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||
{isSubmitting ? t("login.signingIn") : t("login.signIn")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style={styles.footer}>
|
||||
Don't have an account?{" "}
|
||||
{t("login.noAccount")}{" "}
|
||||
<a href="/signup" style={styles.link}>
|
||||
Sign up
|
||||
{t("login.signUp")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div style={{ position: "absolute", top: "1rem", right: "1rem" }}>
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
Redirecting...
|
||||
{t("signup.redirecting")}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,20 +21,20 @@ afterEach(() => cleanup());
|
|||
test("renders signup form with title", () => {
|
||||
renderWithProviders(<SignupPage />);
|
||||
// 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(<SignupPage />);
|
||||
expect(screen.getByLabelText("Invite Code")).toBeDefined();
|
||||
expect(screen.getByLabelText("Código de Invitación")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders continue button", () => {
|
||||
renderWithProviders(<SignupPage />);
|
||||
expect(screen.getByRole("button", { name: "Continue" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Continuar" })).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders link to login", () => {
|
||||
renderWithProviders(<SignupPage />);
|
||||
expect(screen.getByText("Sign in")).toBeDefined();
|
||||
expect(screen.getByText("Iniciar sesión")).toBeDefined();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div style={styles.container}>
|
||||
<div style={styles.card}>
|
||||
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.6)" }}>
|
||||
Checking invite code...
|
||||
{t("signup.checkingInviteCode")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -132,8 +134,8 @@ function SignupContent() {
|
|||
<div style={styles.container}>
|
||||
<div style={styles.card}>
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>Join with Invite</h1>
|
||||
<p style={styles.subtitle}>Enter your invite code to get started</p>
|
||||
<h1 style={styles.title}>{t("signup.title")}</h1>
|
||||
<p style={styles.subtitle}>{t("signup.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleInviteSubmit} style={styles.form}>
|
||||
|
|
@ -141,7 +143,7 @@ function SignupContent() {
|
|||
|
||||
<div style={styles.field}>
|
||||
<label htmlFor="inviteCode" style={styles.label}>
|
||||
Invite Code
|
||||
{t("signup.inviteCode")}
|
||||
</label>
|
||||
<input
|
||||
id="inviteCode"
|
||||
|
|
@ -153,7 +155,7 @@ function SignupContent() {
|
|||
setInviteValid(null);
|
||||
}}
|
||||
style={styles.input}
|
||||
placeholder="word-word-00"
|
||||
placeholder={t("signup.inviteCodePlaceholder")}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
|
|
@ -165,7 +167,7 @@ function SignupContent() {
|
|||
display: "block",
|
||||
}}
|
||||
>
|
||||
Ask your inviter for this code
|
||||
{t("signup.inviteHint")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -177,14 +179,14 @@ function SignupContent() {
|
|||
}}
|
||||
disabled={isCheckingInvite || !inviteCode.trim()}
|
||||
>
|
||||
{isCheckingInvite ? "Checking..." : "Continue"}
|
||||
{isCheckingInvite ? t("signup.checking") : t("signup.continue")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style={styles.footer}>
|
||||
Already have an account?{" "}
|
||||
{t("signup.alreadyHaveAccount")}{" "}
|
||||
<a href="/login" style={styles.link}>
|
||||
Sign in
|
||||
{t("signup.signIn")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -202,9 +204,9 @@ function SignupContent() {
|
|||
<div style={styles.container}>
|
||||
<div style={styles.card}>
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>Create account</h1>
|
||||
<h1 style={styles.title}>{t("signup.createAccountTitle")}</h1>
|
||||
<p style={styles.subtitle}>
|
||||
Using invite:{" "}
|
||||
{t("signup.createAccountSubtitle")}{" "}
|
||||
<code
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
|
|
@ -223,7 +225,7 @@ function SignupContent() {
|
|||
|
||||
<div style={styles.field}>
|
||||
<label htmlFor="email" style={styles.label}>
|
||||
Email
|
||||
{t("signup.email")}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
|
|
@ -231,7 +233,7 @@ function SignupContent() {
|
|||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="you@example.com"
|
||||
placeholder={t("signup.emailPlaceholder")}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
|
|
@ -239,7 +241,7 @@ function SignupContent() {
|
|||
|
||||
<div style={styles.field}>
|
||||
<label htmlFor="password" style={styles.label}>
|
||||
Password
|
||||
{t("signup.password")}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
|
|
@ -247,14 +249,14 @@ function SignupContent() {
|
|||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="••••••••"
|
||||
placeholder={t("signup.passwordPlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={styles.field}>
|
||||
<label htmlFor="confirmPassword" style={styles.label}>
|
||||
Confirm Password
|
||||
{t("signup.confirmPassword")}
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
|
|
@ -262,7 +264,7 @@ function SignupContent() {
|
|||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="••••••••"
|
||||
placeholder={t("signup.confirmPasswordPlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -275,7 +277,7 @@ function SignupContent() {
|
|||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Creating account..." : "Create account"}
|
||||
{isSubmitting ? t("signup.creatingAccount") : t("signup.createAccount")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
|
@ -293,7 +295,7 @@ function SignupContent() {
|
|||
padding: 0,
|
||||
}}
|
||||
>
|
||||
Use a different invite code
|
||||
{t("signup.useDifferentCode")}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
44
frontend/locales/ca/auth.json
Normal file
44
frontend/locales/ca/auth.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
44
frontend/locales/en/auth.json
Normal file
44
frontend/locales/en/auth.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
44
frontend/locales/es/auth.json
Normal file
44
frontend/locales/es/auth.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue