fix(frontend): i18n reactive badges + aria-labels + ExportEngagementButton + noMatch (review polish)

- ExportEngagementButton: full i18n (export.btn, export.btnLoading, export.aria)
- Layout: aria-label nav.brandHome on brand link, nav.themeAria on theme button
- SimulationList: aria-label simulation.list.rowActions on chevron button
- C2TasksPanel: aria-label c2.tasks.col.expand on expand column header
- ImportC2HistoryModal: aria-label common.select on select column header
- MitreMatrixModal: common.collapse/expand interpolated aria-label + mitre.matrix.noMatch
- fr.json: new keys common.expand/collapse/select, nav.brandHome/themeAria, export.*, simulation.list.rowActions, mitre.matrix.noMatch, c2.tasks.col.expand

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-22 00:23:03 +02:00
parent ac0bc8d1b2
commit 2ff097b648
9 changed files with 31 additions and 16 deletions

View File

@@ -57,7 +57,7 @@ export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element {
<table className="w-full text-[14px]">
<thead>
<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.command')}</th>
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.source')}</th>

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronDown, Download } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { downloadEngagementExport, type ExportFormat } from '@/api/exports';
import { useToast } from '@/hooks/useToast';
@@ -14,6 +15,7 @@ const FORMATS: { label: string; value: ExportFormat }[] = [
];
export function ExportEngagementButton({ engagementId }: ExportEngagementButtonProps): JSX.Element {
const { t } = useTranslation();
const { push } = useToast();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState<ExportFormat | null>(null);
@@ -58,11 +60,11 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
onClick={() => setOpen((v) => !v)}
data-testid="export-btn"
>
<Download size={14} aria-hidden /> Export
<Download size={14} aria-hidden /> {t('export.btn')}
</button>
<button
type="button"
aria-label="Export options"
aria-label={t('export.aria')}
aria-expanded={open}
className="btn-outline px-sm"
onClick={() => setOpen((v) => !v)}
@@ -89,7 +91,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
>
{loading === value ? (
<span className="font-mono text-[11px] animate-pulse" aria-hidden>
EXPORTING
{t('export.btnLoading')}
</span>
) : null}
{loading !== value ? label : null}

View File

@@ -140,7 +140,7 @@ export function ImportC2HistoryModal({
<table className="w-full text-[14px]">
<thead>
<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">{t('c2.tasks.col.command')}</th>
<th className="px-md py-sm text-left font-medium text-ink">{t('c2.tasks.col.status')}</th>

View File

@@ -43,7 +43,7 @@ export function Layout(): JSX.Element {
<button
type="button"
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"
>
<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) */}
<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">
<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="text-[20px] font-medium tracking-tight text-slab-text">Mimic</span>
</Link>

View File

@@ -278,7 +278,7 @@ export function MitreMatrixModal({
{hasSubtechniques ? (
<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)}
className={`mr-[2px] flex-shrink-0 text-[9px] w-3 leading-none mt-[1px] ${
isSelected ? 'text-white' : 'text-graphite'
@@ -329,7 +329,7 @@ export function MitreMatrixModal({
);
})}
{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>

View File

@@ -79,7 +79,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
</button>
<button
type="button"
aria-label="More options"
aria-label={t('simulation.list.rowActions')}
aria-expanded={open}
className="btn-primary px-sm"
onClick={() => setOpen((v) => !v)}

View File

@@ -16,7 +16,10 @@
"close": "Fermer",
"confirm": "Confirmer",
"yes": "Oui",
"no": "Non"
"no": "Non",
"expand": "Déplier {{id}}",
"collapse": "Replier {{id}}",
"select": "Sélectionner"
},
"nav": {
"engagements": "Engagements",
@@ -24,8 +27,15 @@
"users": "Utilisateurs",
"signOut": "Se déconnecter",
"brand": "Mimic",
"brandHome": "Accueil Mimic",
"themeAria": "Thème : {{label}} — cliquer pour changer",
"footer": "Mimic — Outillage interne Purple Team. Engagements autorisés uniquement."
},
"export": {
"btn": "Exporter",
"btnLoading": "Export en cours…",
"aria": "Options d'export"
},
"auth": {
"login": {
"title": "Mimic",
@@ -208,7 +218,8 @@
"desc": "Créez votre première simulation pour cet engagement."
},
"new": "Nouvelle simulation",
"error": "Impossible de charger les simulations"
"error": "Impossible de charger les simulations",
"rowActions": "Plus d'options"
}
},
"template": {
@@ -382,7 +393,8 @@
"command": "Commande",
"source": "Source",
"status": "Statut",
"completedAt": "Terminé le"
"completedAt": "Terminé le",
"expand": "Déplier"
},
"refreshing": "Actualisation…"
},
@@ -464,7 +476,8 @@
"applyItem_other": "Appliquer ({{count}} éléments)",
"clearAll": "Tout effacer",
"close": "Fermer",
"retry": "Réessayer"
"retry": "Réessayer",
"noMatch": "Aucun résultat"
},
"field": {
"empty": "Aucune technique associée.",

View File

@@ -47,7 +47,7 @@ describe('ExportEngagementButton', () => {
renderWithProviders(<ExportEngagementButton engagementId={1} />);
expect(screen.getByTestId('export-btn')).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 () => {

View File

@@ -224,7 +224,7 @@ describe('MitreMatrixModal', () => {
await waitFor(() => screen.getByText('T1078'));
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);
expect(screen.getByText(/Default Accounts/)).toBeInTheDocument();