refactor(auth): merge the duplicate login form and localize the auth screens
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 1m8s
CI / tests-integration (push) Successful in 1m53s
CI / tests-unit (push) Successful in 1m59s
CI / tests-ui (push) Successful in 2m42s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 4m33s
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 1m8s
CI / tests-integration (push) Successful in 1m53s
CI / tests-unit (push) Successful in 1m59s
CI / tests-ui (push) Successful in 2m42s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 4m33s
`home-login-form.tsx` and `login-form.tsx` were two ~240-line near-identical components. Delete the former and give `LoginForm` a `variant` prop: - `variant="page"` sr-only labels plus the register/forgot footer (/login) - `variant="compact"` visible labels, no footer (homepage sidebar) Field ids now come from `useId()`, so the two usages can never collide, and the hardcoded "Show"/"Hide"/"Loading" strings are translated. Localization of the login and register screens: - `home-login-form.tsx` was entirely hardcoded English. - `passwordStrength()` returned hardcoded "Weak"/"Fair"/"Good"/"Strong". - `register.ts` returned only English strings. It now returns a locale-independent `code` next to the message, and the form renders `t(code)` with the English string as a fallback. - Backfilled the new keys across all 25 locales, plus the login/register strings that were still English in most of them. `ar`, `fi` and `ja` had their entire login/register namespace in English and are now filled in. Locale parity stays at 0 missing keys, as `i18n:check` requires. Copy that did not match the enforced rules: the UI advertised "min 8 chars" (EN) / "min 6 tekens" (NL) while registration requires 12 characters plus an uppercase, a lowercase, a digit and a special character. Corrected in every locale. `password-reset.ts` enforced only 6 characters and is raised to 12 to match registration. Accessibility: `login-form.tsx` had no `<label>`, no `id` and no `required` on any field. All three are now present, and error banners are announced with `role="alert"`. Adds `src/i18n/auth-messages.test.ts`, which asserts every `RegisterErrorCode` resolves to a non-empty message in all 25 locales; verified it fails when a key is removed. The existing register tests now also assert the error `code`.
This commit is contained in:
1 parent
3d828a61ab
commit
6bffc53779
33 files changed
+1756
-842
No files matched your search
@@ -5,7 +5,11 @@ import Script from "next/script";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useActionState, useEffect, useRef, useState } from "react";
|
||||
import { type RegisterState, register } from "@/actions/register";
|
||||
import {
|
||||
type RegisterErrorCode,
|
||||
type RegisterState,
|
||||
register,
|
||||
} from "@/actions/register";
|
||||
import Link from "@/components/link";
|
||||
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
|
||||
|
||||
@@ -16,7 +20,10 @@ interface RegisterFormProps {
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
function passwordStrength(pw: string): {
|
||||
function passwordStrength(
|
||||
pw: string,
|
||||
t: (key: string) => string,
|
||||
): {
|
||||
score: number;
|
||||
label: string;
|
||||
color: string;
|
||||
@@ -28,10 +35,10 @@ function passwordStrength(pw: string): {
|
||||
if (/\d/.test(pw)) score += 1;
|
||||
if (/[^a-zA-Z0-9]/.test(pw)) score += 1;
|
||||
|
||||
if (score <= 1) return { score, label: "Weak", color: "#ef4444" };
|
||||
if (score <= 2) return { score, label: "Fair", color: "#f59e0b" };
|
||||
if (score <= 3) return { score, label: "Good", color: "#22c55e" };
|
||||
return { score: 5, label: "Strong", color: "#16a34a" };
|
||||
if (score <= 1) return { score, label: t("strengthWeak"), color: "#ef4444" };
|
||||
if (score <= 2) return { score, label: t("strengthFair"), color: "#f59e0b" };
|
||||
if (score <= 3) return { score, label: t("strengthGood"), color: "#22c55e" };
|
||||
return { score: 5, label: t("strengthStrong"), color: "#16a34a" };
|
||||
}
|
||||
|
||||
export function RegisterForm({
|
||||
@@ -46,6 +53,21 @@ export function RegisterForm({
|
||||
RegisterState,
|
||||
FormData
|
||||
>(register, { error: null, ok: false });
|
||||
|
||||
/**
|
||||
* Prefer the locale-independent `code` so the message follows the visitor's
|
||||
* language, falling back to the server's English string when a code has no
|
||||
* translation yet.
|
||||
*/
|
||||
const translateErrorCode = (code: RegisterErrorCode | undefined) => {
|
||||
if (!code) return null;
|
||||
try {
|
||||
return t(code);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const errorMessage = error ?? translateErrorCode(state.code) ?? state.error;
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
const [termsError, setTermsError] = useState(false);
|
||||
const termsRef = useRef<HTMLDivElement>(null);
|
||||
@@ -58,7 +80,7 @@ export function RegisterForm({
|
||||
);
|
||||
const autoLoginStartedRef = useRef(false);
|
||||
const [referralCode, setReferralCode] = useState("");
|
||||
const strength = passwordStrength(password);
|
||||
const strength = passwordStrength(password, t);
|
||||
|
||||
// The invite link is `/register?ref=<username>`; read it client-side so the
|
||||
// attribution travels with the sign-up form without server state.
|
||||
@@ -186,12 +208,14 @@ export function RegisterForm({
|
||||
borderRadius: "0 0 12px 12px",
|
||||
}}
|
||||
>
|
||||
{(error || state.error) && (
|
||||
{errorMessage && (
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
className="animate-fade-in-up p-3 rounded-lg text-sm font-semibold text-primary-foreground"
|
||||
style={{ backgroundColor: "var(--color-danger)" }}
|
||||
>
|
||||
{error || state.error}
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user