docs(sprint-12): plan i18n FR (react-i18next) + EngagementDetail tab merge
This commit is contained in:
376
tasks/todo.md
376
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).
|
**Base** : `sprint/11-spectrum-ux` (PR #13 ouverte, pas encore mergée). Sprint 12 enchaîne directement.
|
||||||
**Branch** : `sprint/11-spectrum-ux`.
|
**Worktree** : `.claude/worktrees/sprint-12-i18n-fr/` (dédié — conforme à la mémoire worktree-per-sprint).
|
||||||
**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.
|
**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)
|
## Décisions binding (lockées par user)
|
||||||
|
|
||||||
- **Q1 → B** : Tabs primitive **+ consumer EngagementDetailPage** (3 tabs : Schedule / Description / Simulations au lieu du stack vertical actuel).
|
1. **Approche i18n** : `react-i18next` complet (deps `react-i18next` + `i18next`, JSON FR, hook `useTranslation`).
|
||||||
- **Q2 → B** : **Compact density GLOBAL** — toutes les tables passent à 32px row, pas opt-in. User accepte le tradeoff WCAG (touch target) sur Engagements list.
|
2. **Acronymes** : RT, SOC, MITRE, ATT&CK, C2, BAS, TLS, MITM, JWT, IP, URL, API, ID — **gardés en anglais** dans les valeurs FR.
|
||||||
- **Q3 → A** : AlertBanner + refactor `SimulationFormPage` (Done banner + SOC-blocked banner).
|
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** :
|
**Current** (3 tabs Schedule / Description / Simulations) :
|
||||||
- `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.
|
```tsx
|
||||||
- `frontend/src/components/Tabs.tsx` (NEW) — composant `<Tabs items={[{id, label, count?}]} activeId onChange />`. Underline variant.
|
type TabId = 'schedule' | 'description' | 'simulations';
|
||||||
- `frontend/src/styles/index.css` — nouvelles recipes :
|
const items: TabItem<TabId>[] = [
|
||||||
- `.tab-underline` — `text-graphite caption-bold cursor-pointer border-b-2 border-transparent hover:text-ink` (instantané, pas de transition)
|
{ id: 'schedule', label: 'Schedule' },
|
||||||
- `.tab-underline-active` — `text-primary border-primary` (override)
|
{ id: 'description', label: 'Description' },
|
||||||
- `.tab-count-pill` — `rounded-pill bg-cloud text-graphite text-[11px] px-xs py-0 font-mono`
|
{ id: 'simulations', label: 'Simulations', count },
|
||||||
- `.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)
|
|
||||||
|
|
||||||
**Brutalism check** :
|
**Target** (2 tabs) :
|
||||||
- Tab : `border-b-2` only, pas de bg, pas de rounded, pas de transition
|
```tsx
|
||||||
- Pill : `rounded-pill` autorisé (exception status pill DESIGN.md L125)
|
type TabId = 'description' | 'simulations';
|
||||||
- Hover : `hover:text-ink` instantané
|
const items: TabItem<TabId>[] = [
|
||||||
|
{ 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
|
||||||
|
<header className="flex items-start justify-between gap-md mb-md">
|
||||||
|
<dl className="grid grid-cols-2 gap-x-lg gap-y-xxs text-caption-md">
|
||||||
|
<dt className="text-graphite">Start date</dt> <dd>{formatDate(eng.start_date)}</dd>
|
||||||
|
<dt className="text-graphite">End date</dt> <dd>{eng.end_date ? formatDate(eng.end_date) : '—'}</dd>
|
||||||
|
<dt className="text-graphite">Status</dt> <dd><StatusBadge status={eng.status} /></dd>
|
||||||
|
<dt className="text-graphite">Created at</dt> <dd>{formatDate(eng.created_at)}</dd>
|
||||||
|
</dl>
|
||||||
|
<Link to={`/engagements/${id}/edit`} className="btn-outline">Edit</Link>
|
||||||
|
</header>
|
||||||
|
<hr className="my-md border-hairline" />
|
||||||
|
<h2 className="text-display-sm mb-sm">Description</h2>
|
||||||
|
{eng.description || <p className="text-graphite">No description provided.</p>}
|
||||||
|
```
|
||||||
|
|
||||||
**Files** :
|
- Default `useHashTab('description')` au lieu de `'schedule'`.
|
||||||
- `frontend/src/components/AlertBanner.tsx` (NEW) — `<AlertBanner variant="error|warn|success|info" title? children />`. 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}`.
|
- Header de la page (BackLink + name + créé par + Edit + Export) reste **au-dessus des tabs**, intouché.
|
||||||
- `frontend/src/styles/index.css` — 4 nouvelles recipes :
|
- Mettre à jour `EngagementDetailPage.test.tsx` si une assertion vise le tab "Schedule".
|
||||||
- `.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) → `<AlertBanner variant="success">` / `<AlertBanner variant="warn">`.
|
|
||||||
- `DESIGN.md` — extend `### Toast Notifications` avec subsection `### Inline Banners` (additive, mêmes 4 variants).
|
|
||||||
|
|
||||||
**Note** : `bloom-deep`, `warn`, `success`, `primary` sont les tokens existants — pas de nouveau token couleur.
|
**Note** : les strings ci-dessus seront ensuite traduites dans le pass i18n. Pas la peine de les écrire en FR dans commit 1.
|
||||||
|
|
||||||
### Livrable 3 — `BackLink` component (dédup)
|
|
||||||
|
|
||||||
**Files** :
|
|
||||||
- `frontend/src/components/BackLink.tsx` (NEW) — `<BackLink to="/engagements">Back to engagements</BackLink>`. 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 `<table` dans `frontend/src/`)
|
|
||||||
- `DESIGN.md` — `### Data Tables` section : modifier la ligne "Row min-height 44px" → "Row min-height 32px (compact density, all surfaces)". Documenter le tradeoff WCAG (touch target dérogé en faveur de la densité opérateur — c'est un BAS tool, pas un site grand public).
|
|
||||||
|
|
||||||
**ATTENTION** : WCAG SC 2.5.5 (Target Size: Minimum) recommande 44×44. Tu déroges. Garder les boutons d'action des rows ≥ 24px d'icône avec padding pour préserver le hit area minimal. Tester en clavier + lecteur d'écran nicht regressé.
|
|
||||||
|
|
||||||
### Livrable 5 — DESIGN.md amendments + CHANGELOG
|
|
||||||
|
|
||||||
**Files** :
|
|
||||||
- `DESIGN.md` :
|
|
||||||
- `### Navigation` : nouvelle subsection "Sub-page tabs" (livrable 1)
|
|
||||||
- `### Toast Notifications` : nouvelle subsection "Inline Banners" (livrable 2)
|
|
||||||
- `### Data Tables` : ligne row min-height passe à 32px + paragraphe rationale (livrable 4)
|
|
||||||
- `CHANGELOG.md` : entrée Sprint 11 listant chaque primitive + ses tokens DESIGN.md.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sequencing
|
## Task 2 — Full FR translation via react-i18next
|
||||||
|
|
||||||
1. **frontend-builder** : livrables 1 + 2 + 3 + 4 + 5 en cascade. Une seule passe. Commits atomiques :
|
### Structure cible
|
||||||
```
|
|
||||||
feat(frontend): add Tabs primitive + useHashTab hook
|
|
||||||
feat(frontend): refactor EngagementDetailPage to 3-tab layout
|
|
||||||
feat(frontend): add AlertBanner component + 4 semantic variants
|
|
||||||
refactor(frontend): migrate SimulationFormPage banners to AlertBanner
|
|
||||||
feat(frontend): add BackLink helper + dedup 3 hand-rolled instances
|
|
||||||
feat(frontend): compact table density (32px row) global
|
|
||||||
docs(design): add tabs / inline banners / compact-table-density to DESIGN.md
|
|
||||||
```
|
|
||||||
(collapse OK si plus efficace ; ~5-7 commits attendus)
|
|
||||||
2. **design-reviewer** : visual + token compliance check. Focus :
|
|
||||||
- Underline tabs : pas de transition, pas de bg fill, border-b instantané
|
|
||||||
- Count pill : `rounded-pill` est dans l'exception DESIGN.md, OK
|
|
||||||
- AlertBanner : `border-l-4` semantic OK, pas de shadow
|
|
||||||
- BackLink : caption + icon align baseline
|
|
||||||
- 32px tables : densité lisible ? Hit area boutons d'action OK ?
|
|
||||||
3. **code-reviewer** : sprint diff. Focus :
|
|
||||||
- Aucune transition / shadow / rounded leak
|
|
||||||
- useHashTab : `hashchange` listener cleanup dans useEffect return
|
|
||||||
- Tabs : prop API stable, fallback default ID si hash invalide
|
|
||||||
- AlertBanner : ARIA `role="alert"` ou `role="status"` selon variant
|
|
||||||
- BackLink : `to` est `string` ou objet React Router ? Choisir un seul
|
|
||||||
4. **spec-reviewer** : `SPEC.md` n'a pas vocation à mentionner ces primitives, **skip ce sprint** sauf si tu détectes une feature côté backend qui aurait dérivé.
|
|
||||||
|
|
||||||
## Contraintes tests
|
```
|
||||||
|
frontend/src/i18n/
|
||||||
|
├── index.ts # init i18next, single FR locale
|
||||||
|
├── fr.json # toutes les keys FR
|
||||||
|
└── status.ts # engagementStatusLabel + simulationStatusLabel
|
||||||
|
```
|
||||||
|
|
||||||
- `vitest --run` baseline **212/212** doit tenir.
|
### Init (`frontend/src/i18n/index.ts`)
|
||||||
- **Ajouter 4 specs** (1 par new component) :
|
```ts
|
||||||
- `Tabs.test.tsx` — active state, hash sync, count pill render
|
import i18n from 'i18next';
|
||||||
- `AlertBanner.test.tsx` — 4 variants rendent les bonnes classes
|
import { initReactI18next } from 'react-i18next';
|
||||||
- `BackLink.test.tsx` — render + click navigation
|
import fr from './fr.json';
|
||||||
- `useHashTab.test.tsx` — hash read/write
|
|
||||||
- `tsc --noEmit` + `eslint --max-warnings=0` clean.
|
i18n.use(initReactI18next).init({
|
||||||
- Brutalism unit assertions : pour chaque new component, un test qui vérifie `expect(el).not.toHaveClass('rounded-md')` (sauf pill counter) + `.not.toHaveClass(/transition-/)` + `.not.toHaveClass(/shadow-/)`.
|
resources: { fr: { translation: fr } },
|
||||||
|
lng: 'fr',
|
||||||
|
fallbackLng: 'fr',
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
returnEmptyString: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
|
```
|
||||||
|
|
||||||
|
Mount dans `frontend/src/main.tsx` : `import './i18n'` avant `<App />`.
|
||||||
|
|
||||||
|
### Pattern usage
|
||||||
|
```tsx
|
||||||
|
const { t } = useTranslation();
|
||||||
|
<button>{t('common.save')}</button>
|
||||||
|
<h1>{t('engagement.list.title')}</h1>
|
||||||
|
<p>{t('engagement.delete.confirm', { name: eng.name })}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
## Out of scope (explicite)
|
||||||
|
|
||||||
- ❌ Side nav / drawer / breadcrumb component — top nav + BackLink suffit
|
- ❌ Backend i18n (pas de translations DB-side)
|
||||||
- ❌ Global engagement selector top-bar (Spectrum pattern rejeté)
|
- ❌ Multi-langue (uniquement FR pour ce sprint, infra prête mais pas d'EN switcher)
|
||||||
- ❌ Editor dynamic-column grid — pas d'éditeur dans Mimic
|
- ❌ Language picker UI
|
||||||
- ❌ Dark mode rework
|
- ❌ ForbiddenState / NotFoundState components creation
|
||||||
- ❌ Toast queue system (les toasts actuels via `useToast` restent)
|
- ❌ Color palette / brutalism / token changes
|
||||||
- ❌ PDF/document deliverable
|
- ❌ New features
|
||||||
- ❌ 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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user