feat(frontend): i18n engagement pages (list, detail, form) + fr-FR date formatting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-21 23:26:22 +02:00
parent fe597e9be3
commit ea870af324
4 changed files with 83 additions and 76 deletions

View File

@@ -1,4 +1,5 @@
import { useParams, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { extractApiError } from '@/api/client';
import { useAuth } from '@/hooks/useAuth';
import { useEngagement } from '@/hooks/useEngagements';
@@ -11,10 +12,12 @@ import { SimulationList } from '@/components/SimulationList';
import { ExportEngagementButton } from '@/components/ExportEngagementButton';
import { BackLink } from '@/components/BackLink';
import { Tabs } from '@/components/Tabs';
import { formatDateTime } from '@/lib/format';
type TabId = 'description' | 'simulations';
export function EngagementDetailPage(): JSX.Element {
const { t } = useTranslation();
const { id } = useParams<{ id: string }>();
const numericId = id ? Number(id) : undefined;
const { canEditEngagements } = useAuth();
@@ -25,35 +28,36 @@ export function EngagementDetailPage(): JSX.Element {
const [activeTabRaw, setActiveTab] = useHashTab('description');
const activeTab = activeTabRaw as TabId;
if (detail.isLoading) return <LoadingState label="Loading engagement…" />;
if (detail.isLoading) return <LoadingState label={t('engagement.detail.loading')} />;
if (detail.isError) {
return (
<ErrorState
message={extractApiError(detail.error, 'Could not load engagement')}
message={extractApiError(detail.error, t('engagement.detail.errorLoad'))}
onRetry={() => detail.refetch()}
/>
);
}
if (!detail.data) return <ErrorState message="Engagement not found" />;
if (!detail.data) return <ErrorState message={t('engagement.detail.notFound')} />;
const eng = detail.data;
const simCount = simsQuery.data?.length;
const tabs = [
{ id: 'description', label: 'Description' },
{ id: 'simulations', label: 'Simulations', count: simCount },
{ id: 'description', label: t('engagement.detail.tabs.description') },
{ id: 'simulations', label: t('engagement.detail.tabs.simulations'), count: simCount },
];
return (
<div className="flex flex-col gap-xl">
<header className="flex items-start justify-between gap-md">
<div className="flex flex-col gap-sm">
<BackLink to="/engagements">Back to engagements</BackLink>
<BackLink to="/engagements">{t('engagement.detail.backTo')}</BackLink>
<h1 className="text-[32px] font-medium leading-none">{eng.name}</h1>
<div className="flex items-center gap-md">
<StatusBadge status={eng.status} />
<span className="text-[14px] text-graphite">
Created by <span className="text-ink">{eng.created_by.username}</span>
{t('engagement.detail.createdBy')}{' '}
<span className="text-ink">{eng.created_by.username}</span>
</span>
</div>
</div>
@@ -73,25 +77,25 @@ export function EngagementDetailPage(): JSX.Element {
<div className="card-product flex flex-col gap-md">
<header className="flex items-start justify-between gap-md">
<dl className="grid grid-cols-2 gap-x-lg gap-y-xxs text-caption-md">
<dt className="text-graphite">Start date</dt>
<dt className="text-graphite">{t('engagement.detail.schedule.startDate')}</dt>
<dd className="font-mono">{eng.start_date}</dd>
<dt className="text-graphite">End date</dt>
<dt className="text-graphite">{t('engagement.detail.schedule.endDate')}</dt>
<dd className="font-mono">{eng.end_date ?? '—'}</dd>
<dt className="text-graphite">Status</dt>
<dt className="text-graphite">{t('engagement.detail.schedule.status')}</dt>
<dd><StatusBadge status={eng.status} /></dd>
<dt className="text-graphite">Created at</dt>
<dd className="font-mono">{eng.created_at}</dd>
<dt className="text-graphite">{t('engagement.detail.schedule.createdAt')}</dt>
<dd className="font-mono">{formatDateTime(eng.created_at)}</dd>
</dl>
{canEditEngagements ? (
<Link to={`/engagements/${eng.id}/edit`} className="btn-outline shrink-0">
Edit
{t('engagement.detail.edit')}
</Link>
) : null}
</header>
<hr className="border-hairline" />
<h2 className="text-display-sm">Description</h2>
<h2 className="text-display-sm">{t('engagement.detail.tabs.description')}</h2>
<p className="text-[16px] text-charcoal whitespace-pre-line">
{eng.description?.trim() ? eng.description : 'No description provided.'}
{eng.description?.trim() ? eng.description : t('engagement.detail.noDescription')}
</p>
</div>
)}