From 10d4293f445940430a882bad6266f81fdc340056 Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 23:12:57 +0200 Subject: [PATCH] docs(sprint-12): plan i18n FR (react-i18next) + EngagementDetail tab merge --- tasks/todo.md | 376 +++++++++++++++++++++++++++++++------------------- 1 file changed, 236 insertions(+), 140 deletions(-) diff --git a/tasks/todo.md b/tasks/todo.md index 02b3964..11df455 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,164 +1,260 @@ -# Sprint 11 — Spectrum UX port : 4 primitives + compact density global +# Sprint 12 — i18n FR complet + EngagementDetailPage tab merge -**Base** : `origin/main` (PR #12 mergée — sprint 8 + 9 + 10 en). -**Branch** : `sprint/11-spectrum-ux`. -**Scope** : frontend-only sprint, 5 livrables coordonnés. Pas de backend, pas de schéma, pas de nouvelle API. Pas de modif tokens couleurs/spacing/font dans tailwind.config. +**Base** : `sprint/11-spectrum-ux` (PR #13 ouverte, pas encore mergée). Sprint 12 enchaîne directement. +**Worktree** : `.claude/worktrees/sprint-12-i18n-fr/` (dédié — conforme à la mémoire worktree-per-sprint). +**Branch** : `sprint/12-i18n-fr`. +**Scope** : frontend uniquement. Backend intouché. --- -## Contexte - -Analyse Spectrum (`/home/user/Documents/01_Projects/spectrum`) faite via workflow. Résultat : Spectrum n'a **pas de design system** (Tailwind raw + violet ramp + recipes inlinées 30× partout). Mimic est structurellement meilleur. On ne porte pas la philosophie Spectrum — on extrait **5 patterns isolés** qui survivent au brutalisme Mimic. - -## Constraints absolues - -1. **Primary `#024ad8` (Electric Blue) reste**. Pas de violet, pas de spectrum ramp. -2. **Brutalisme reste** : `rounded-none` sur containers (sauf status pills + tab count pills + avatars), zero `transition-*`, zero `shadow-*`, hairline 1px borders. -3. **Pas de modif `tailwind.config.*`** sauf si une nouvelle classe utility est strictement nécessaire (justifier dans le PR). -4. **DESIGN.md amendments additives uniquement** — pas de modif des tokens existants. -5. **Mono uniquement pour data** (IDs MITRE, dates ISO, commands, etc.) — rule sprint 7. - ## Décisions binding (lockées par user) -- **Q1 → B** : Tabs primitive **+ consumer EngagementDetailPage** (3 tabs : Schedule / Description / Simulations au lieu du stack vertical actuel). -- **Q2 → B** : **Compact density GLOBAL** — toutes les tables passent à 32px row, pas opt-in. User accepte le tradeoff WCAG (touch target) sur Engagements list. -- **Q3 → A** : AlertBanner + refactor `SimulationFormPage` (Done banner + SOC-blocked banner). +1. **Approche i18n** : `react-i18next` complet (deps `react-i18next` + `i18next`, JSON FR, hook `useTranslation`). +2. **Acronymes** : RT, SOC, MITRE, ATT&CK, C2, BAS, TLS, MITM, JWT, IP, URL, API, ID — **gardés en anglais** dans les valeurs FR. +3. **Status enum** : valeurs DB (`pending`, `in_progress`, `done`, `planned`, `active`, `closed`) gardées EN — traduction au rendering layer via `i18n/status.ts`. +4. **Format de date** : `fr-FR` locale (`jj/mm/aaaa`, ex. `20/06/2026`). +5. **Ton toasts/alerts** : **bref** (ex. `"Engagement créé"`, pas `"Engagement enregistré avec succès"`). +6. **Schedule → Description tab** : merger Schedule INTO Description. Résultat : 2 tabs (Description + Simulations). + +## Contraintes constantes + +- Primary `#024ad8` Electric Blue intouchée. +- Brutalisme intact : `rounded-none` partout sauf status pills + tab count pills + avatars, zéro `transition-*`, zéro `shadow-*`. +- `tailwind.config.*` inchangé sauf cas exceptionnel justifié. +- DESIGN.md amendments additives uniquement. +- Mono uniquement pour data (IDs MITRE, dates ISO côté backend, commands, etc.). --- -## Livrables +## Task 1 — Merge Schedule into Description (mécanique, commit 1) -### Livrable 1 — `Tabs` primitive + `useHashTab` hook + consumer +**File** : `frontend/src/pages/EngagementDetailPage.tsx` -**Files** : -- `frontend/src/hooks/useHashTab.ts` (NEW) — ~30 LoC. Reads `window.location.hash`, falls back to default ID, listens to `hashchange`, updates URL without reload. TypeScript pur, zéro style. -- `frontend/src/components/Tabs.tsx` (NEW) — composant ``. Underline variant. -- `frontend/src/styles/index.css` — nouvelles recipes : - - `.tab-underline` — `text-graphite caption-bold cursor-pointer border-b-2 border-transparent hover:text-ink` (instantané, pas de transition) - - `.tab-underline-active` — `text-primary border-primary` (override) - - `.tab-count-pill` — `rounded-pill bg-cloud text-graphite text-[11px] px-xs py-0 font-mono` - - `.tab-count-pill-active` — `bg-primary-soft text-primary` (override) -- `frontend/src/pages/EngagementDetailPage.tsx` — refactor en 3 tabs : - - Tab 1 "Schedule" (id `schedule`, default) — dates + statut + bouton "Edit engagement" - - Tab 2 "Description" (id `description`) — texte description - - Tab 3 "Simulations" (id `simulations`) — la liste actuelle de simulations - - Count pills sur tab 3 = nombre de simulations - - Tabs branchées sur `useHashTab('schedule')` -- `DESIGN.md` — nouvelle subsection `### Navigation › Sub-page tabs` (additive) +**Current** (3 tabs Schedule / Description / Simulations) : +```tsx +type TabId = 'schedule' | 'description' | 'simulations'; +const items: TabItem[] = [ + { id: 'schedule', label: 'Schedule' }, + { id: 'description', label: 'Description' }, + { id: 'simulations', label: 'Simulations', count }, +]; +``` -**Brutalism check** : -- Tab : `border-b-2` only, pas de bg, pas de rounded, pas de transition -- Pill : `rounded-pill` autorisé (exception status pill DESIGN.md L125) -- Hover : `hover:text-ink` instantané +**Target** (2 tabs) : +```tsx +type TabId = 'description' | 'simulations'; +const items: TabItem[] = [ + { id: 'description', label: 'Description' }, + { id: 'simulations', label: 'Simulations', count }, +]; +``` -### Livrable 2 — `AlertBanner` component + 4 recipes +**Migration du contenu Schedule** : déplacer dans le panel Description, en HEADER (au-dessus du paragraphe description) : +```tsx +
+
+
Start date
{formatDate(eng.start_date)}
+
End date
{eng.end_date ? formatDate(eng.end_date) : '—'}
+
Status
+
Created at
{formatDate(eng.created_at)}
+
+ Edit +
+
+

