feat(frontend): full FR i18n via react-i18next + 2-tab engagement detail (sprint 12) #14

Merged
knacky merged 14 commits from sprint/12-i18n-fr into main 2026-06-22 08:25:51 +00:00
4 changed files with 19 additions and 12 deletions
Showing only changes of commit fe597e9be3 - Show all commits

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -1 +1,2 @@
import '@testing-library/jest-dom/vitest'; import '@testing-library/jest-dom/vitest';
import './src/i18n';