fix: translate auth pages to Russian, fix backend error parsing, calendar timezone

- Translate forgot-password and reset-password pages to Russian
- Translate verify-email page to Russian
- Fix error message parsing: backend returns {error:{message}} not {message}
- Apply timezone fix in calendar (FullCalendar timeZone prop + fTime helper)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dev Server 2026-03-12 16:50:51 +03:00
parent f6caa7df6b
commit 75d6072309
4 changed files with 43 additions and 43 deletions

View File

@ -23,8 +23,8 @@ import { requestPasswordReset } from 'src/auth/context/jwt';
const ForgotPasswordSchema = zod.object({ const ForgotPasswordSchema = zod.object({
email: zod email: zod
.string() .string()
.min(1, { message: 'Email is required!' }) .min(1, { message: 'Введите email!' })
.email({ message: 'Email must be a valid email address!' }), .email({ message: 'Введите корректный email адрес!' }),
}); });
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@ -46,11 +46,11 @@ export function JwtForgotPasswordView() {
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
try { try {
await requestPasswordReset({ email: data.email }); await requestPasswordReset({ email: data.email });
setSuccessMsg('Password reset instructions have been sent to your email.'); setSuccessMsg('Инструкции по сбросу пароля отправлены на ваш email.');
setErrorMsg(''); setErrorMsg('');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const msg = error?.response?.data?.message || error?.response?.data?.detail || 'Error sending request. Please check your email.'; const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || 'Ошибка отправки запроса. Проверьте правильность email.';
setErrorMsg(msg); setErrorMsg(msg);
} }
}); });
@ -58,10 +58,10 @@ export function JwtForgotPasswordView() {
return ( return (
<> <>
<Stack spacing={1.5} sx={{ mb: 5 }}> <Stack spacing={1.5} sx={{ mb: 5 }}>
<Typography variant="h5">Forgot your password?</Typography> <Typography variant="h5">Забыли пароль?</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}> <Typography variant="body2" sx={{ color: 'text.secondary' }}>
Enter your email address and we will send you a link to reset your password. Введите ваш email и мы отправим вам ссылку для сброса пароля.
</Typography> </Typography>
</Stack> </Stack>
@ -80,7 +80,7 @@ export function JwtForgotPasswordView() {
{!successMsg && ( {!successMsg && (
<Form methods={methods} onSubmit={onSubmit}> <Form methods={methods} onSubmit={onSubmit}>
<Stack spacing={3}> <Stack spacing={3}>
<Field.Text name="email" label="Email address" InputLabelProps={{ shrink: true }} /> <Field.Text name="email" label="Email" InputLabelProps={{ shrink: true }} />
<LoadingButton <LoadingButton
fullWidth fullWidth
@ -89,9 +89,9 @@ export function JwtForgotPasswordView() {
type="submit" type="submit"
variant="contained" variant="contained"
loading={isSubmitting} loading={isSubmitting}
loadingIndicator="Sending..." loadingIndicator="Отправка..."
> >
Send reset link Отправить ссылку
</LoadingButton> </LoadingButton>
</Stack> </Stack>
</Form> </Form>
@ -104,7 +104,7 @@ export function JwtForgotPasswordView() {
color="inherit" color="inherit"
sx={{ display: 'block', mt: 3, textAlign: 'center' }} sx={{ display: 'block', mt: 3, textAlign: 'center' }}
> >
Back to sign in Вернуться ко входу
</Link> </Link>
</> </>
); );

View File

@ -29,12 +29,12 @@ const ResetPasswordSchema = zod
.object({ .object({
newPassword: zod newPassword: zod
.string() .string()
.min(1, { message: 'Password is required!' }) .min(1, { message: 'Введите пароль!' })
.min(6, { message: 'Password must be at least 6 characters!' }), .min(6, { message: 'Пароль должен содержать не менее 6 символов!' }),
newPasswordConfirm: zod.string().min(1, { message: 'Please confirm your password!' }), newPasswordConfirm: zod.string().min(1, { message: 'Подтвердите пароль!' }),
}) })
.refine((data) => data.newPassword === data.newPasswordConfirm, { .refine((data) => data.newPassword === data.newPasswordConfirm, {
message: 'Passwords do not match!', message: 'Пароли не совпадают!',
path: ['newPasswordConfirm'], path: ['newPasswordConfirm'],
}); });
@ -63,7 +63,7 @@ function ResetPasswordContent() {
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
if (!token) { if (!token) {
setErrorMsg('Reset link is missing. Please request a new password reset.'); setErrorMsg('Ссылка для сброса отсутствует. Запросите новый сброс пароля.');
return; return;
} }
try { try {
@ -72,11 +72,11 @@ function ResetPasswordContent() {
newPassword: data.newPassword, newPassword: data.newPassword,
newPasswordConfirm: data.newPasswordConfirm, newPasswordConfirm: data.newPasswordConfirm,
}); });
setSuccessMsg('Password changed successfully. You can now sign in with your new password.'); setSuccessMsg('Пароль успешно изменён. Теперь вы можете войти с новым паролем.');
setErrorMsg(''); setErrorMsg('');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const msg = error?.response?.data?.message || error?.response?.data?.detail || 'Failed to reset password. The link may have expired — please request a new one.'; const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || 'Не удалось сбросить пароль. Возможно, ссылка устарела — запросите новую.';
setErrorMsg(msg); setErrorMsg(msg);
} }
}); });
@ -85,13 +85,13 @@ function ResetPasswordContent() {
return ( return (
<> <>
<Stack spacing={1.5} sx={{ mb: 5 }}> <Stack spacing={1.5} sx={{ mb: 5 }}>
<Typography variant="h5">Reset password</Typography> <Typography variant="h5">Сброс пароля</Typography>
</Stack> </Stack>
<Alert severity="error" sx={{ mb: 3 }}> <Alert severity="error" sx={{ mb: 3 }}>
Reset link is missing. Please follow the link from your email or request a new one. Ссылка для сброса отсутствует. Перейдите по ссылке из письма или запросите новую.
</Alert> </Alert>
<Link component={RouterLink} href={paths.auth.jwt.forgotPassword} variant="subtitle2"> <Link component={RouterLink} href={paths.auth.jwt.forgotPassword} variant="subtitle2">
Request new reset link Запросить новую ссылку
</Link> </Link>
</> </>
); );
@ -101,13 +101,13 @@ function ResetPasswordContent() {
return ( return (
<> <>
<Stack spacing={1.5} sx={{ mb: 5 }}> <Stack spacing={1.5} sx={{ mb: 5 }}>
<Typography variant="h5">Reset password</Typography> <Typography variant="h5">Сброс пароля</Typography>
</Stack> </Stack>
<Alert severity="success" sx={{ mb: 3 }}> <Alert severity="success" sx={{ mb: 3 }}>
{successMsg} {successMsg}
</Alert> </Alert>
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2"> <Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
Sign in Войти
</Link> </Link>
</> </>
); );
@ -116,9 +116,9 @@ function ResetPasswordContent() {
return ( return (
<> <>
<Stack spacing={1.5} sx={{ mb: 5 }}> <Stack spacing={1.5} sx={{ mb: 5 }}>
<Typography variant="h5">Set new password</Typography> <Typography variant="h5">Новый пароль</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}> <Typography variant="body2" sx={{ color: 'text.secondary' }}>
Enter your new password below. Введите новый пароль ниже.
</Typography> </Typography>
</Stack> </Stack>
@ -132,8 +132,8 @@ function ResetPasswordContent() {
<Stack spacing={3}> <Stack spacing={3}>
<Field.Text <Field.Text
name="newPassword" name="newPassword"
label="New password" label="Новый пароль"
placeholder="6+ characters" placeholder="6+ символов"
type={newPassword.value ? 'text' : 'password'} type={newPassword.value ? 'text' : 'password'}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
InputProps={{ InputProps={{
@ -149,8 +149,8 @@ function ResetPasswordContent() {
<Field.Text <Field.Text
name="newPasswordConfirm" name="newPasswordConfirm"
label="Confirm new password" label="Подтвердите пароль"
placeholder="6+ characters" placeholder="6+ символов"
type={newPasswordConfirm.value ? 'text' : 'password'} type={newPasswordConfirm.value ? 'text' : 'password'}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
InputProps={{ InputProps={{
@ -171,9 +171,9 @@ function ResetPasswordContent() {
type="submit" type="submit"
variant="contained" variant="contained"
loading={isSubmitting} loading={isSubmitting}
loadingIndicator="Saving..." loadingIndicator="Сохранение..."
> >
Save new password Сохранить пароль
</LoadingButton> </LoadingButton>
</Stack> </Stack>
</Form> </Form>
@ -185,7 +185,7 @@ function ResetPasswordContent() {
color="inherit" color="inherit"
sx={{ display: 'block', mt: 3, textAlign: 'center' }} sx={{ display: 'block', mt: 3, textAlign: 'center' }}
> >
Back to sign in Вернуться ко входу
</Link> </Link>
</> </>
); );
@ -195,7 +195,7 @@ function ResetPasswordContent() {
export function JwtResetPasswordView() { export function JwtResetPasswordView() {
return ( return (
<Suspense fallback={<Typography variant="body2">Loading...</Typography>}> <Suspense fallback={<Typography variant="body2">Загрузка...</Typography>}>
<ResetPasswordContent /> <ResetPasswordContent />
</Suspense> </Suspense>
); );

View File

@ -178,7 +178,7 @@ export function JwtSignUpView() {
router.replace(paths.dashboard.root); router.replace(paths.dashboard.root);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const msg = error?.response?.data?.message || error?.response?.data?.detail || (error instanceof Error ? error.message : 'Ошибка регистрации. Проверьте введённые данные.'); const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || (error instanceof Error ? error.message : 'Ошибка регистрации. Проверьте введённые данные.');
setErrorMsg(msg); setErrorMsg(msg);
} }
}); });

View File

@ -26,7 +26,7 @@ function VerifyEmailContent() {
useEffect(() => { useEffect(() => {
if (!token) { if (!token) {
setStatus('error'); setStatus('error');
setMessage('Verification link is missing. Please check your email or request a new one.'); setMessage('Ссылка для подтверждения отсутствует. Проверьте письмо или запросите новое.');
return; return;
} }
@ -37,16 +37,16 @@ function VerifyEmailContent() {
if (cancelled) return; if (cancelled) return;
if (res?.success) { if (res?.success) {
setStatus('success'); setStatus('success');
setMessage('Email successfully verified. You can now sign in.'); setMessage('Email успешно подтверждён. Теперь вы можете войти.');
} else { } else {
setStatus('error'); setStatus('error');
setMessage(res?.message || 'Failed to verify email.'); setMessage(res?.error?.message || res?.message || 'Не удалось подтвердить email.');
} }
}) })
.catch((err) => { .catch((err) => {
if (cancelled) return; if (cancelled) return;
setStatus('error'); setStatus('error');
const msg = err?.response?.data?.message || err?.response?.data?.detail || 'Invalid or expired link. Please request a new verification email.'; const msg = err?.response?.data?.error?.message || err?.response?.data?.message || err?.response?.data?.detail || 'Ссылка недействительна или устарела. Запросите новое письмо с подтверждением.';
setMessage(msg); setMessage(msg);
}); });
@ -58,14 +58,14 @@ function VerifyEmailContent() {
return ( return (
<> <>
<Stack spacing={1.5} sx={{ mb: 5 }}> <Stack spacing={1.5} sx={{ mb: 5 }}>
<Typography variant="h5">Email verification</Typography> <Typography variant="h5">Подтверждение email</Typography>
</Stack> </Stack>
{status === 'loading' && ( {status === 'loading' && (
<Stack alignItems="center" sx={{ py: 4 }}> <Stack alignItems="center" sx={{ py: 4 }}>
<CircularProgress /> <CircularProgress />
<Typography variant="body2" sx={{ mt: 2, color: 'text.secondary' }}> <Typography variant="body2" sx={{ mt: 2, color: 'text.secondary' }}>
Verifying your email... Подтверждение email...
</Typography> </Typography>
</Stack> </Stack>
)} )}
@ -76,7 +76,7 @@ function VerifyEmailContent() {
{message} {message}
</Alert> </Alert>
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2"> <Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
Sign in to your account Войти в аккаунт
</Link> </Link>
</> </>
)} )}
@ -88,10 +88,10 @@ function VerifyEmailContent() {
</Alert> </Alert>
<Stack spacing={1}> <Stack spacing={1}>
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2"> <Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
Back to sign in Вернуться ко входу
</Link> </Link>
<Link component={RouterLink} href={paths.auth.jwt.signUp} variant="body2" color="text.secondary"> <Link component={RouterLink} href={paths.auth.jwt.signUp} variant="body2" color="text.secondary">
Create a new account Создать новый аккаунт
</Link> </Link>
</Stack> </Stack>
</> </>
@ -104,7 +104,7 @@ function VerifyEmailContent() {
export function JwtVerifyEmailView() { export function JwtVerifyEmailView() {
return ( return (
<Suspense fallback={<Typography variant="body2">Loading...</Typography>}> <Suspense fallback={<Typography variant="body2">Загрузка...</Typography>}>
<VerifyEmailContent /> <VerifyEmailContent />
</Suspense> </Suspense>
); );