No description provided.
} +``` -**Files** : -- `frontend/src/components/AlertBanner.tsx` (NEW) — `{t('engagement.delete.confirm', { name: eng.name })}
+``` + +### Convention de keys — **nested by feature** + +``` +common.* save, cancel, delete, edit, new, loading, retry, back, dismiss +nav.* engagements, templates, users, signOut, brand +auth.* login.title, login.subtitle, login.signIn, login.signingIn, login.invalid, forbidden +engagement.list.* title, subtitle, new, empty.title, empty.desc, col.name, col.status, col.start, col.end, col.createdBy, col.actions, view, edit, delete, deleteConfirm, toast.deleted, error.delete +engagement.detail.* tabs.description, tabs.simulations, schedule.startDate, schedule.endDate, schedule.status, schedule.createdAt, edit, createdBy, noDescription, loading, errorLoad, notFound +engagement.form.* title.new, title.edit, subtitle.new, subtitle.edit, field.name, field.description, field.startDate, field.endDate, field.endDateHint, field.status, validation.nameRequired, validation.startDateRequired, validation.endDateAfterStart, btn.create, btn.save, btn.saving, btn.cancel, toast.created, toast.updated, error.save, error.load +simulation.form.* title.new, field.name, field.mitre, field.description, field.commands, field.commandsHint, field.prerequisites, field.executedAt, field.executionResult, btn.create, btn.creating, btn.save, btn.saving, btn.saveSoc, btn.markReview, btn.close, btn.reopen, btn.delete, btn.executeC2, btn.importC2History, btn.cancel, deleteConfirm.title, deleteConfirm.desc, deleteConfirm.confirm, deleteConfirm.cancel, banner.done, banner.socNotReady, header.redTeam, header.soc, field.logSource, field.logs, field.socComment, field.incidentNumber, validation.nameRequired, toast.created, toast.updated, toast.deleted, toast.markedReview, toast.closed, toast.reopened, toast.socUpdated, error.create, error.update, error.delete, error.soc, error.transition, loading, errorLoad +template.list.* title, subtitle, new, empty.title, empty.desc, col.name, col.description, col.commands, col.createdAt, edit, delete +template.form.* title.new, title.edit, field.name, field.description, field.descriptionPlaceholder, field.commands, field.commandsHint, field.commandsPlaceholder, field.prerequisites, field.prerequisitesPlaceholder, field.mitre, field.mitreSearch, field.mitreOpenMatrix, field.mitreEmpty, btn.save, btn.saving, btn.delete, btn.cancel, validation.nameRequired, toast.created, toast.saved, toast.deleted, deleteConfirm.title, deleteConfirm.desc, deleteConfirm.confirm, deleteConfirm.cancel, loading, errorLoad, errorSave, errorDelete +user.admin.* title, subtitle, new, col.username, col.role, col.createdAt, col.actions, role.admin, role.redteam, role.soc, field.username, field.password, btn.create, btn.creating, btn.resetPassword, btn.delete, btn.cancel, ... +c2.config.* title, disabled, field.url, field.urlHint, field.token, field.tokenHint, field.verifyTls, verifyTlsHint, btn.save, btn.saving, btn.test, btn.testing, btn.delete, toast.saved, toast.tested, toast.deleted, error.save, error.test, error.delete +c2.tasks.* title, empty, col.task, col.command, col.source, col.status, col.completedAt, refreshing +c2.modal.execute.* title, callback, btn.launch, btn.launching, btn.cancel, commandCount_one, commandCount_other, validation.callbackRequired, validation.commandsRequired, toast.launched, error.launch +c2.modal.import.* title, total, page, of, prev, next, btn.import, btn.importing, btn.cancel, selected, empty, toast.imported, toast.partial, error.import +c2.modal.picker.* title, empty, hostnameColon, lastCheckinColon +mitre.matrix.* title, filter, loading, error, applyItem_one, applyItem_other, clearAll, close, retry +mitre.field.* empty, search, openMatrix, savedToast, errorToast +mitre.tag.* remove +state.* loading, error.title, error.desc, empty.default, retry +toast.* dismiss +status.engagement.* planned, active, closed +status.simulation.* pending, in_progress, review_required, done +``` + +### Status enum map (`frontend/src/i18n/status.ts`) +```ts +import i18n from './index'; +import type { EngagementStatus, SimulationStatus } from '@/types'; + +export const engagementStatusLabel = (s: EngagementStatus): string => + i18n.t(`status.engagement.${s}`); +export const simulationStatusLabel = (s: SimulationStatus): string => + i18n.t(`status.simulation.${s}`); +``` + +Consumers : `StatusBadge.tsx`, `SimulationStatusBadge.tsx` si distinct, `EngagementDetailPage` (header dans Description tab), `EngagementFormPage` (STATUS_OPTIONS dérivé du map), `SimulationFormPage` banners (restructurer comme phrases entières dans JSON, pas inline status word). + +### Date formatting (`frontend/src/lib/format.ts`) +```ts +export const formatDate = (iso: string | null | undefined): string => { + if (!iso) return '—'; + return new Date(iso).toLocaleDateString('fr-FR'); // jj/mm/aaaa +}; +export const formatDateTime = (iso: string): string => + new Date(iso).toLocaleString('fr-FR'); +``` + +Replace raw ISO renders dans : `EngagementDetailPage`, `EngagementsListPage`, `TemplatesListPage`, `UsersAdminPage`. Garder ISO dans les commands / execution_result / params (data brut). + +### Acronymes — locked EN dans le JSON FR + +Exemples corrects : +- `"c2.config.title": "Configuration C2"` +- `"mitre.matrix.title": "Matrice MITRE ATT&CK"` +- `"simulation.form.header.redTeam": "Red Team"` (acronyme idiom) +- `"simulation.form.header.soc": "SOC"` (acronyme) +- `"auth.forbidden": "Accès refusé"` (déjà FR dans le code) + +### Strings déjà FR (preserve) + +- `ProtectedRoute.tsx:35` → `'Accès refusé'` (déjà FR). Route via `t('auth.forbidden')`. +- `Toast.test.tsx:28` → assertion `'Session expirée'`. Confirmer cohérence avec la key associée. + +### Composants out-of-scope + +`ForbiddenState.tsx` et `NotFoundState.tsx` n'existent pas — pas de keys i18n à créer. Si tu vois un cas où ils manquent, **flag dans le PR body** pour un sprint follow-up, ne crée rien. + +--- + +## Counts (estimation workflow) + +| Métrique | Valeur | +|---|---| +| Strings UI uniques à traduire (dedup) | **~140 keys** | +| Test assertions à update | **~123** (sur 193 raw matches, 70 sont data fixture) | +| Fichiers source modifiés | **~30** | +| Fichiers tests modifiés | **23** | +| Nouveaux fichiers | 3 (`i18n/index.ts`, `i18n/fr.json`, `i18n/status.ts`) | + +--- + +## Commits plan (10 commits) + +1. `feat(frontend): merge Schedule tab into Description on EngagementDetailPage` +2. `chore(frontend): install react-i18next + i18next deps` +3. `feat(frontend): scaffold i18n init, fr.json skeleton, status map` +4. `feat(frontend): i18n common + nav + auth (Layout, LoginPage, ProtectedRoute)` +5. `feat(frontend): i18n engagement pages (list, detail, form) + status map wiring` +6. `feat(frontend): i18n simulation pages (form + list component)` +7. `feat(frontend): i18n template pages (list + form + picker modal)` +8. `feat(frontend): i18n MITRE components (matrix modal, picker, field, tag)` +9. `feat(frontend): i18n C2 components (config, tasks, modals, picker)` +10. `feat(frontend): i18n shared state components + users admin + fr-FR date formatting` + +**Ordre** : chaque commit = une slice verticale (source + tests mis à jour ensemble) → vitest reste vert à chaque commit. + +--- + +## Tests strategy + +### Baseline +- vitest **236/236** doit tenir à chaque commit. +- Tests rendent avec i18n init déjà chargé (FR loaded), pas de mock setup needed. + +### Pattern de rewrite (3 stratégies) +1. **Direct rewrite** (cas le plus fréquent) : `getByRole('button', { name: /save/i })` → `getByRole('button', { name: /enregistrer/i })`. +2. **Refactor vers `data-testid`** (recommandé pour les boutons avec action semantics) : `data-testid="btn-save"` côté source, `getByTestId('btn-save')` côté test. Survit aux re-wordings futurs. + - Cible prioritaire : `SimulationFormPage.test.tsx` (5 boutons), `UsersAdminPage.test.tsx` (4), `TemplateFormPage.test.tsx` (4), `EngagementFormPage.test.tsx` (2). +3. **Keep domain assertions** (~70 sites) : `getByText('T1078')`, `getByText('alice')`, `getByText('WIN-DC01')`, `getByText('whoami')`. Pas de change. + +### Nouveau test (commit 3) +`frontend/tests/i18n.test.ts` : +- i18n init avec `lng: 'fr'`. +- `t('common.save')` → `'Enregistrer'`. +- Missing key returns key path (debug). +- `simulationStatusLabel('done')` → `'Terminé'`. +- Interpolation : `t('engagement.delete.confirm', { name: 'X' })` contient `« X »`. + +--- + +## DoD + +1. EngagementDetailPage a **2 tabs** (Description + Simulations). Schedule content = header block dans le panel Description. Edit button atteignable. +2. `package.json` inclut `react-i18next` et `i18next`. +3. `frontend/src/i18n/index.ts` initialise i18next avec FR comme `lng` ET `fallbackLng`. +4. `frontend/src/i18n/fr.json` contient ~140 keys, organisées en namespaces nested. +5. `frontend/src/i18n/status.ts` expose les 2 labels et est consommé partout où raw status est rendu. +6. **Zéro string UI EN hardcodée** dans `frontend/src/pages/` + `frontend/src/components/`. Exceptions autorisées : acronymes (liste lockée), brand "Mimic", glyphes (`—`, `×`, `▾`, `▸`), API enum passthroughs, MITRE IDs, command/payload text. +7. Dates rendues via `formatDate()` au format `fr-FR` (jj/mm/aaaa). +8. `npm run test` → **236+ / 236+** (+5 nouveaux tests i18n). +9. `npm run build` succès (zéro TS error, PostCSS clean — leçon `feedback_css_apply_tokens` respectée). +10. **Manual smoke obligatoire** : login → engagements → detail → simulation form → templates → users → C2 config — chaque string visible est en FR (sauf acronymes + brand). +11. CHANGELOG.md entrée sprint 12 listant migration i18n + tab merge. + +--- ## Out of scope (explicite) -- ❌ Side nav / drawer / breadcrumb component — top nav + BackLink suffit -- ❌ Global engagement selector top-bar (Spectrum pattern rejeté) -- ❌ Editor dynamic-column grid — pas d'éditeur dans Mimic -- ❌ Dark mode rework -- ❌ Toast queue system (les toasts actuels via `useToast` restent) -- ❌ PDF/document deliverable -- ❌ Color palette changes -- ❌ Tokens spacing/typo dans tailwind.config - -## Definition of Done - -- ✅ 5 primitives livrées + EngagementDetailPage refactorée en tabs + SimulationFormPage banners migrés + BackLink dédup × 3 -- ✅ vitest **212+/212+** (4 new specs ajoutées), tsc + lint clean -- ✅ pytest **469/469** intact (zéro backend touché) -- ✅ Toutes tables passent à 32px row density (validation visuelle) -- ✅ Design-reviewer APPROVED (brutalism check sur les 4 new recipes) -- ✅ Code-reviewer APPROVED -- ✅ DESIGN.md amendé (3 subsections additives) -- ✅ CHANGELOG.md entrée sprint 11 -- ✅ PR ouverte sur Gitea avec body +- ❌ Backend i18n (pas de translations DB-side) +- ❌ Multi-langue (uniquement FR pour ce sprint, infra prête mais pas d'EN switcher) +- ❌ Language picker UI +- ❌ ForbiddenState / NotFoundState components creation +- ❌ Color palette / brutalism / token changes +- ❌ New features