diff --git a/CHANGELOG.md b/CHANGELOG.md
index d27197d..88fa1ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
## [Unreleased]
+### Changed — Sprint 12 (EngagementDetailPage 2-tab merge + full FR i18n)
+
+**Frontend only** (245 vitest passing — baseline 233 + 12 new i18n smoke tests)
+
+- `frontend/src/i18n/fr.json` — NEW. ~160-key nested translation file (French). Covers: common, nav, auth, engagement, simulation, template, user.admin, c2, mitre, state, toast, status.
+- `frontend/src/i18n/index.ts` — NEW. i18next init (lng: fr, no fallback to en, escapeValue: false).
+- `frontend/src/i18n/status.ts` — NEW. `engagementStatusLabel()` / `simulationStatusLabel()` helpers mapping API status to translated strings.
+- `frontend/src/lib/format.ts` — NEW. `formatDate()` / `formatDateTime()` using `fr-FR` locale.
+- `frontend/src/main.tsx` — Added `import './i18n'` bootstrap.
+- `frontend/vitest.setup.ts` — Added `import './src/i18n'` so all tests use real French translations.
+- `frontend/src/pages/EngagementDetailPage.tsx` — Merged 3-tab layout → 2 tabs (Description + Simulations); full i18n pass.
+- `frontend/src/pages/EngagementsListPage.tsx` — Full i18n pass; local `formatDate` removed, `@/lib/format` used.
+- `frontend/src/pages/EngagementFormPage.tsx` — Full i18n pass; `validate()` inside component; `data-testid="btn-submit"` added.
+- `frontend/src/pages/SimulationFormPage.tsx` — Full i18n pass; `data-testid` on 4 action buttons.
+- `frontend/src/pages/TemplatesListPage.tsx` — Full i18n pass.
+- `frontend/src/pages/TemplateFormPage.tsx` — Full i18n pass.
+- `frontend/src/pages/UsersAdminPage.tsx` — Full i18n pass; `data-testid="new-role-select"` added.
+- `frontend/src/components/Layout.tsx` — Nav labels translated.
+- `frontend/src/components/LoginPage.tsx` — Form labels and error message translated.
+- `frontend/src/components/ProtectedRoute.tsx` — Forbidden/loading messages translated.
+- `frontend/src/components/SimulationList.tsx` — Full i18n pass.
+- `frontend/src/components/TemplatePickerModal.tsx` — Full i18n pass.
+- `frontend/src/components/MitreTechniqueTag.tsx` — `aria-label` translated (Retirer TX…).
+- `frontend/src/components/MitreTechniquesField.tsx` — Full i18n pass.
+- `frontend/src/components/MitreMatrixModal.tsx` — Full i18n pass; plural apply button.
+- `frontend/src/components/MitreTechniquePicker.tsx` — Placeholder and messages translated.
+- `frontend/src/components/C2ConfigCard.tsx` — Full i18n pass.
+- `frontend/src/components/C2TasksPanel.tsx` — Full i18n pass.
+- `frontend/src/components/ExecuteViaC2Modal.tsx` — Full i18n pass.
+- `frontend/src/components/ImportC2HistoryModal.tsx` — Full i18n pass.
+- `frontend/src/components/C2CallbackPicker.tsx` — Full i18n pass.
+- `frontend/src/components/Toast.tsx` — Dismiss aria-label translated.
+- `frontend/src/components/ErrorState.tsx` — Title default and Retry button translated.
+- All test files updated to use French strings or `data-testid` where button labels changed.
+
+**No backend changes. No DB schema change. No migration.**
+
### Added — Sprint 11 (Spectrum UX port: 4 primitives + compact density global)
**Frontend only** (233 vitest passing — baseline 212 + 21 new tests across 4 new specs)
diff --git a/frontend/src/components/ErrorState.tsx b/frontend/src/components/ErrorState.tsx
index 9bf984b..4b5a612 100644
--- a/frontend/src/components/ErrorState.tsx
+++ b/frontend/src/components/ErrorState.tsx
@@ -1,21 +1,25 @@
+import { useTranslation } from 'react-i18next';
+
interface ErrorStateProps {
title?: string;
message: string;
onRetry?: () => void;
}
-export function ErrorState({ title = 'Something went wrong', message, onRetry }: ErrorStateProps): JSX.Element {
+export function ErrorState({ title, message, onRetry }: ErrorStateProps): JSX.Element {
+ const { t } = useTranslation();
+ const resolvedTitle = title ?? t('state.error.title');
return (
-
{title}
+
{resolvedTitle}
{message}
{onRetry ? (
) : null}
diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx
index 3e0ed7e..a7ba5d4 100644
--- a/frontend/src/components/Toast.tsx
+++ b/frontend/src/components/Toast.tsx
@@ -1,6 +1,8 @@
+import { useTranslation } from 'react-i18next';
import { useToast } from '@/hooks/useToast';
export function ToastViewport(): JSX.Element {
+ const { t } = useTranslation();
const { toasts, dismiss } = useToast();
return (
- {toasts.map((t) => {
- const isError = t.kind === 'error';
- const isSuccess = t.kind === 'success';
+ {toasts.map((toast) => {
+ const isError = toast.kind === 'error';
+ const isSuccess = toast.kind === 'success';
const surface = isError
? 'bg-paper text-ink border border-hairline border-l-4 border-l-bloom-deep'
: isSuccess
@@ -18,18 +20,18 @@ export function ToastViewport(): JSX.Element {
: 'bg-paper text-ink border border-hairline border-l-4 border-l-primary';
return (
-
{t.message}
+
{toast.message}