Description

+{eng.description ||

No description provided.

} +``` -**Files** : -- `frontend/src/components/AlertBanner.tsx` (NEW) — ``. Brutalist : border-l-4 strip semantic, fill `bg-paper`, border `border-hairline` partout sauf le strip gauche, `rounded-none`. Icon Lucide à gauche (`AlertCircle` error, `AlertTriangle` warn, `CheckCircle` success, `Info` info) à `size={16}`. -- `frontend/src/styles/index.css` — 4 nouvelles recipes : - - `.alert-error` — `bg-paper border border-hairline border-l-4 border-l-bloom-deep px-md py-sm flex items-start gap-sm` - - `.alert-warn` — same + `border-l-warn` - - `.alert-success` — same + `border-l-success` - - `.alert-info` — same + `border-l-primary` -- `frontend/src/pages/SimulationFormPage.tsx` — refactor 2 banners hand-rollés (~L300 Done banner + ~L310 SOC-blocked banner) → `` / ``. -- `DESIGN.md` — extend `### Toast Notifications` avec subsection `### Inline Banners` (additive, mêmes 4 variants). +- Default `useHashTab('description')` au lieu de `'schedule'`. +- Header de la page (BackLink + name + créé par + Edit + Export) reste **au-dessus des tabs**, intouché. +- Mettre à jour `EngagementDetailPage.test.tsx` si une assertion vise le tab "Schedule". -**Note** : `bloom-deep`, `warn`, `success`, `primary` sont les tokens existants — pas de nouveau token couleur. - -### Livrable 3 — `BackLink` component (dédup) - -**Files** : -- `frontend/src/components/BackLink.tsx` (NEW) — `Back to engagements`. Renders `← {children}` (ArrowLeft Lucide size=14, gap-xxs). Style : `text-graphite hover:text-primary caption-md` instantané. -- Refactor consumers (3 instances hand-rollées) : - - `frontend/src/pages/EngagementDetailPage.tsx` (~L35) - - `frontend/src/pages/SimulationFormPage.tsx` (back-link en edit mode) - - `frontend/src/pages/TemplateFormPage.tsx` (back-link en edit mode) - -### Livrable 4 — `.table-compact` recipe + apply GLOBAL - -**Files** : -- `frontend/src/styles/index.css` — nouvelle recipe (mais appliquée comme default, pas opt-in cette sprint per user decision Q2-B) : - - Soit : modifier la recipe `.card-product table` existante pour passer à 32px row, `caption-md` text, `py-xxs px-xs` cells, divider `border-hairline`. - - Soit : ajouter `.table-compact` puis l'appliquer à TOUTES les list pages (Engagements, Templates, Users, etc.). - - **Choix builder** : pick the cleaner path. Préférer modifier le default si toutes les tables doivent suivre, sinon `.table-compact` + apply partout. -- Apply sur : - - `frontend/src/pages/EngagementsListPage.tsx` - - `frontend/src/pages/TemplatesListPage.tsx` - - `frontend/src/pages/UsersAdminPage.tsx` - - `frontend/src/components/SimulationList.tsx` (utilisé dans EngagementDetailPage) - - Toute autre table dans le code (grep ``. + +### Pattern usage +```tsx +const { t } = useTranslation(); + +

{t('engagement.list.title')}

+

{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