From ad0a3f5cacf7acd0341b79abf256cd75af8357df Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 23:51:12 +0200 Subject: [PATCH] feat(frontend): i18n C2 components (config card, tasks panel, execute/import modals, picker) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/C2CallbackPicker.tsx | 23 +++++---- frontend/src/components/C2ConfigCard.tsx | 49 ++++++++++--------- frontend/src/components/C2TasksPanel.tsx | 18 ++++--- frontend/src/components/ExecuteViaC2Modal.tsx | 20 ++++---- .../src/components/ImportC2HistoryModal.tsx | 44 +++++++++-------- frontend/src/i18n/fr.json | 28 +++++++++-- .../tests/components/C2ConfigCard.test.tsx | 10 ++-- .../tests/components/C2TasksPanel.test.tsx | 2 +- .../components/ExecuteViaC2Modal.test.tsx | 4 +- .../components/ImportC2HistoryModal.test.tsx | 8 +-- 10 files changed, 117 insertions(+), 89 deletions(-) diff --git a/frontend/src/components/C2CallbackPicker.tsx b/frontend/src/components/C2CallbackPicker.tsx index 7c760c0..ac4f683 100644 --- a/frontend/src/components/C2CallbackPicker.tsx +++ b/frontend/src/components/C2CallbackPicker.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import { extractApiError } from '@/api/client'; import type { C2Callback } from '@/api/types'; @@ -20,20 +21,22 @@ export function C2CallbackPicker({ onSelect, rowTestId = 'c2-callback-row', }: C2CallbackPickerProps): JSX.Element { + const { t } = useTranslation(); + if (isLoading) { - return

Loading callbacks…

; + return

{t('state.loading')}

; } if (isError) { return (

- Could not load callbacks: {extractApiError(error, 'Unknown error')} + {extractApiError(error, t('state.error.desc'))}

); } if (callbacks.length === 0) { - return

No callbacks available.

; + return

{t('c2.modal.picker.empty')}

; } return ( @@ -41,12 +44,12 @@ export function C2CallbackPicker({ - - - - - - + + + + + + @@ -78,7 +81,7 @@ export function C2CallbackPicker({ : 'bg-cloud text-graphite border border-hairline' }`} > - {cb.active ? 'Active' : 'Inactive'} + {cb.active ? t('c2.modal.picker.status.active') : t('c2.modal.picker.status.inactive')} diff --git a/frontend/src/components/C2ConfigCard.tsx b/frontend/src/components/C2ConfigCard.tsx index 7dc2955..5e0cf1f 100644 --- a/frontend/src/components/C2ConfigCard.tsx +++ b/frontend/src/components/C2ConfigCard.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, type FormEvent } from 'react'; +import { useTranslation } from 'react-i18next'; import { extractApiError } from '@/api/client'; import { useC2Config, useDeleteC2Config, useTestC2Config, useUpdateC2Config } from '@/hooks/useC2'; import { ConfirmDialog } from './ConfirmDialog'; @@ -10,6 +11,7 @@ interface C2ConfigCardProps { } export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { + const { t } = useTranslation(); const { push } = useToast(); const configQuery = useC2Config(engagementId); @@ -55,11 +57,11 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { try { await updateMutation.mutateAsync(input); - push('C2 configuration saved', 'success'); + push(t('c2.config.toast.saved'), 'success'); setToken(''); setReplaceToken(false); } catch (err) { - push(extractApiError(err, 'Could not save C2 configuration'), 'error'); + push(extractApiError(err, t('c2.config.error.save')), 'error'); } }; @@ -68,13 +70,13 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { setTestResult(null); try { await deleteMutation.mutateAsync(); - push('C2 configuration removed', 'success'); + push(t('c2.config.toast.deleted'), 'success'); setUrl(''); setToken(''); setVerifyTls(false); setReplaceToken(false); } catch (err) { - push(extractApiError(err, 'Could not remove C2 configuration'), 'error'); + push(extractApiError(err, t('c2.config.error.delete')), 'error'); } }; @@ -84,10 +86,10 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { const result = await testMutation.mutateAsync(); setTestResult({ ok: result.ok, - message: result.ok ? 'Connected' : (result.error ?? 'Connection failed'), + message: result.ok ? t('c2.config.connected') : (result.error ?? t('c2.config.connectionFailed')), }); } catch (err) { - setTestResult({ ok: false, message: extractApiError(err, 'Test failed') }); + setTestResult({ ok: false, message: extractApiError(err, t('c2.config.connectionFailed')) }); } }; @@ -98,25 +100,25 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { data-testid="c2-config-card" className="card-product flex flex-col gap-md" > -

C2 configuration

+

{t('c2.config.title')}

{is503 && (
- C2 features are disabled (server has no encryption key configured). + {t('c2.config.disabled')}
)} {configQuery.isLoading ? ( -

Loading…

+

{t('c2.config.loading')}

) : ( - + {config?.has_token && !replaceToken ? (
setReplaceToken(true)} disabled={disabled} > - Replace token + {t('c2.config.btn.replaceToken')}
) : ( @@ -158,7 +160,7 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { data-testid="c2-token-input" type="password" name="api_token" - placeholder={config?.has_token ? 'Enter new token' : 'API token'} + placeholder={config?.has_token ? t('c2.config.field.tokenHint') : t('c2.config.field.token')} value={token} onChange={(e) => setToken(e.target.value)} disabled={disabled} @@ -178,12 +180,11 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { className="h-4 w-4 accent-primary" />

- Uncheck only for lab Mythic with self-signed certificates. Disabling - verification exposes the API token to MITM. + {t('c2.config.field.verifyTlsHint')}

@@ -194,7 +195,7 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { className="btn-primary" disabled={disabled || submitting} > - {updateMutation.isPending ? 'Saving…' : 'Save'} + {updateMutation.isPending ? t('c2.config.btn.saving') : t('c2.config.btn.save')} {testResult !== null && ( @@ -223,7 +224,7 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { onClick={() => setShowDeleteConfirm(true)} disabled={disabled || submitting} > - Delete configuration + {t('c2.config.btn.delete')} )} @@ -232,10 +233,10 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { {showDeleteConfirm && ( setShowDeleteConfirm(false)} diff --git a/frontend/src/components/C2TasksPanel.tsx b/frontend/src/components/C2TasksPanel.tsx index 138729a..0a9e8f8 100644 --- a/frontend/src/components/C2TasksPanel.tsx +++ b/frontend/src/components/C2TasksPanel.tsx @@ -1,5 +1,6 @@ import { Fragment, useState } from 'react'; import { ChevronRight, ChevronDown } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { useC2Tasks } from '@/hooks/useC2'; import { C2TaskStatusBadge } from './C2TaskStatusBadge'; @@ -8,6 +9,7 @@ interface C2TasksPanelProps { } export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element { + const { t } = useTranslation(); const query = useC2Tasks(simulationId, { enabled: true }); const [expandedIds, setExpandedIds] = useState>(new Set()); @@ -33,13 +35,13 @@ export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element { className="card-product flex flex-col gap-md" >
-

C2 Tasks

+

{t('c2.tasks.title')}

{isRefreshing && ( - Refreshing… + {t('c2.tasks.refreshing')} )}
@@ -47,7 +49,7 @@ export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element { {tasks.length === 0 ? (

- No C2 tasks yet. Use Execute via C2 to launch commands. + {t('c2.tasks.empty')}

) : ( @@ -56,11 +58,11 @@ export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element {
- - - - + + + + + diff --git a/frontend/src/components/ExecuteViaC2Modal.tsx b/frontend/src/components/ExecuteViaC2Modal.tsx index 5eee92b..fdae5f1 100644 --- a/frontend/src/components/ExecuteViaC2Modal.tsx +++ b/frontend/src/components/ExecuteViaC2Modal.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { extractApiError } from '@/api/client'; import { useC2Callbacks, useExecuteC2 } from '@/hooks/useC2'; import { C2CallbackPicker } from './C2CallbackPicker'; @@ -17,6 +18,7 @@ export function ExecuteViaC2Modal({ initialCommands, onClose, }: ExecuteViaC2ModalProps): JSX.Element { + const { t } = useTranslation(); const { push } = useToast(); const callbacksQuery = useC2Callbacks(engagementId, { enabled: true }); @@ -43,10 +45,10 @@ export function ExecuteViaC2Modal({ callback_display_id: selectedId, commands: commandLines, }); - push(`${result.tasks.length} task(s) submitted`, 'success'); + push(t('c2.modal.execute.toast.launched', { count: result.tasks.length }), 'success'); onClose(); } catch (err) { - setSubmitError(extractApiError(err, 'Could not execute via C2')); + setSubmitError(extractApiError(err, t('c2.modal.execute.error.launch'))); } }; @@ -62,12 +64,12 @@ export function ExecuteViaC2Modal({

- Execute via C2 + {t('c2.modal.execute.title')}

{/* Callback picker */}
- Select callback + {t('c2.modal.execute.callback')}
Display IDActiveHostUserDomainLast check-in{t('c2.modal.picker.col.displayId')}{t('c2.modal.picker.col.active')}{t('c2.modal.picker.col.host')}{t('c2.modal.picker.col.user')}{t('c2.modal.picker.col.domain')}{t('c2.modal.picker.col.lastCheckin')}
{cb.host}
- TaskCommandSourceStatusCompleted at{t('c2.tasks.col.task')}{t('c2.tasks.col.command')}{t('c2.tasks.col.source')}{t('c2.tasks.col.status')}{t('c2.tasks.col.completedAt')}