From 7aa63d79dff5be5664d4efbc81d9e17fc17d702d Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Wed, 19 Nov 2025 03:02:13 +0100 Subject: [PATCH] Implement extensive localization improvements across forms and components - Refactored `it.json` translations with added keys for authentication, admin panel, and settings. - Updated authentication forms (`LoginForm`, `RegisterForm`, `PasswordResetConfirmForm`) to use localized strings via `next-intl`. - Enhanced password validation schemas with dynamic translations and refined error messages. - Adjusted `Header` and related components to include localized navigation and status elements. - Improved placeholder hints, button labels, and inline validation messages for seamless localization. --- frontend/messages/en.json | 126 +++++++++++++++--- frontend/messages/it.json | 118 +++++++++++++--- frontend/src/components/auth/LoginForm.tsx | 53 +++++--- .../auth/PasswordResetConfirmForm.tsx | 68 +++++----- .../auth/PasswordResetRequestForm.tsx | 39 +++--- frontend/src/components/auth/RegisterForm.tsx | 97 ++++++++------ frontend/src/components/layout/Header.tsx | 14 +- .../settings/PasswordChangeForm.tsx | 67 +++++----- .../settings/ProfileSettingsForm.tsx | 53 ++++---- 9 files changed, 421 insertions(+), 214 deletions(-) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 63e3904..a147cb4 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -25,11 +25,14 @@ "dashboard": "Dashboard", "settings": "Settings", "profile": "Profile", - "logout": "Logout", + "logout": "Log out", + "loggingOut": "Logging out...", "login": "Login", "register": "Register", "demos": "Demos", - "design": "Design System" + "design": "Design System", + "admin": "Admin", + "adminPanel": "Admin Panel" }, "auth": { "login": { @@ -41,11 +44,13 @@ "passwordPlaceholder": "Enter your password", "rememberMe": "Remember me", "forgotPassword": "Forgot password?", - "loginButton": "Sign In", + "loginButton": "Sign in", + "loginButtonLoading": "Signing in...", "noAccount": "Don't have an account?", "registerLink": "Sign up", "success": "Successfully logged in", - "error": "Invalid email or password" + "error": "Invalid email or password", + "unexpectedError": "An unexpected error occurred. Please try again." }, "register": { "title": "Create an account", @@ -53,63 +58,142 @@ "firstNameLabel": "First Name", "firstNamePlaceholder": "John", "lastNameLabel": "Last Name", - "lastNamePlaceholder": "Doe", + "lastNamePlaceholder": "Doe (optional)", "emailLabel": "Email", - "emailPlaceholder": "name@example.com", + "emailPlaceholder": "you@example.com", "passwordLabel": "Password", "passwordPlaceholder": "Create a strong password", "confirmPasswordLabel": "Confirm Password", - "confirmPasswordPlaceholder": "Re-enter your password", + "confirmPasswordPlaceholder": "Confirm your password", "phoneLabel": "Phone Number", "phonePlaceholder": "+1 (555) 000-0000", - "registerButton": "Create Account", + "registerButton": "Create account", + "registerButtonLoading": "Creating account...", "hasAccount": "Already have an account?", "loginLink": "Sign in", "success": "Account created successfully", - "error": "Failed to create account" + "error": "Failed to create account", + "unexpectedError": "An unexpected error occurred. Please try again.", + "passwordRequirements": { + "minLength": "At least 8 characters", + "hasNumber": "Contains a number", + "hasUppercase": "Contains an uppercase letter" + }, + "required": "*", + "firstNameRequired": "First name is required", + "firstNameMinLength": "First name must be at least 2 characters", + "firstNameMaxLength": "First name must not exceed 50 characters", + "lastNameMaxLength": "Last name must not exceed 50 characters", + "passwordRequired": "Password is required", + "passwordMinLength": "Password must be at least 8 characters", + "passwordNumber": "Password must contain at least one number", + "passwordUppercase": "Password must contain at least one uppercase letter", + "confirmPasswordRequired": "Please confirm your password", + "passwordMismatch": "Passwords do not match" }, "passwordReset": { "title": "Reset your password", "subtitle": "Enter your email address and we'll send you a reset link", + "instructions": "Enter your email address and we'll send you instructions to reset your password.", "emailLabel": "Email", - "emailPlaceholder": "name@example.com", - "sendButton": "Send Reset Link", + "emailPlaceholder": "you@example.com", + "sendButton": "Send Reset Instructions", + "sendButtonLoading": "Sending...", "backToLogin": "Back to login", - "success": "Password reset link sent to your email", - "error": "Failed to send reset link" + "rememberPassword": "Remember your password?", + "success": "Password reset instructions have been sent to your email address. Please check your inbox.", + "error": "Failed to send reset link", + "unexpectedError": "An unexpected error occurred. Please try again.", + "required": "*" }, "passwordChange": { "title": "Change Password", "currentPasswordLabel": "Current Password", "newPasswordLabel": "New Password", + "newPasswordPlaceholder": "Enter new password", "confirmPasswordLabel": "Confirm New Password", + "confirmPasswordPlaceholder": "Re-enter new password", "changeButton": "Change Password", "success": "Password changed successfully", "error": "Failed to change password" + }, + "passwordResetConfirm": { + "title": "Reset Password", + "instructions": "Enter your new password below. Make sure it meets all security requirements.", + "newPasswordLabel": "New Password", + "newPasswordPlaceholder": "Enter new password", + "confirmPasswordLabel": "Confirm Password", + "confirmPasswordPlaceholder": "Re-enter new password", + "resetButton": "Reset Password", + "resetButtonLoading": "Resetting Password...", + "rememberPassword": "Remember your password?", + "backToLogin": "Back to login", + "success": "Your password has been successfully reset. You can now log in with your new password.", + "unexpectedError": "An unexpected error occurred. Please try again.", + "required": "*", + "tokenRequired": "Reset token is required", + "passwordRequired": "New password is required", + "passwordMinLength": "Password must be at least 8 characters", + "passwordNumber": "Password must contain at least one number", + "passwordUppercase": "Password must contain at least one uppercase letter", + "confirmPasswordRequired": "Please confirm your password", + "passwordMismatch": "Passwords do not match", + "passwordRequirements": { + "minLength": "At least 8 characters", + "hasNumber": "Contains a number", + "hasUppercase": "Contains an uppercase letter" + } } }, "settings": { "title": "Settings", "profile": { - "title": "Profile", - "subtitle": "Manage your profile information", + "title": "Profile Information", + "subtitle": "Update your personal information. Your email address is read-only.", "firstNameLabel": "First Name", + "firstNamePlaceholder": "John", "lastNameLabel": "Last Name", + "lastNamePlaceholder": "Doe", "emailLabel": "Email", + "emailDescription": "Your email address cannot be changed from this form", "phoneLabel": "Phone Number", - "updateButton": "Update Profile", + "updateButton": "Save Changes", + "updateButtonLoading": "Saving...", + "resetButton": "Reset", "success": "Profile updated successfully", - "error": "Failed to update profile" + "error": "Failed to update profile", + "unexpectedError": "An unexpected error occurred. Please try again.", + "firstNameRequired": "First name is required", + "firstNameMinLength": "First name must be at least 2 characters", + "firstNameMaxLength": "First name must not exceed 50 characters", + "lastNameMaxLength": "Last name must not exceed 50 characters", + "emailInvalid": "Invalid email address" }, "password": { - "title": "Password", - "subtitle": "Change your account password", + "title": "Change Password", + "subtitle": "Update your password to keep your account secure. Make sure it's strong and unique.", "currentPasswordLabel": "Current Password", + "currentPasswordPlaceholder": "Enter your current password", "newPasswordLabel": "New Password", + "newPasswordPlaceholder": "Enter your new password", + "newPasswordDescription": "At least 8 characters with uppercase, lowercase, number, and special character", "confirmPasswordLabel": "Confirm New Password", - "updateButton": "Update Password", + "confirmPasswordPlaceholder": "Confirm your new password", + "updateButton": "Change Password", + "updateButtonLoading": "Changing Password...", + "cancelButton": "Cancel", "success": "Password updated successfully", - "error": "Failed to update password" + "error": "Failed to update password", + "unexpectedError": "An unexpected error occurred. Please try again.", + "currentPasswordRequired": "Current password is required", + "newPasswordRequired": "New password is required", + "newPasswordMinLength": "Password must be at least 8 characters", + "newPasswordNumber": "Password must contain at least one number", + "newPasswordUppercase": "Password must contain at least one uppercase letter", + "newPasswordLowercase": "Password must contain at least one lowercase letter", + "newPasswordSpecial": "Password must contain at least one special character", + "confirmPasswordRequired": "Please confirm your new password", + "passwordMismatch": "Passwords do not match" }, "sessions": { "title": "Sessions", diff --git a/frontend/messages/it.json b/frontend/messages/it.json index dd1c414..14fb4f9 100644 --- a/frontend/messages/it.json +++ b/frontend/messages/it.json @@ -26,10 +26,13 @@ "settings": "Impostazioni", "profile": "Profilo", "logout": "Disconnetti", + "loggingOut": "Disconnessione...", "login": "Accedi", "register": "Registrati", "demos": "Demo", - "design": "Design System" + "design": "Design System", + "admin": "Admin", + "adminPanel": "Pannello Admin" }, "auth": { "login": { @@ -42,10 +45,12 @@ "rememberMe": "Ricordami", "forgotPassword": "Password dimenticata?", "loginButton": "Accedi", + "loginButtonLoading": "Accesso in corso...", "noAccount": "Non hai un account?", "registerLink": "Registrati", "success": "Accesso effettuato con successo", - "error": "Email o password non validi" + "error": "Email o password non validi", + "unexpectedError": "Si è verificato un errore imprevisto. Riprova." }, "register": { "title": "Crea un account", @@ -53,63 +58,142 @@ "firstNameLabel": "Nome", "firstNamePlaceholder": "Mario", "lastNameLabel": "Cognome", - "lastNamePlaceholder": "Rossi", + "lastNamePlaceholder": "Rossi (facoltativo)", "emailLabel": "Email", "emailPlaceholder": "nome@esempio.com", "passwordLabel": "Password", "passwordPlaceholder": "Crea una password sicura", "confirmPasswordLabel": "Conferma Password", - "confirmPasswordPlaceholder": "Reinserisci la tua password", + "confirmPasswordPlaceholder": "Conferma la tua password", "phoneLabel": "Numero di Telefono", "phonePlaceholder": "+39 123 456 7890", - "registerButton": "Crea Account", + "registerButton": "Crea account", + "registerButtonLoading": "Creazione account...", "hasAccount": "Hai già un account?", "loginLink": "Accedi", "success": "Account creato con successo", - "error": "Impossibile creare l'account" + "error": "Impossibile creare l'account", + "unexpectedError": "Si è verificato un errore imprevisto. Riprova.", + "passwordRequirements": { + "minLength": "Almeno 8 caratteri", + "hasNumber": "Contiene un numero", + "hasUppercase": "Contiene una lettera maiuscola" + }, + "required": "*", + "firstNameRequired": "Il nome è obbligatorio", + "firstNameMinLength": "Il nome deve essere di almeno 2 caratteri", + "firstNameMaxLength": "Il nome non deve superare i 50 caratteri", + "lastNameMaxLength": "Il cognome non deve superare i 50 caratteri", + "passwordRequired": "La password è obbligatoria", + "passwordMinLength": "La password deve essere di almeno 8 caratteri", + "passwordNumber": "La password deve contenere almeno un numero", + "passwordUppercase": "La password deve contenere almeno una lettera maiuscola", + "confirmPasswordRequired": "Conferma la tua password", + "passwordMismatch": "Le password non corrispondono" }, "passwordReset": { "title": "Reimposta la tua password", "subtitle": "Inserisci il tuo indirizzo email e ti invieremo un link per reimpostare la password", + "instructions": "Inserisci il tuo indirizzo email e ti invieremo le istruzioni per reimpostare la password.", "emailLabel": "Email", "emailPlaceholder": "nome@esempio.com", - "sendButton": "Invia Link di Reset", + "sendButton": "Invia Istruzioni di Reset", + "sendButtonLoading": "Invio in corso...", "backToLogin": "Torna al login", - "success": "Link di reset password inviato alla tua email", - "error": "Impossibile inviare il link di reset" + "rememberPassword": "Ricordi la tua password?", + "success": "Le istruzioni per il reset della password sono state inviate al tuo indirizzo email. Controlla la tua casella di posta.", + "error": "Impossibile inviare il link di reset", + "unexpectedError": "Si è verificato un errore imprevisto. Riprova.", + "required": "*" }, "passwordChange": { "title": "Cambia Password", "currentPasswordLabel": "Password Attuale", "newPasswordLabel": "Nuova Password", + "newPasswordPlaceholder": "Inserisci nuova password", "confirmPasswordLabel": "Conferma Nuova Password", + "confirmPasswordPlaceholder": "Reinserisci nuova password", "changeButton": "Cambia Password", "success": "Password cambiata con successo", "error": "Impossibile cambiare la password" + }, + "passwordResetConfirm": { + "title": "Reimposta Password", + "instructions": "Inserisci la tua nuova password qui sotto. Assicurati che soddisfi tutti i requisiti di sicurezza.", + "newPasswordLabel": "Nuova Password", + "newPasswordPlaceholder": "Inserisci nuova password", + "confirmPasswordLabel": "Conferma Password", + "confirmPasswordPlaceholder": "Reinserisci nuova password", + "resetButton": "Reimposta Password", + "resetButtonLoading": "Reimpostazione Password...", + "rememberPassword": "Ricordi la tua password?", + "backToLogin": "Torna al login", + "success": "La tua password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password.", + "unexpectedError": "Si è verificato un errore imprevisto. Riprova.", + "required": "*", + "tokenRequired": "Token di reset è obbligatorio", + "passwordRequired": "La nuova password è obbligatoria", + "passwordMinLength": "La password deve essere di almeno 8 caratteri", + "passwordNumber": "La password deve contenere almeno un numero", + "passwordUppercase": "La password deve contenere almeno una lettera maiuscola", + "confirmPasswordRequired": "Conferma la tua password", + "passwordMismatch": "Le password non corrispondono", + "passwordRequirements": { + "minLength": "Almeno 8 caratteri", + "hasNumber": "Contiene un numero", + "hasUppercase": "Contiene una lettera maiuscola" + } } }, "settings": { "title": "Impostazioni", "profile": { - "title": "Profilo", - "subtitle": "Gestisci le informazioni del tuo profilo", + "title": "Informazioni Profilo", + "subtitle": "Aggiorna le tue informazioni personali. Il tuo indirizzo email è di sola lettura.", "firstNameLabel": "Nome", + "firstNamePlaceholder": "Mario", "lastNameLabel": "Cognome", + "lastNamePlaceholder": "Rossi", "emailLabel": "Email", + "emailDescription": "Il tuo indirizzo email non può essere modificato da questo modulo", "phoneLabel": "Numero di Telefono", - "updateButton": "Aggiorna Profilo", + "updateButton": "Salva Modifiche", + "updateButtonLoading": "Salvataggio...", + "resetButton": "Ripristina", "success": "Profilo aggiornato con successo", - "error": "Impossibile aggiornare il profilo" + "error": "Impossibile aggiornare il profilo", + "unexpectedError": "Si è verificato un errore imprevisto. Riprova.", + "firstNameRequired": "Il nome è obbligatorio", + "firstNameMinLength": "Il nome deve essere di almeno 2 caratteri", + "firstNameMaxLength": "Il nome non deve superare i 50 caratteri", + "lastNameMaxLength": "Il cognome non deve superare i 50 caratteri", + "emailInvalid": "Indirizzo email non valido" }, "password": { - "title": "Password", - "subtitle": "Cambia la password del tuo account", + "title": "Cambia Password", + "subtitle": "Aggiorna la tua password per mantenere il tuo account sicuro. Assicurati che sia forte e univoca.", "currentPasswordLabel": "Password Attuale", + "currentPasswordPlaceholder": "Inserisci la tua password attuale", "newPasswordLabel": "Nuova Password", + "newPasswordPlaceholder": "Inserisci la tua nuova password", + "newPasswordDescription": "Almeno 8 caratteri con maiuscole, minuscole, numeri e caratteri speciali", "confirmPasswordLabel": "Conferma Nuova Password", - "updateButton": "Aggiorna Password", + "confirmPasswordPlaceholder": "Conferma la tua nuova password", + "updateButton": "Cambia Password", + "updateButtonLoading": "Cambio Password...", + "cancelButton": "Annulla", "success": "Password aggiornata con successo", - "error": "Impossibile aggiornare la password" + "error": "Impossibile aggiornare la password", + "unexpectedError": "Si è verificato un errore imprevisto. Riprova.", + "currentPasswordRequired": "La password attuale è obbligatoria", + "newPasswordRequired": "La nuova password è obbligatoria", + "newPasswordMinLength": "La password deve essere di almeno 8 caratteri", + "newPasswordNumber": "La password deve contenere almeno un numero", + "newPasswordUppercase": "La password deve contenere almeno una lettera maiuscola", + "newPasswordLowercase": "La password deve contenere almeno una lettera minuscola", + "newPasswordSpecial": "La password deve contenere almeno un carattere speciale", + "confirmPasswordRequired": "Conferma la tua nuova password", + "passwordMismatch": "Le password non corrispondono" }, "sessions": { "title": "Sessioni", diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index 2c8de97..e941700 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; +import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -23,17 +24,18 @@ import config from '@/config/app.config'; // Validation Schema // ============================================================================ -const loginSchema = z.object({ - email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), - password: z - .string() - .min(1, 'Password is required') - .min(8, 'Password must be at least 8 characters') - .regex(/[0-9]/, 'Password must contain at least one number') - .regex(/[A-Z]/, 'Password must contain at least one uppercase letter'), -}); +const createLoginSchema = (t: (key: string) => string) => + z.object({ + email: z.string().min(1, t('validation.required')).email(t('validation.email')), + password: z + .string() + .min(1, t('validation.required')) + .min(8, t('validation.minLength').replace('{count}', '8')) + .regex(/[0-9]/, t('errors.validation.passwordWeak')) + .regex(/[A-Z]/, t('errors.validation.passwordWeak')), + }); -type LoginFormData = z.infer; +type LoginFormData = z.infer>; // ============================================================================ // Component @@ -74,9 +76,22 @@ export function LoginForm({ showPasswordResetLink = true, className, }: LoginFormProps) { + const t = useTranslations('auth.login'); + const tValidation = useTranslations('validation'); + const tErrors = useTranslations('errors.validation'); const [serverError, setServerError] = useState(null); const loginMutation = useLogin(); + const loginSchema = createLoginSchema((key: string) => { + if (key.startsWith('validation.')) { + return tValidation(key.replace('validation.', '')); + } + if (key.startsWith('errors.validation.')) { + return tErrors(key.replace('errors.validation.', '')); + } + return key; + }); + const form = useForm({ resolver: zodResolver(loginSchema), mode: 'onBlur', @@ -116,7 +131,7 @@ export function LoginForm({ }); } else { // Unexpected error format - setServerError('An unexpected error occurred. Please try again.'); + setServerError(t('unexpectedError')); } } }; @@ -135,11 +150,11 @@ export function LoginForm({ {/* Email Field */}
- +
- + {showPasswordResetLink && ( - Forgot password? + {t('forgotPassword')} )}
- {isSubmitting ? 'Signing in...' : 'Sign in'} + {isSubmitting ? t('loginButtonLoading') : t('loginButton')} {/* Registration Link */} {showRegisterLink && config.features.enableRegistration && (

- Don't have an account?{' '} + {t('noAccount')}{' '} - Sign up + {t('registerLink')}

)} diff --git a/frontend/src/components/auth/PasswordResetConfirmForm.tsx b/frontend/src/components/auth/PasswordResetConfirmForm.tsx index a0f2450..edf341f 100644 --- a/frontend/src/components/auth/PasswordResetConfirmForm.tsx +++ b/frontend/src/components/auth/PasswordResetConfirmForm.tsx @@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; +import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -22,23 +23,24 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro // Validation Schema // ============================================================================ -const resetConfirmSchema = z - .object({ - token: z.string().min(1, 'Reset token is required'), - new_password: z - .string() - .min(1, 'New password is required') - .min(8, 'Password must be at least 8 characters') - .regex(/[0-9]/, 'Password must contain at least one number') - .regex(/[A-Z]/, 'Password must contain at least one uppercase letter'), - confirm_password: z.string().min(1, 'Please confirm your password'), - }) - .refine((data) => data.new_password === data.confirm_password, { - message: 'Passwords do not match', - path: ['confirm_password'], - }); +const createResetConfirmSchema = (t: (key: string) => string) => + z + .object({ + token: z.string().min(1, t('tokenRequired')), + new_password: z + .string() + .min(1, t('passwordRequired')) + .min(8, t('passwordMinLength')) + .regex(/[0-9]/, t('passwordNumber')) + .regex(/[A-Z]/, t('passwordUppercase')), + confirm_password: z.string().min(1, t('confirmPasswordRequired')), + }) + .refine((data) => data.new_password === data.confirm_password, { + message: t('passwordMismatch'), + path: ['confirm_password'], + }); -type ResetConfirmFormData = z.infer; +type ResetConfirmFormData = z.infer>; // ============================================================================ // Helper Functions @@ -104,10 +106,13 @@ export function PasswordResetConfirmForm({ showLoginLink = true, className, }: PasswordResetConfirmFormProps) { + const t = useTranslations('auth.passwordResetConfirm'); const [serverError, setServerError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const resetMutation = usePasswordResetConfirm(); + const resetConfirmSchema = createResetConfirmSchema((key: string) => t(key)); + const form = useForm({ resolver: zodResolver(resetConfirmSchema), defaultValues: { @@ -134,9 +139,7 @@ export function PasswordResetConfirmForm({ }); // Show success message - setSuccessMessage( - 'Your password has been successfully reset. You can now log in with your new password.' - ); + setSuccessMessage(t('success')); // Reset form form.reset({ token, new_password: '', confirm_password: '' }); @@ -161,7 +164,7 @@ export function PasswordResetConfirmForm({ }); } else { // Unexpected error format - setServerError('An unexpected error occurred. Please try again.'); + setServerError(t('unexpectedError')); } } }; @@ -186,9 +189,7 @@ export function PasswordResetConfirmForm({ )} {/* Instructions */} -

- Enter your new password below. Make sure it meets all security requirements. -

+

{t('instructions')}

{/* Hidden Token Field (for form submission) */} @@ -196,12 +197,12 @@ export function PasswordResetConfirmForm({ {/* New Password Field */}
- {passwordStrength.hasMinLength ? '✓' : '○'} At least 8 characters + {passwordStrength.hasMinLength ? '✓' : '○'} {t('passwordRequirements.minLength')}
  • - {passwordStrength.hasNumber ? '✓' : '○'} Contains a number + {passwordStrength.hasNumber ? '✓' : '○'} {t('passwordRequirements.hasNumber')}
  • - {passwordStrength.hasUppercase ? '✓' : '○'} Contains an uppercase letter + {passwordStrength.hasUppercase ? '✓' : '○'}{' '} + {t('passwordRequirements.hasUppercase')}
  • @@ -268,12 +270,12 @@ export function PasswordResetConfirmForm({ {/* Confirm Password Field */}
    - {isSubmitting ? 'Resetting Password...' : 'Reset Password'} + {isSubmitting ? t('resetButtonLoading') : t('resetButton')} {/* Login Link */} {showLoginLink && (

    - Remember your password?{' '} + {t('rememberPassword')}{' '} - Back to login + {t('backToLogin')}

    )} diff --git a/frontend/src/components/auth/PasswordResetRequestForm.tsx b/frontend/src/components/auth/PasswordResetRequestForm.tsx index 5eec5e3..96f1fb3 100644 --- a/frontend/src/components/auth/PasswordResetRequestForm.tsx +++ b/frontend/src/components/auth/PasswordResetRequestForm.tsx @@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; +import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -22,11 +23,12 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro // Validation Schema // ============================================================================ -const resetRequestSchema = z.object({ - email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), -}); +const createResetRequestSchema = (t: (key: string) => string) => + z.object({ + email: z.string().min(1, t('validation.required')).email(t('validation.email')), + }); -type ResetRequestFormData = z.infer; +type ResetRequestFormData = z.infer>; // ============================================================================ // Component @@ -64,10 +66,19 @@ export function PasswordResetRequestForm({ showLoginLink = true, className, }: PasswordResetRequestFormProps) { + const t = useTranslations('auth.passwordReset'); + const tValidation = useTranslations('validation'); const [serverError, setServerError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const resetMutation = usePasswordResetRequest(); + const resetRequestSchema = createResetRequestSchema((key: string) => { + if (key.startsWith('validation.')) { + return tValidation(key.replace('validation.', '')); + } + return t(key); + }); + const form = useForm({ resolver: zodResolver(resetRequestSchema), defaultValues: { @@ -86,9 +97,7 @@ export function PasswordResetRequestForm({ await resetMutation.mutateAsync({ email: data.email }); // Show success message - setSuccessMessage( - 'Password reset instructions have been sent to your email address. Please check your inbox.' - ); + setSuccessMessage(t('success')); // Reset form form.reset(); @@ -113,7 +122,7 @@ export function PasswordResetRequestForm({ }); } else { // Unexpected error format - setServerError('An unexpected error occurred. Please try again.'); + setServerError(t('unexpectedError')); } } }; @@ -138,19 +147,17 @@ export function PasswordResetRequestForm({ )} {/* Instructions */} -

    - Enter your email address and we'll send you instructions to reset your password. -

    +

    {t('instructions')}

    {/* Email Field */}
    - {isSubmitting ? 'Sending...' : 'Send Reset Instructions'} + {isSubmitting ? t('sendButtonLoading') : t('sendButton')} {/* Login Link */} {showLoginLink && (

    - Remember your password?{' '} + {t('rememberPassword')}{' '} - Back to login + {t('backToLogin')}

    )} diff --git a/frontend/src/components/auth/RegisterForm.tsx b/frontend/src/components/auth/RegisterForm.tsx index 2703aee..f67ae5a 100644 --- a/frontend/src/components/auth/RegisterForm.tsx +++ b/frontend/src/components/auth/RegisterForm.tsx @@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; +import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -23,33 +24,34 @@ import config from '@/config/app.config'; // Validation Schema // ============================================================================ -const registerSchema = z - .object({ - email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), - first_name: z - .string() - .min(1, 'First name is required') - .min(2, 'First name must be at least 2 characters') - .max(50, 'First name must not exceed 50 characters'), - last_name: z - .string() - .max(50, 'Last name must not exceed 50 characters') - .optional() - .or(z.literal('')), // Allow empty string - password: z - .string() - .min(1, 'Password is required') - .min(8, 'Password must be at least 8 characters') - .regex(/[0-9]/, 'Password must contain at least one number') - .regex(/[A-Z]/, 'Password must contain at least one uppercase letter'), - confirmPassword: z.string().min(1, 'Please confirm your password'), - }) - .refine((data) => data.password === data.confirmPassword, { - message: 'Passwords do not match', - path: ['confirmPassword'], - }); +const createRegisterSchema = (t: (key: string) => string) => + z + .object({ + email: z.string().min(1, t('validation.required')).email(t('validation.email')), + first_name: z + .string() + .min(1, t('firstNameRequired')) + .min(2, t('firstNameMinLength')) + .max(50, t('firstNameMaxLength')), + last_name: z + .string() + .max(50, t('lastNameMaxLength')) + .optional() + .or(z.literal('')), // Allow empty string + password: z + .string() + .min(1, t('passwordRequired')) + .min(8, t('passwordMinLength')) + .regex(/[0-9]/, t('passwordNumber')) + .regex(/[A-Z]/, t('passwordUppercase')), + confirmPassword: z.string().min(1, t('confirmPasswordRequired')), + }) + .refine((data) => data.password === data.confirmPassword, { + message: t('passwordMismatch'), + path: ['confirmPassword'], + }); -type RegisterFormData = z.infer; +type RegisterFormData = z.infer>; // ============================================================================ // Component @@ -84,9 +86,18 @@ interface RegisterFormProps { * ``` */ export function RegisterForm({ onSuccess, showLoginLink = true, className }: RegisterFormProps) { + const t = useTranslations('auth.register'); + const tValidation = useTranslations('validation'); const [serverError, setServerError] = useState(null); const registerMutation = useRegister(); + const registerSchema = createRegisterSchema((key: string) => { + if (key.startsWith('validation.')) { + return tValidation(key.replace('validation.', '')); + } + return t(key); + }); + const form = useForm({ resolver: zodResolver(registerSchema), mode: 'onBlur', @@ -133,7 +144,7 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg }); } else { // Unexpected error format - setServerError('An unexpected error occurred. Please try again.'); + setServerError(t('unexpectedError')); } } }; @@ -159,12 +170,12 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg {/* First Name Field */}
    - + - {hasMinLength ? '✓' : '○'} At least 8 characters + {hasMinLength ? '✓' : '○'} {t('passwordRequirements.minLength')}

    - {hasNumber ? '✓' : '○'} Contains a number + {hasNumber ? '✓' : '○'} {t('passwordRequirements.hasNumber')}

    - {hasUppercase ? '✓' : '○'} Contains an uppercase letter + {hasUppercase ? '✓' : '○'} {t('passwordRequirements.hasUppercase')}

    )} @@ -276,12 +287,12 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg {/* Confirm Password Field */}
    - {isSubmitting ? 'Creating account...' : 'Create account'} + {isSubmitting ? t('registerButtonLoading') : t('registerButton')} {/* Login Link */} {showLoginLink && (

    - Already have an account?{' '} + {t('hasAccount')}{' '} - Sign in + {t('loginLink')}

    )} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index d30226d..f42943d 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -10,6 +10,7 @@ import { Link } from '@/lib/i18n/routing'; import { usePathname } from '@/lib/i18n/routing'; import { useAuth } from '@/lib/auth/AuthContext'; import { useLogout } from '@/lib/api/hooks/useAuth'; +import { useTranslations } from 'next-intl'; import { DropdownMenu, DropdownMenuContent, @@ -68,6 +69,7 @@ function NavLink({ } export function Header() { + const t = useTranslations('navigation'); const { user } = useAuth(); const { mutate: logout, isPending: isLoggingOut } = useLogout(); @@ -87,9 +89,9 @@ export function Header() { {/* Navigation Links */}
    @@ -120,20 +122,20 @@ export function Header() { - Profile + {t('profile')} - Settings + {t('settings')} {user?.is_superuser && ( - Admin Panel + {t('adminPanel')} )} @@ -144,7 +146,7 @@ export function Header() { disabled={isLoggingOut} > - {isLoggingOut ? 'Logging out...' : 'Log out'} + {isLoggingOut ? t('loggingOut') : t('logout')} diff --git a/frontend/src/components/settings/PasswordChangeForm.tsx b/frontend/src/components/settings/PasswordChangeForm.tsx index 494736b..2e3ea61 100644 --- a/frontend/src/components/settings/PasswordChangeForm.tsx +++ b/frontend/src/components/settings/PasswordChangeForm.tsx @@ -11,6 +11,7 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; +import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert } from '@/components/ui/alert'; @@ -22,25 +23,26 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro // Validation Schema // ============================================================================ -const passwordChangeSchema = z - .object({ - current_password: z.string().min(1, 'Current password is required'), - new_password: z - .string() - .min(1, 'New password is required') - .min(8, 'Password must be at least 8 characters') - .regex(/[0-9]/, 'Password must contain at least one number') - .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') - .regex(/[a-z]/, 'Password must contain at least one lowercase letter') - .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'), - confirm_password: z.string().min(1, 'Please confirm your new password'), - }) - .refine((data) => data.new_password === data.confirm_password, { - message: 'Passwords do not match', - path: ['confirm_password'], - }); +const createPasswordChangeSchema = (t: (key: string) => string) => + z + .object({ + current_password: z.string().min(1, t('currentPasswordRequired')), + new_password: z + .string() + .min(1, t('newPasswordRequired')) + .min(8, t('newPasswordMinLength')) + .regex(/[0-9]/, t('newPasswordNumber')) + .regex(/[A-Z]/, t('newPasswordUppercase')) + .regex(/[a-z]/, t('newPasswordLowercase')) + .regex(/[^A-Za-z0-9]/, t('newPasswordSpecial')), + confirm_password: z.string().min(1, t('confirmPasswordRequired')), + }) + .refine((data) => data.new_password === data.confirm_password, { + message: t('passwordMismatch'), + path: ['confirm_password'], + }); -type PasswordChangeFormData = z.infer; +type PasswordChangeFormData = z.infer>; // ============================================================================ // Component @@ -72,6 +74,7 @@ interface PasswordChangeFormProps { * ``` */ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormProps) { + const t = useTranslations('settings.password'); const [serverError, setServerError] = useState(null); const passwordChangeMutation = usePasswordChange((message) => { toast.success(message); @@ -79,6 +82,8 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP onSuccess?.(); }); + const passwordChangeSchema = createPasswordChangeSchema((key: string) => t(key)); + const form = useForm({ resolver: zodResolver(passwordChangeSchema), defaultValues: { @@ -122,7 +127,7 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP }); } else { // Unexpected error format - setServerError('An unexpected error occurred. Please try again.'); + setServerError(t('unexpectedError')); } } }; @@ -133,10 +138,8 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP return ( - Change Password - - Update your password to keep your account secure. Make sure it's strong and unique. - + {t('title')} + {t('subtitle')}
    @@ -149,9 +152,9 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP {/* Current Password Field */} {/* Confirm Password Field */} {/* istanbul ignore next - Cancel button requires isDirty state, tested in E2E */} {isDirty && !isSubmitting && ( )}
    diff --git a/frontend/src/components/settings/ProfileSettingsForm.tsx b/frontend/src/components/settings/ProfileSettingsForm.tsx index 5f4be00..ef23353 100644 --- a/frontend/src/components/settings/ProfileSettingsForm.tsx +++ b/frontend/src/components/settings/ProfileSettingsForm.tsx @@ -11,6 +11,7 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; +import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert } from '@/components/ui/alert'; @@ -23,21 +24,18 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro // Validation Schema // ============================================================================ -const profileSchema = z.object({ - first_name: z - .string() - .min(1, 'First name is required') - .min(2, 'First name must be at least 2 characters') - .max(50, 'First name must not exceed 50 characters'), - last_name: z - .string() - .max(50, 'Last name must not exceed 50 characters') - .optional() - .or(z.literal('')), - email: z.string().email('Invalid email address'), -}); +const createProfileSchema = (t: (key: string) => string) => + z.object({ + first_name: z + .string() + .min(1, t('firstNameRequired')) + .min(2, t('firstNameMinLength')) + .max(50, t('firstNameMaxLength')), + last_name: z.string().max(50, t('lastNameMaxLength')).optional().or(z.literal('')), + email: z.string().email(t('emailInvalid')), + }); -type ProfileFormData = z.infer; +type ProfileFormData = z.infer>; // ============================================================================ // Component @@ -67,6 +65,7 @@ interface ProfileSettingsFormProps { * ``` */ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFormProps) { + const t = useTranslations('settings.profile'); const [serverError, setServerError] = useState(null); const currentUser = useCurrentUser(); const updateProfileMutation = useUpdateProfile((message) => { @@ -74,6 +73,8 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor onSuccess?.(); }); + const profileSchema = createProfileSchema((key: string) => t(key)); + const form = useForm({ resolver: zodResolver(profileSchema), defaultValues: { @@ -135,7 +136,7 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor }); } else { // Unexpected error format - setServerError('An unexpected error occurred. Please try again.'); + setServerError(t('unexpectedError')); } } }; @@ -146,10 +147,8 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor return ( - Profile Information - - Update your personal information. Your email address is read-only. - + {t('title')} + {t('subtitle')} @@ -162,9 +161,9 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor {/* First Name Field */} @@ -197,12 +196,12 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor {/* Submit Button */}
    {/* istanbul ignore next - Reset button requires isDirty state, tested in E2E */} {isDirty && !isSubmitting && ( )}