feat(frontend): full FR i18n via react-i18next + 2-tab engagement detail (sprint 12) #14
478
frontend/src/i18n/fr.json
Normal file
478
frontend/src/i18n/fr.json
Normal file
@@ -0,0 +1,478 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement…",
|
||||
"cancel": "Annuler",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Modifier",
|
||||
"new": "Nouveau",
|
||||
"loading": "Chargement…",
|
||||
"retry": "Réessayer",
|
||||
"back": "Retour",
|
||||
"dismiss": "Fermer",
|
||||
"view": "Voir",
|
||||
"create": "Créer",
|
||||
"creating": "Création…",
|
||||
"close": "Fermer",
|
||||
"confirm": "Confirmer"
|
||||
},
|
||||
"nav": {
|
||||
"engagements": "Engagements",
|
||||
"templates": "Templates",
|
||||
"users": "Utilisateurs",
|
||||
"signOut": "Se déconnecter",
|
||||
"brand": "Mimic"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "Mimic",
|
||||
"subtitle": "Connectez-vous pour accéder à vos engagements.",
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"signIn": "Se connecter",
|
||||
"signingIn": "Connexion…",
|
||||
"invalid": "Identifiants invalides"
|
||||
},
|
||||
"forbidden": "Accès refusé",
|
||||
"loadingSession": "Chargement de la session…"
|
||||
},
|
||||
"engagement": {
|
||||
"list": {
|
||||
"title": "Engagements",
|
||||
"subtitle": "Missions red team et leur statut.",
|
||||
"new": "Nouveau",
|
||||
"empty": {
|
||||
"title": "Aucun engagement",
|
||||
"desc": "Créez votre premier engagement pour commencer à suivre les missions red team."
|
||||
},
|
||||
"col": {
|
||||
"name": "Nom",
|
||||
"status": "Statut",
|
||||
"start": "Début",
|
||||
"end": "Fin",
|
||||
"createdBy": "Créé par",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"view": "Voir",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"deleteConfirm": "Supprimer l'engagement « {{name}} » ? Cette action est irréversible.",
|
||||
"toast": {
|
||||
"deleted": "Engagement supprimé"
|
||||
},
|
||||
"error": {
|
||||
"load": "Impossible de charger les engagements",
|
||||
"delete": "Impossible de supprimer l'engagement"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"tabs": {
|
||||
"description": "Description",
|
||||
"simulations": "Simulations"
|
||||
},
|
||||
"schedule": {
|
||||
"startDate": "Date de début",
|
||||
"endDate": "Date de fin",
|
||||
"status": "Statut",
|
||||
"createdAt": "Créé le"
|
||||
},
|
||||
"edit": "Modifier",
|
||||
"createdBy": "Créé par",
|
||||
"noDescription": "Aucune description fournie.",
|
||||
"loading": "Chargement de l'engagement…",
|
||||
"errorLoad": "Impossible de charger l'engagement",
|
||||
"notFound": "Engagement introuvable",
|
||||
"backTo": "Retour aux engagements"
|
||||
},
|
||||
"form": {
|
||||
"title": {
|
||||
"new": "Nouvel engagement",
|
||||
"edit": "Modifier l'engagement"
|
||||
},
|
||||
"subtitle": {
|
||||
"new": "Définir le périmètre et la planification.",
|
||||
"edit": "Mettre à jour les détails."
|
||||
},
|
||||
"field": {
|
||||
"name": "Nom",
|
||||
"description": "Description",
|
||||
"startDate": "Date de début",
|
||||
"endDate": "Date de fin",
|
||||
"endDateHint": "Optionnel",
|
||||
"status": "Statut"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Le nom est obligatoire",
|
||||
"startDateRequired": "La date de début est obligatoire",
|
||||
"endDateAfterStart": "La date de fin doit être égale ou postérieure à la date de début"
|
||||
},
|
||||
"btn": {
|
||||
"create": "Créer",
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement…",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"toast": {
|
||||
"created": "Engagement créé",
|
||||
"updated": "Engagement mis à jour"
|
||||
},
|
||||
"error": {
|
||||
"save": "Impossible d'enregistrer l'engagement",
|
||||
"load": "Impossible de charger l'engagement"
|
||||
},
|
||||
"loading": "Chargement…",
|
||||
"c2Config": "Configuration C2"
|
||||
}
|
||||
},
|
||||
"simulation": {
|
||||
"form": {
|
||||
"title": {
|
||||
"new": "Nouvelle simulation"
|
||||
},
|
||||
"field": {
|
||||
"name": "Nom",
|
||||
"mitre": "Techniques et tactiques MITRE",
|
||||
"description": "Description",
|
||||
"commands": "Commandes",
|
||||
"commandsHint": "Une commande par ligne",
|
||||
"prerequisites": "Prérequis",
|
||||
"executedAt": "Exécuté le",
|
||||
"executionResult": "Résultat d'exécution",
|
||||
"logSource": "Source de log",
|
||||
"logs": "Logs",
|
||||
"socComment": "Commentaire SOC",
|
||||
"incidentNumber": "Numéro d'incident"
|
||||
},
|
||||
"btn": {
|
||||
"create": "Créer la simulation",
|
||||
"creating": "Création…",
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement…",
|
||||
"saveSoc": "Enregistrer SOC",
|
||||
"markReview": "Marquer pour révision",
|
||||
"close": "Clore",
|
||||
"reopen": "Rouvrir",
|
||||
"delete": "Supprimer",
|
||||
"executeC2": "Exécuter via C2",
|
||||
"importC2History": "Importer l'historique C2",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"title": "Supprimer la simulation",
|
||||
"desc": "Cette action est permanente. La simulation sera définitivement supprimée.",
|
||||
"confirm": "Supprimer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"banner": {
|
||||
"done": "Cette simulation est terminée et en lecture seule. Utilisez Rouvrir pour effectuer des modifications.",
|
||||
"socNotReady": "La simulation n'est pas encore prête pour la révision — l'équipe red team doit la marquer comme « Révision requise » avant que vous puissiez remplir la section SOC."
|
||||
},
|
||||
"header": {
|
||||
"redTeam": "Red Team",
|
||||
"soc": "SOC"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Le nom est obligatoire"
|
||||
},
|
||||
"toast": {
|
||||
"created": "Simulation créée",
|
||||
"updated": "Simulation mise à jour",
|
||||
"deleted": "Simulation supprimée",
|
||||
"markedReview": "Simulation marquée pour révision",
|
||||
"closed": "Simulation close",
|
||||
"reopened": "Simulation rouverte",
|
||||
"socUpdated": "Rapport SOC mis à jour"
|
||||
},
|
||||
"error": {
|
||||
"create": "Impossible de créer la simulation",
|
||||
"update": "Impossible de mettre à jour la simulation",
|
||||
"delete": "Impossible de supprimer la simulation",
|
||||
"soc": "Impossible de mettre à jour les champs SOC",
|
||||
"transition": "Transition échouée",
|
||||
"load": "Impossible de charger la simulation"
|
||||
},
|
||||
"loading": "Chargement de la simulation…"
|
||||
},
|
||||
"list": {
|
||||
"col": {
|
||||
"name": "Nom",
|
||||
"mitre": "MITRE",
|
||||
"status": "Statut",
|
||||
"executedAt": "Exécuté le"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Aucune simulation",
|
||||
"desc": "Créez votre première simulation pour cet engagement."
|
||||
},
|
||||
"new": "Nouvelle simulation",
|
||||
"error": "Impossible de charger les simulations"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"list": {
|
||||
"title": "Templates",
|
||||
"subtitle": "Modèles de simulation réutilisables.",
|
||||
"new": "Nouveau",
|
||||
"empty": {
|
||||
"title": "Aucun template",
|
||||
"desc": "Créez votre premier template de simulation."
|
||||
},
|
||||
"col": {
|
||||
"name": "Nom",
|
||||
"mitre": "MITRE",
|
||||
"createdBy": "Créé par",
|
||||
"updated": "Modifié"
|
||||
},
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"form": {
|
||||
"title": {
|
||||
"new": "Nouveau template",
|
||||
"edit": "Modifier le template"
|
||||
},
|
||||
"field": {
|
||||
"name": "Nom",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Description du template…",
|
||||
"commands": "Commandes",
|
||||
"commandsHint": "Une commande par ligne",
|
||||
"commandsPlaceholder": "whoami\nnet user",
|
||||
"prerequisites": "Prérequis",
|
||||
"prerequisitesPlaceholder": "Prérequis…",
|
||||
"mitre": "Techniques MITRE",
|
||||
"mitreSearch": "Rechercher une technique…",
|
||||
"mitreOpenMatrix": "Ouvrir la matrice",
|
||||
"mitreEmpty": "Aucune technique associée."
|
||||
},
|
||||
"btn": {
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement…",
|
||||
"delete": "Supprimer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Le nom est obligatoire"
|
||||
},
|
||||
"toast": {
|
||||
"created": "Template créé",
|
||||
"saved": "Template enregistré",
|
||||
"deleted": "Template supprimé"
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"title": "Supprimer le template",
|
||||
"desc": "Cette action est permanente.",
|
||||
"confirm": "Supprimer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"loading": "Chargement du template…",
|
||||
"errorLoad": "Impossible de charger le template",
|
||||
"errorSave": "Impossible d'enregistrer le template",
|
||||
"errorDelete": "Impossible de supprimer le template"
|
||||
},
|
||||
"picker": {
|
||||
"title": "Choisir un template",
|
||||
"empty": "Aucun template disponible.",
|
||||
"loading": "Chargement…"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"admin": {
|
||||
"title": "Utilisateurs",
|
||||
"subtitle": "Gestion des comptes.",
|
||||
"new": "Nouveau",
|
||||
"col": {
|
||||
"username": "Nom d'utilisateur",
|
||||
"role": "Rôle",
|
||||
"createdAt": "Créé le",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"role": {
|
||||
"admin": "Admin",
|
||||
"redteam": "Red Team",
|
||||
"soc": "SOC"
|
||||
},
|
||||
"field": {
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe"
|
||||
},
|
||||
"btn": {
|
||||
"create": "Créer",
|
||||
"creating": "Création…",
|
||||
"resetPassword": "Réinitialiser le mot de passe",
|
||||
"setPassword": "Définir le mot de passe",
|
||||
"delete": "Supprimer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"you": "(vous)",
|
||||
"toast": {
|
||||
"created": "Utilisateur créé",
|
||||
"deleted": "Utilisateur supprimé",
|
||||
"passwordReset": "Mot de passe réinitialisé"
|
||||
},
|
||||
"error": {
|
||||
"create": "Impossible de créer l'utilisateur",
|
||||
"delete": "Impossible de supprimer l'utilisateur",
|
||||
"passwordReset": "Impossible de réinitialiser le mot de passe"
|
||||
},
|
||||
"deleteConfirm": "Supprimer l'utilisateur « {{username}} » ?",
|
||||
"newPassword": "Nouveau mot de passe",
|
||||
"empty": {
|
||||
"title": "Aucun utilisateur",
|
||||
"desc": "Créez le premier compte via le formulaire ci-dessus."
|
||||
}
|
||||
}
|
||||
},
|
||||
"c2": {
|
||||
"config": {
|
||||
"title": "Configuration C2",
|
||||
"disabled": "Les fonctionnalités C2 sont désactivées (aucune clé de chiffrement configurée sur le serveur).",
|
||||
"field": {
|
||||
"url": "URL",
|
||||
"urlHint": "HTTPS requis (ex. https://mythic.lab:7443)",
|
||||
"token": "Token API",
|
||||
"tokenHint": "Token API",
|
||||
"verifyTls": "Vérifier le certificat TLS",
|
||||
"verifyTlsHint": "Décochez uniquement pour un Mythic de lab avec des certificats auto-signés. Désactiver la vérification expose le token API aux attaques MITM."
|
||||
},
|
||||
"btn": {
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement…",
|
||||
"test": "Tester la connexion",
|
||||
"testing": "Test…",
|
||||
"delete": "Supprimer la configuration",
|
||||
"replaceToken": "Remplacer le token"
|
||||
},
|
||||
"toast": {
|
||||
"saved": "Configuration C2 enregistrée",
|
||||
"deleted": "Configuration C2 supprimée"
|
||||
},
|
||||
"error": {
|
||||
"save": "Impossible d'enregistrer la configuration C2",
|
||||
"delete": "Impossible de supprimer la configuration C2"
|
||||
},
|
||||
"connected": "Connecté",
|
||||
"connectionFailed": "Connexion échouée",
|
||||
"deleteConfirm": {
|
||||
"title": "Supprimer la configuration C2",
|
||||
"desc": "La configuration C2 de cet engagement sera supprimée. Le token API sera définitivement effacé.",
|
||||
"confirm": "Supprimer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"loading": "Chargement…"
|
||||
},
|
||||
"tasks": {
|
||||
"title": "Tâches C2",
|
||||
"empty": "Aucune tâche C2.",
|
||||
"col": {
|
||||
"task": "Tâche",
|
||||
"command": "Commande",
|
||||
"source": "Source",
|
||||
"status": "Statut",
|
||||
"completedAt": "Terminé le"
|
||||
},
|
||||
"refreshing": "Actualisation…"
|
||||
},
|
||||
"modal": {
|
||||
"execute": {
|
||||
"title": "Exécuter via C2",
|
||||
"callback": "Agent cible",
|
||||
"btn": {
|
||||
"launch": "Lancer",
|
||||
"launching": "Lancement…",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"commandCount_one": "{{count}} commande",
|
||||
"commandCount_other": "{{count}} commandes",
|
||||
"validation": {
|
||||
"callbackRequired": "Sélectionnez un agent",
|
||||
"commandsRequired": "Aucune commande à exécuter"
|
||||
},
|
||||
"toast": {
|
||||
"launched": "Tâche C2 lancée"
|
||||
},
|
||||
"error": {
|
||||
"launch": "Impossible de lancer la tâche C2"
|
||||
}
|
||||
},
|
||||
"import": {
|
||||
"title": "Importer l'historique C2",
|
||||
"total": "Total",
|
||||
"page": "Page",
|
||||
"of": "sur",
|
||||
"prev": "Précédent",
|
||||
"next": "Suivant",
|
||||
"btn": {
|
||||
"import": "Importer",
|
||||
"importing": "Importation…",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"selected": "sélectionné(s)",
|
||||
"empty": "Aucun historique C2 disponible.",
|
||||
"toast": {
|
||||
"imported": "Historique C2 importé",
|
||||
"partial": "Import partiel"
|
||||
},
|
||||
"error": {
|
||||
"import": "Impossible d'importer l'historique C2"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"title": "Sélectionner un agent",
|
||||
"empty": "Aucun agent disponible.",
|
||||
"hostnameColon": "Hôte :",
|
||||
"lastCheckinColon": "Dernière connexion :"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mitre": {
|
||||
"matrix": {
|
||||
"title": "Matrice MITRE ATT&CK",
|
||||
"filter": "Filtrer…",
|
||||
"loading": "Chargement de la matrice…",
|
||||
"error": "Impossible de charger la matrice MITRE",
|
||||
"applyItem_one": "Appliquer ({{count}} élément)",
|
||||
"applyItem_other": "Appliquer ({{count}} éléments)",
|
||||
"clearAll": "Tout effacer",
|
||||
"close": "Fermer",
|
||||
"retry": "Réessayer"
|
||||
},
|
||||
"field": {
|
||||
"empty": "Aucune technique associée.",
|
||||
"search": "Rechercher une technique…",
|
||||
"openMatrix": "Ouvrir la matrice",
|
||||
"savedToast": "Technique ajoutée",
|
||||
"errorToast": "Impossible d'ajouter la technique"
|
||||
},
|
||||
"tag": {
|
||||
"remove": "Retirer"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"loading": "Chargement…",
|
||||
"error": {
|
||||
"title": "Erreur",
|
||||
"desc": "Une erreur est survenue."
|
||||
},
|
||||
"empty": {
|
||||
"default": "Aucun résultat."
|
||||
},
|
||||
"retry": "Réessayer"
|
||||
},
|
||||
"toast": {
|
||||
"dismiss": "Fermer"
|
||||
},
|
||||
"status": {
|
||||
"engagement": {
|
||||
"planned": "Planifié",
|
||||
"active": "Actif",
|
||||
"closed": "Clôturé"
|
||||
},
|
||||
"simulation": {
|
||||
"pending": "En attente",
|
||||
"in_progress": "En cours",
|
||||
"review_required": "Révision requise",
|
||||
"done": "Terminé"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
frontend/src/i18n/index.ts
Normal file
13
frontend/src/i18n/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import fr from './fr.json';
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: { fr: { translation: fr } },
|
||||
lng: 'fr',
|
||||
fallbackLng: 'fr',
|
||||
interpolation: { escapeValue: false },
|
||||
returnEmptyString: false,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
8
frontend/src/i18n/status.ts
Normal file
8
frontend/src/i18n/status.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import i18n from './index';
|
||||
import type { EngagementStatus, SimulationStatus } from '@/api/types';
|
||||
|
||||
export const engagementStatusLabel = (s: EngagementStatus): string =>
|
||||
i18n.t(`status.engagement.${s}`);
|
||||
|
||||
export const simulationStatusLabel = (s: SimulationStatus): string =>
|
||||
i18n.t(`status.simulation.${s}`);
|
||||
7
frontend/src/lib/format.ts
Normal file
7
frontend/src/lib/format.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const formatDate = (iso: string | null | undefined): string => {
|
||||
if (!iso) return '—';
|
||||
return new Date(iso).toLocaleDateString('fr-FR');
|
||||
};
|
||||
|
||||
export const formatDateTime = (iso: string): string =>
|
||||
new Date(iso).toLocaleString('fr-FR');
|
||||
@@ -5,6 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
import { App } from './App';
|
||||
import { AuthProvider } from './hooks/useAuth';
|
||||
import { ToastProvider } from './hooks/useToast';
|
||||
import './i18n';
|
||||
import './styles/index.css';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
||||
46
frontend/tests/i18n.test.ts
Normal file
46
frontend/tests/i18n.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import i18n from '@/i18n/index';
|
||||
import { engagementStatusLabel, simulationStatusLabel } from '@/i18n/status';
|
||||
import { formatDate, formatDateTime } from '@/lib/format';
|
||||
|
||||
describe('i18n smoke tests', () => {
|
||||
it('resolves common.save to French', () => {
|
||||
expect(i18n.t('common.save')).toBe('Enregistrer');
|
||||
});
|
||||
|
||||
it('missing key returns the key path', () => {
|
||||
expect(i18n.t('this.key.does.not.exist')).toBe('this.key.does.not.exist');
|
||||
});
|
||||
|
||||
it('simulationStatusLabel done → Terminé', () => {
|
||||
expect(simulationStatusLabel('done')).toBe('Terminé');
|
||||
});
|
||||
|
||||
it('engagementStatusLabel active → Actif', () => {
|
||||
expect(engagementStatusLabel('active')).toBe('Actif');
|
||||
});
|
||||
|
||||
it('interpolation inserts the value', () => {
|
||||
expect(i18n.t('engagement.list.deleteConfirm', { name: 'Test' })).toContain('Test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('format helpers', () => {
|
||||
it('formatDate returns — for null', () => {
|
||||
expect(formatDate(null)).toBe('—');
|
||||
});
|
||||
|
||||
it('formatDate returns — for undefined', () => {
|
||||
expect(formatDate(undefined)).toBe('—');
|
||||
});
|
||||
|
||||
it('formatDate formats ISO date in fr-FR', () => {
|
||||
const result = formatDate('2026-06-01');
|
||||
expect(result).toMatch(/\d{2}\/\d{2}\/\d{4}/);
|
||||
});
|
||||
|
||||
it('formatDateTime returns a non-empty string for valid ISO', () => {
|
||||
const result = formatDateTime('2026-06-01T08:00:00');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user