i18n(events): translate PasswordResetModal across 5 locales

The rebuilt modal in this PR shipped with hard-coded English strings.
That made the reset flow untranslated for German/Dutch/Portuguese/
Russian customers — toasts, confirm dialog, success screen all
fell back to English regardless of the active locale.

- New `events.passwordReset.*` namespace in en/de/nl/pt/ru with 22
  keys covering both modal screens, the warning banner, validation
  errors, and the toast messages.
- Modal uses `useTranslation()` for every previously hard-coded
  string. Reuses `common.cancel`, `events.copy`, `events.copied`
  where they already exist across all locales.
- The {{eventName}} interpolation uses i18next's standard variable
  syntax so the description line reads naturally in each language.

No behaviour change. TypeScript clean (`npx tsc --noEmit`), ESLint
clean. JSON validity checked for all 5 locale files.
This commit is contained in:
Paul Nothaft
2026-05-03 23:16:21 +02:00
parent c0ca5ed687
commit c270bcfc9f
6 changed files with 141 additions and 25 deletions
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { X, Key, Copy, CheckCircle, Mail, Lock, Eye, EyeOff } from 'lucide-react';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import { Button, Card, Input, PasswordGenerator } from '../common';
interface PasswordResetModalProps {
@@ -18,6 +19,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
onConfirm,
onClose
}) => {
const { t } = useTranslation();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
@@ -33,10 +35,10 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
// Empty is allowed → server auto-generates. Only validate when typed.
if (password) {
if (password.length < 6) {
next.password = 'Password must be at least 6 characters';
next.password = t('events.passwordReset.errorMinLength');
}
if (password !== confirmPassword) {
next.confirmPassword = 'Passwords do not match';
next.confirmPassword = t('events.passwordReset.errorMismatch');
}
}
setErrors(next);
@@ -52,14 +54,14 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
setResultPassword(result.newPassword);
setResultWasGenerated(!supplied);
if (supplied) {
toast.success('Password reset successfully');
toast.success(t('events.passwordReset.toastSuccess'));
}
} catch (error: any) {
const serverError = error?.response?.data;
if (serverError?.error === 'Password does not meet security requirements') {
setErrors({ password: serverError.feedback?.join?.(' ') || 'Password does not meet security requirements' });
setErrors({ password: serverError.feedback?.join?.(' ') || t('events.passwordReset.errorMinLength') });
} else {
toast.error(serverError?.error || 'Failed to reset password');
toast.error(serverError?.error || t('events.passwordReset.toastError'));
}
} finally {
setIsResetting(false);
@@ -70,7 +72,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
if (resultPassword) {
await navigator.clipboard.writeText(resultPassword);
setCopied(true);
toast.success('Password copied to clipboard');
toast.success(t('events.passwordReset.toastCopied'));
setTimeout(() => setCopied(false), 2000);
}
};
@@ -86,7 +88,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
<Card className="max-w-md w-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-neutral-900">
{resultPassword ? 'New Password' : 'Reset Gallery Password'}
{resultPassword ? t('events.passwordReset.newTitle') : t('events.passwordReset.title')}
</h2>
<button
onClick={onClose}
@@ -99,22 +101,22 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
{!resultPassword ? (
<>
<p className="text-neutral-600 mb-4">
Set a new password for <strong>{eventName}</strong>, or leave both fields empty to have one auto-generated.
{t('events.passwordReset.description', { eventName })}
</p>
<div className="space-y-4 mb-4">
<div>
<Input
type={showPassword ? 'text' : 'password'}
label="New password"
placeholder="Leave empty to auto-generate"
label={t('events.passwordReset.newPasswordLabel')}
placeholder={t('events.passwordReset.placeholder')}
value={password}
onChange={(e) => {
setPassword(e.target.value);
if (errors.password) setErrors((prev) => ({ ...prev, password: undefined }));
}}
error={errors.password}
helperText="Use 6+ characters, or leave blank to auto-generate"
helperText={t('events.passwordReset.helperText')}
leftIcon={<Lock className="w-5 h-5" />}
rightIcon={
<button
@@ -142,8 +144,8 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
{password.length > 0 && (
<Input
type={showPassword ? 'text' : 'password'}
label="Confirm password"
placeholder="Confirm password"
label={t('events.passwordReset.confirmLabel')}
placeholder={t('events.passwordReset.confirmLabel')}
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
@@ -167,11 +169,11 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-neutral-500" />
<span className="text-sm font-medium text-neutral-700">
Send email notification
{t('events.passwordReset.sendEmail')}
</span>
</div>
<p className="text-xs text-neutral-500 mt-1">
Notify the host about the password change
{t('events.passwordReset.sendEmailHelp')}
</p>
</div>
</label>
@@ -179,8 +181,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-6">
<p className="text-sm text-amber-800">
<strong>Note:</strong> The old password will no longer work.
Make sure to share the new password with the host.
{t('events.passwordReset.warning')}
</p>
</div>
@@ -191,7 +192,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
disabled={isResetting}
className="flex-1"
>
Cancel
{t('common.cancel')}
</Button>
<Button
variant="primary"
@@ -201,7 +202,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
leftIcon={<Key className="w-4 h-4" />}
className="flex-1"
>
Reset Password
{t('events.passwordReset.submit')}
</Button>
</div>
</>
@@ -210,11 +211,11 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-3 mb-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<p className="font-medium text-green-900">Password reset successfully!</p>
<p className="font-medium text-green-900">{t('events.passwordReset.successHeading')}</p>
</div>
{sendEmail && (
<p className="text-sm text-green-700">
An email notification has been sent to the host.
{t('events.passwordReset.emailSentNote')}
</p>
)}
</div>
@@ -223,7 +224,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
<>
<div className="mb-4">
<label className="block text-sm font-medium text-neutral-700 mb-2">
Auto-generated gallery password
{t('events.passwordReset.generatedLabel')}
</label>
<div className="flex gap-2">
<input
@@ -237,14 +238,14 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
onClick={handleCopy}
leftIcon={copied ? <CheckCircle className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
>
{copied ? 'Copied!' : 'Copy'}
{copied ? t('events.copied') : t('events.copy')}
</Button>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-6">
<p className="text-sm text-blue-800">
<strong>Important:</strong> Save this password securely. It cannot be recovered once you close this window.
{t('events.passwordReset.saveSecurelyNote')}
</p>
</div>
</>
@@ -255,7 +256,7 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
onClick={onClose}
className="w-full"
>
Done
{t('events.passwordReset.done')}
</Button>
</>
)}