feat(frontend): full FR i18n via react-i18next + 2-tab engagement detail (sprint 12) #14
@@ -57,7 +57,7 @@ export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element {
|
|||||||
<table className="w-full text-[14px]">
|
<table className="w-full text-[14px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-cloud border-b border-hairline">
|
<tr className="bg-cloud border-b border-hairline">
|
||||||
<th className="px-md py-sm text-left font-medium text-ink w-8" aria-label="Expand" />
|
<th className="px-md py-sm text-left font-medium text-ink w-8" aria-label={t('c2.tasks.col.expand')} />
|
||||||
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.task')}</th>
|
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.task')}</th>
|
||||||
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.command')}</th>
|
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.command')}</th>
|
||||||
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.source')}</th>
|
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.source')}</th>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { ChevronDown, Download } from 'lucide-react';
|
import { ChevronDown, Download } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { downloadEngagementExport, type ExportFormat } from '@/api/exports';
|
import { downloadEngagementExport, type ExportFormat } from '@/api/exports';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ const FORMATS: { label: string; value: ExportFormat }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function ExportEngagementButton({ engagementId }: ExportEngagementButtonProps): JSX.Element {
|
export function ExportEngagementButton({ engagementId }: ExportEngagementButtonProps): JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { push } = useToast();
|
const { push } = useToast();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState<ExportFormat | null>(null);
|
const [loading, setLoading] = useState<ExportFormat | null>(null);
|
||||||
@@ -58,11 +60,11 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
|
|||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
data-testid="export-btn"
|
data-testid="export-btn"
|
||||||
>
|
>
|
||||||
<Download size={14} aria-hidden /> Export
|
<Download size={14} aria-hidden /> {t('export.btn')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Export options"
|
aria-label={t('export.aria')}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="btn-outline px-sm"
|
className="btn-outline px-sm"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
@@ -89,7 +91,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
|
|||||||
>
|
>
|
||||||
{loading === value ? (
|
{loading === value ? (
|
||||||
<span className="font-mono text-[11px] animate-pulse" aria-hidden>
|
<span className="font-mono text-[11px] animate-pulse" aria-hidden>
|
||||||
EXPORTING…
|
{t('export.btnLoading')}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{loading !== value ? label : null}
|
{loading !== value ? label : null}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function ImportC2HistoryModal({
|
|||||||
<table className="w-full text-[14px]">
|
<table className="w-full text-[14px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-cloud border-b border-hairline">
|
<tr className="bg-cloud border-b border-hairline">
|
||||||
<th className="px-md py-sm w-8" aria-label="Select" />
|
<th className="px-md py-sm w-8" aria-label={t('common.select')} />
|
||||||
<th className="px-md py-sm text-left font-medium text-ink">#</th>
|
<th className="px-md py-sm text-left font-medium text-ink">#</th>
|
||||||
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.command')}</th>
|
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.command')}</th>
|
||||||
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.status')}</th>
|
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.status')}</th>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function Layout(): JSX.Element {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cycleTheme}
|
onClick={cycleTheme}
|
||||||
aria-label={`Theme: ${themeLabel(theme)} — click to cycle`}
|
aria-label={t('nav.themeAria', { label: themeLabel(theme) })}
|
||||||
className="flex items-center gap-xxs text-[12px] text-slab-muted hover:text-slab-text"
|
className="flex items-center gap-xxs text-[12px] text-slab-muted hover:text-slab-text"
|
||||||
>
|
>
|
||||||
<ThemeIcon theme={theme} />
|
<ThemeIcon theme={theme} />
|
||||||
@@ -64,7 +64,7 @@ export function Layout(): JSX.Element {
|
|||||||
{/* nav-bar-top — fixed dark slab, never inverts (same visual family as utility-strip + footer) */}
|
{/* nav-bar-top — fixed dark slab, never inverts (same visual family as utility-strip + footer) */}
|
||||||
<header className="bg-slab text-slab-text">
|
<header className="bg-slab text-slab-text">
|
||||||
<div className="mx-auto w-full max-w-page px-xl h-16 flex items-center justify-between">
|
<div className="mx-auto w-full max-w-page px-xl h-16 flex items-center justify-between">
|
||||||
<Link to="/engagements" className="flex items-center gap-sm" aria-label="Mimic home">
|
<Link to="/engagements" className="flex items-center gap-sm" aria-label={t('nav.brandHome')}>
|
||||||
<span className="inline-block h-6 w-6 rotate-12 bg-primary" aria-hidden />
|
<span className="inline-block h-6 w-6 rotate-12 bg-primary" aria-hidden />
|
||||||
<span className="text-[20px] font-medium tracking-tight text-slab-text">Mimic</span>
|
<span className="text-[20px] font-medium tracking-tight text-slab-text">Mimic</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ export function MitreMatrixModal({
|
|||||||
{hasSubtechniques ? (
|
{hasSubtechniques ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={isExpanded ? `Collapse ${tech.id}` : `Expand ${tech.id}`}
|
aria-label={isExpanded ? t('common.collapse', { id: tech.id }) : t('common.expand', { id: tech.id })}
|
||||||
onClick={() => toggleExpand(tech.id)}
|
onClick={() => toggleExpand(tech.id)}
|
||||||
className={`mr-[2px] flex-shrink-0 text-[9px] w-3 leading-none mt-[1px] ${
|
className={`mr-[2px] flex-shrink-0 text-[9px] w-3 leading-none mt-[1px] ${
|
||||||
isSelected ? 'text-white' : 'text-graphite'
|
isSelected ? 'text-white' : 'text-graphite'
|
||||||
@@ -329,7 +329,7 @@ export function MitreMatrixModal({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{visibleTechniques.length === 0 && searchLower && (
|
{visibleTechniques.length === 0 && searchLower && (
|
||||||
<div className="px-xs py-xxs text-[10px] text-graphite italic">No match</div>
|
<div className="px-xs py-xxs text-[10px] text-graphite italic">{t('mitre.matrix.noMatch')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="More options"
|
aria-label={t('simulation.list.rowActions')}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="btn-primary px-sm"
|
className="btn-primary px-sm"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"confirm": "Confirmer",
|
"confirm": "Confirmer",
|
||||||
"yes": "Oui",
|
"yes": "Oui",
|
||||||
"no": "Non"
|
"no": "Non",
|
||||||
|
"expand": "Déplier {{id}}",
|
||||||
|
"collapse": "Replier {{id}}",
|
||||||
|
"select": "Sélectionner"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"engagements": "Engagements",
|
"engagements": "Engagements",
|
||||||
@@ -24,8 +27,15 @@
|
|||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
"signOut": "Se déconnecter",
|
"signOut": "Se déconnecter",
|
||||||
"brand": "Mimic",
|
"brand": "Mimic",
|
||||||
|
"brandHome": "Accueil Mimic",
|
||||||
|
"themeAria": "Thème : {{label}} — cliquer pour changer",
|
||||||
"footer": "Mimic — Outillage interne Purple Team. Engagements autorisés uniquement."
|
"footer": "Mimic — Outillage interne Purple Team. Engagements autorisés uniquement."
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"btn": "Exporter",
|
||||||
|
"btnLoading": "Export en cours…",
|
||||||
|
"aria": "Options d'export"
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Mimic",
|
"title": "Mimic",
|
||||||
@@ -208,7 +218,8 @@
|
|||||||
"desc": "Créez votre première simulation pour cet engagement."
|
"desc": "Créez votre première simulation pour cet engagement."
|
||||||
},
|
},
|
||||||
"new": "Nouvelle simulation",
|
"new": "Nouvelle simulation",
|
||||||
"error": "Impossible de charger les simulations"
|
"error": "Impossible de charger les simulations",
|
||||||
|
"rowActions": "Plus d'options"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"template": {
|
"template": {
|
||||||
@@ -382,7 +393,8 @@
|
|||||||
"command": "Commande",
|
"command": "Commande",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"completedAt": "Terminé le"
|
"completedAt": "Terminé le",
|
||||||
|
"expand": "Déplier"
|
||||||
},
|
},
|
||||||
"refreshing": "Actualisation…"
|
"refreshing": "Actualisation…"
|
||||||
},
|
},
|
||||||
@@ -464,7 +476,8 @@
|
|||||||
"applyItem_other": "Appliquer ({{count}} éléments)",
|
"applyItem_other": "Appliquer ({{count}} éléments)",
|
||||||
"clearAll": "Tout effacer",
|
"clearAll": "Tout effacer",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"retry": "Réessayer"
|
"retry": "Réessayer",
|
||||||
|
"noMatch": "Aucun résultat"
|
||||||
},
|
},
|
||||||
"field": {
|
"field": {
|
||||||
"empty": "Aucune technique associée.",
|
"empty": "Aucune technique associée.",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe('ExportEngagementButton', () => {
|
|||||||
renderWithProviders(<ExportEngagementButton engagementId={1} />);
|
renderWithProviders(<ExportEngagementButton engagementId={1} />);
|
||||||
expect(screen.getByTestId('export-btn')).toBeInTheDocument();
|
expect(screen.getByTestId('export-btn')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('export-dropdown-toggle')).toBeInTheDocument();
|
expect(screen.getByTestId('export-dropdown-toggle')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Export')).toBeInTheDocument();
|
expect(screen.getByText('Exporter')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking primary opens dropdown with three formats', async () => {
|
it('clicking primary opens dropdown with three formats', async () => {
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ describe('MitreMatrixModal', () => {
|
|||||||
await waitFor(() => screen.getByText('T1078'));
|
await waitFor(() => screen.getByText('T1078'));
|
||||||
expect(screen.queryByText(/Default Accounts/)).toBeNull();
|
expect(screen.queryByText(/Default Accounts/)).toBeNull();
|
||||||
|
|
||||||
const expandBtn = screen.getByRole('button', { name: /Expand T1078/i });
|
const expandBtn = screen.getByRole('button', { name: /Déplier T1078/i });
|
||||||
await user.click(expandBtn);
|
await user.click(expandBtn);
|
||||||
|
|
||||||
expect(screen.getByText(/Default Accounts/)).toBeInTheDocument();
|
expect(screen.getByText(/Default Accounts/)).toBeInTheDocument();
|
||||||
|
|||||||
Reference in New Issue
Block a user