feat(frontend): full FR i18n via react-i18next + 2-tab engagement detail (sprint 12) #14
@@ -1,5 +1,6 @@
|
|||||||
import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom';
|
import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom';
|
||||||
import { Moon, Sun, Monitor } from 'lucide-react';
|
import { Moon, Sun, Monitor } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import type { Theme } from '@/hooks/useTheme';
|
import type { Theme } from '@/hooks/useTheme';
|
||||||
@@ -20,6 +21,7 @@ export function Layout(): JSX.Element {
|
|||||||
const { user, isAdmin, isRedteam, logout } = useAuth();
|
const { user, isAdmin, isRedteam, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { theme, cycleTheme } = useTheme();
|
const { theme, cycleTheme } = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout();
|
await logout();
|
||||||
@@ -52,7 +54,7 @@ export function Layout(): JSX.Element {
|
|||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="text-[14px] underline-offset-2 hover:underline"
|
className="text-[14px] underline-offset-2 hover:underline"
|
||||||
>
|
>
|
||||||
Sign out
|
{t('nav.signOut')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -78,7 +80,7 @@ export function Layout(): JSX.Element {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Engagements
|
{t('nav.engagements')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{isAdmin || isRedteam ? (
|
{isAdmin || isRedteam ? (
|
||||||
<NavLink
|
<NavLink
|
||||||
@@ -91,7 +93,7 @@ export function Layout(): JSX.Element {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Templates
|
{t('nav.templates')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
) : null}
|
) : null}
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
@@ -105,7 +107,7 @@ export function Layout(): JSX.Element {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Users
|
{t('nav.users')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
) : null}
|
) : null}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import type { Role } from '@/api/types';
|
import type { Role } from '@/api/types';
|
||||||
@@ -23,6 +24,7 @@ export function ProtectedRoute({
|
|||||||
}: ProtectedRouteProps): JSX.Element {
|
}: ProtectedRouteProps): JSX.Element {
|
||||||
const { user, status } = useAuth();
|
const { user, status } = useAuth();
|
||||||
const { push } = useToast();
|
const { push } = useToast();
|
||||||
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const roleDenied = Boolean(
|
const roleDenied = Boolean(
|
||||||
@@ -31,12 +33,12 @@ export function ProtectedRoute({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (roleDenied) {
|
if (roleDenied) {
|
||||||
push('Accès refusé', 'error');
|
push(t('auth.forbidden'), 'error');
|
||||||
}
|
}
|
||||||
}, [roleDenied, push]);
|
}, [roleDenied, push, t]);
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return <LoadingState label="Loading session…" />;
|
return <LoadingState label={t('auth.loadingSession')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'unauthenticated' || !user) {
|
if (status === 'unauthenticated' || !user) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, type FormEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
|
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { extractApiError } from '@/api/client';
|
import { extractApiError } from '@/api/client';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { FormField, TextInput } from '@/components/FormField';
|
import { FormField, TextInput } from '@/components/FormField';
|
||||||
@@ -14,6 +15,7 @@ export function LoginPage(): JSX.Element {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const fromPath = (location.state as LocationState | null)?.from;
|
const fromPath = (location.state as LocationState | null)?.from;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -34,7 +36,7 @@ export function LoginPage(): JSX.Element {
|
|||||||
await login(username, password);
|
await login(username, password);
|
||||||
navigate(fromPath ?? '/engagements', { replace: true });
|
navigate(fromPath ?? '/engagements', { replace: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(extractApiError(err, 'Invalid credentials'));
|
setError(extractApiError(err, t('auth.login.invalid')));
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -48,10 +50,10 @@ export function LoginPage(): JSX.Element {
|
|||||||
<span className="inline-block h-8 w-8 rotate-12 bg-primary" aria-hidden />
|
<span className="inline-block h-8 w-8 rotate-12 bg-primary" aria-hidden />
|
||||||
<h1 className="text-[28px] font-medium leading-none">Mimic</h1>
|
<h1 className="text-[28px] font-medium leading-none">Mimic</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[16px] text-charcoal">Sign in to access your engagements.</p>
|
<p className="text-[16px] text-charcoal">{t('auth.login.subtitle')}</p>
|
||||||
|
|
||||||
<form onSubmit={onSubmit} noValidate className="flex flex-col gap-md">
|
<form onSubmit={onSubmit} noValidate className="flex flex-col gap-md">
|
||||||
<FormField label="Username" htmlFor="login-username" required>
|
<FormField label={t('auth.login.username')} htmlFor="login-username" required>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="login-username"
|
id="login-username"
|
||||||
name="username"
|
name="username"
|
||||||
@@ -62,7 +64,7 @@ export function LoginPage(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="Password" htmlFor="login-password" required>
|
<FormField label={t('auth.login.password')} htmlFor="login-password" required>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="login-password"
|
id="login-password"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -81,7 +83,7 @@ export function LoginPage(): JSX.Element {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<button type="submit" className="btn-primary" disabled={submitting}>
|
<button type="submit" className="btn-primary" disabled={submitting}>
|
||||||
{submitting ? 'Signing in…' : 'Sign in'}
|
{submitting ? t('auth.login.signingIn') : t('auth.login.signIn')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
import '@testing-library/jest-dom/vitest';
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
import './src/i18n';
|
||||||
|
|||||||
Reference in New Issue
Block a user