polish: rebrand to Fleet Ops, add trilingual i18n, adaptive demo guide, and UX overhaul

Rebrands the product from MobilityOps to Fleet Ops across the UI, backend defaults and
knowledge base, and makes nl-BE/en-GB/fr-BE full first-class languages: i18next with
eager-bundled per-namespace resources, a persisted accessible language switcher (topbar
and mobile drawer), locale-aware date/number formatting, and a coverage test that fails
the build on any missing or empty translation key.

Backend dynamic content (demo scenarios, blocked-reason text, integration status) moves
from fixed English/Dutch prose to stable message codes + params so the frontend can
localize it; the demo knowledge base gains a fully translated NL/EN/FR procedure corpus
(11 documents each) with per-language retrieval and localized evidence-state messages.

The Demo Guide becomes breakpoint-adaptive: a docked rail on extra-wide desktop, a
floating panel that auto-collapses to a persistent, closable progress chip on standard
desktop/tablet, and a collapsed/half/full bottom sheet on mobile -- with scroll+focus+
highlight on "go to this step", Escape handling, and reduced-motion support.

The Data Quality Workbench gets accessible choice-card decisions with a clear primary/
secondary/tertiary action hierarchy; the Automation ledger groups repeated successes and
uses meaningful short refs; the Audit trail groups events by correlation id with human
action labels and readable before/after diffs. Attention Queue, Today's movements,
Vehicles, Bookings and Data Quality rows are fully clickable (stretched-link pattern)
with independent secondary links, keyboard support and mobile touch targets.

Fixes a topbar overflow on mobile caused by the new language switcher (moved into the
mobile drawer at <=960px) and two dangling aria-labelledby references introduced this
session. Updates all affected Playwright specs for the new nl-BE default and the new
Audit/DemoGuide DOM structure, and adds new i18n-coverage, demo-guide-adaptive and
clickable-rows specs. 131 backend tests, Ruff and mypy, and 71 Playwright tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-03 18:33:22 +02:00
co-authored by Claude Sonnet 5
parent 257a4cf6c0
commit 337f8716bb
127 changed files with 5529 additions and 1287 deletions
@@ -0,0 +1,192 @@
{
"list": {
"eyebrow": "Assurance / Atelier",
"title": "Qualité des données",
"description": "Résolvez les exceptions étayées par des preuves avant qu'elles ne perturbent l'exploitation.",
"runScan": "Lancer le contrôle qualité",
"confirmScanTitle": "Confirmer le contrôle qualité",
"confirmScanBody": "Lancer maintenant le contrôle déterministe sur les cinq types de règles ?",
"scanning": "Analyse en cours…",
"confirmScanYes": "Oui, lancer",
"cancel": "Annuler",
"scanComplete": "Contrôle terminé : {{summary}}",
"scanNoNew": "aucun nouveau problème trouvé (les problèmes ouverts existants ne sont pas recréés).",
"scanFailed": "Impossible d'exécuter le contrôle qualité.",
"statusLabel": "Statut",
"statusAll": "Tous les statuts",
"statusOpen": "Ouvert",
"statusDeferred": "Reporté",
"statusResolved": "Résolu",
"statusRejected": "Rejeté",
"ruleTypeLabel": "Type de règle",
"ruleTypeAll": "Tous les types de règles",
"demoScenariosOnly": "Scénarios de démo uniquement",
"loading": "Chargement de l'atelier qualité…",
"queueClear": "La file est vide",
"noIssuesMatch": "Aucun problème ne correspond aux filtres actuels.",
"noDemoIssuesMatch": "Aucun problème de scénario de démo ne correspond",
"noDemoIssuesMatchDetail": "Décochez « Scénarios de démo uniquement » pour voir la file complète.",
"unavailable": "Les problèmes de qualité des données sont actuellement indisponibles.",
"count": "{{count}} problèmes",
"evidenceBacked": "Détection étayée par des preuves",
"columns": {
"reference": "Référence",
"rule": "Règle",
"entity": "Entité",
"severity": "Gravité",
"status": "Statut"
}
},
"ruleTypes": {
"possible_duplicate_customer": "Client peut-être en double",
"missing_required_field": "Champ obligatoire manquant",
"odometer_regression": "Anomalie de kilométrage",
"booking_overlap": "Chevauchement de réservations",
"vehicle_status_conflict": "Conflit de statut du véhicule"
},
"severities": {
"high": "Critique",
"medium": "Avertissement",
"low": "Info"
},
"detail": {
"backLink": "Atelier qualité",
"eyebrow": "Qualité / {{rule}}",
"title": "Examinez les preuves enregistrées et consignez une résolution auditée.",
"notFound": "Ce problème est introuvable.",
"loading": "Chargement des preuves du problème…",
"managerOnly": "L'atelier qualité est visible uniquement pour les Operations Managers.",
"managerOnlyDetail": "Les preuves et résolutions de qualité des données sont visibles uniquement pour les Operations Managers.",
"summary": {
"rule": "Règle",
"entity": "Entité",
"evidenceSummary": "Résumé des preuves"
},
"resolved": {
"title": "Problème {{ref}} résolu",
"body": "La modification a été appliquée et enregistrée dans la piste d'audit.",
"viewAudit": "Voir la piste d'audit",
"viewVehicle": "Voir le véhicule",
"continueDemo": "Poursuivre la démo"
},
"deferOrReject": {
"heading": "Reporter ou rejeter",
"description": "Reportez pour un examen ultérieur, ou rejetez si ce n'est pas un véritable problème.",
"defer": "Reporter",
"reject": "Rejeter",
"deferFailed": "Impossible de reporter ce problème.",
"rejectFailed": "Impossible de rejeter ce problème."
},
"explainer": {
"whatIsWrong": "Ce qui ne va pas",
"whyItMatters": "Pourquoi c'est important",
"possible_duplicate_customer": {
"whatIsWrong": "Deux profils clients partagent des données d'identification (e-mail, téléphone ou un nom très similaire) suffisamment fortement pour qu'il s'agisse probablement de la même personne, enregistrée deux fois.",
"whyItMatters": "Les clients en double répartissent l'historique de réservation entre deux fiches, risquent une double facturation et compliquent les échanges avec le support."
},
"missing_required_field": {
"whatIsWrong": "Cette fiche manque d'informations requises pour un fonctionnement normal (par exemple, un client sans e-mail ni numéro de téléphone enregistré).",
"whyItMatters": "Sans ces données, l'entreprise ne peut pas contacter le client, ni identifier de façon fiable le véhicule pour les contrôles de conformité et de remise."
},
"odometer_regression": {
"whatIsWrong": "Un relevé de kilométrage soumis est inférieur au dernier relevé connu (de référence) du véhicule.",
"whyItMatters": "Un kilométrage en baisse signifie généralement une erreur de saisie ou que les relevés ont été enregistrés pour le mauvais véhicule. Laisser passer cela silencieusement corromprait la planification de l'entretien et l'historique kilométrique à la revente."
},
"booking_overlap": {
"whatIsWrong": "Le même véhicule est engagé sur deux réservations dont les périodes se chevauchent.",
"whyItMatters": "Une seule de ces réservations peut réellement être honorée. Non résolu, un client arriverait pour constater que son véhicule est déjà chez quelqu'un d'autre."
},
"vehicle_status_conflict": {
"whatIsWrong": "Le statut opérationnel enregistré de ce véhicule ne correspond pas à ce que son propre historique de réservations et d'inspections implique.",
"whyItMatters": "Un statut incorrect peut faire apparaître un véhicule indisponible comme réservable, ou masquer un véhicule disponible de la flotte."
}
},
"duplicateCustomer": {
"heading": "Comparer et fusionner",
"description": "Choisissez le client de référence et examinez chaque champ divergent.",
"keepAsSurvivor": "Conserver comme fiche principale",
"differs": "Diffère",
"match": "Identique",
"mergePreview": "{{loser}} deviendra une fiche archivée liée à {{survivor}} ; ses réservations seront transférées.",
"mergeInto": "Fusionner avec {{ref}}",
"confirmMergeTitle": "Confirmer la fusion",
"confirmMergeBody": "Fusionner {{loser}} avec {{survivor}} ? Cette action est irréversible.",
"merging": "Fusion en cours…",
"confirmMergeYes": "Oui, fusionner",
"mergeFailed": "Impossible de fusionner ces clients.",
"bothMissing": "Les deux clients de cette comparaison n'ont pas pu être chargés.",
"fieldColumn": "Champ",
"fields": {
"first_name": "Prénom",
"last_name": "Nom",
"email": "E-mail",
"phone": "Téléphone",
"postal_code": "Code postal",
"city": "Ville"
}
},
"missingField": {
"heading": "Compléter les champs manquants",
"description": "Complétez la fiche pour {{ref}}. Le problème se résout automatiquement dès qu'aucune donnée requise ne manque.",
"atLeastOne": "Au moins un e-mail ou un numéro de téléphone est requis.",
"saveAndRecheck": "Enregistrer et revérifier",
"saving": "Enregistrement…",
"saveFailed": "Impossible d'enregistrer ces champs.",
"fields": {
"first_name": "Prénom",
"last_name": "Nom",
"email": "E-mail",
"phone": "Téléphone",
"registration_number": "Numéro d'immatriculation",
"make": "Marque",
"model": "Modèle",
"location": "Localisation"
}
},
"odometerRegression": {
"heading": "Résoudre l'anomalie de kilométrage",
"description": "Le kilométrage de référence n'est jamais abaissé automatiquement — choisissez comment le régulariser.",
"canonicalOdometer": "Kilométrage de référence",
"decisionLegend": "Décision",
"retainCanonical": "Conserver le relevé de référence",
"retainCanonicalDetail": "Considérer le relevé soumis comme erroné ; rien ne change sur la fiche du véhicule.",
"correctReading": "Corriger le relevé",
"correctReadingDetail": "Mettre à jour à la fois la réservation et le kilométrage de référence avec la valeur correcte.",
"noBookingAttached": "Aucune réservation liée à ce problème, seule l'option « conserver la référence » est donc disponible.",
"booking": "Réservation",
"correctedOdometer": "Kilométrage corrigé (km)",
"note": "Note",
"resolving": "Résolution en cours…",
"resolveIssue": "Résoudre le problème",
"resolveFailed": "Impossible de résoudre ce problème."
},
"bookingOverlap": {
"heading": "Résoudre le chevauchement de réservations",
"description": "Bloquez l'un des deux engagements qui se chevauchent. L'autre conserve son statut actuel.",
"columns": {
"booking": "Réservation",
"window": "Période",
"status": "Statut",
"blockThis": "Bloquer celle-ci"
},
"blockLabel": "Bloquer {{ref}}",
"note": "Note",
"resolving": "Résolution en cours…",
"blockButton": "Bloquer {{ref}}",
"blockButtonFallback": "Bloquer la réservation",
"resolveFailed": "Impossible de résoudre ce chevauchement."
},
"vehicleStatusConflict": {
"heading": "Résoudre le conflit de statut",
"description": "Une règle faisant autorité recommande un statut opérationnel corrigé pour ce véhicule.",
"currentStatus": "Statut actuel",
"calculateAndApply": "Calculer et appliquer le statut recommandé",
"confirmTitle": "Confirmer le changement de statut",
"confirmBody": "Appliquer le statut recommandé faisant autorité pour ce véhicule ?",
"applying": "Application en cours…",
"confirmYes": "Oui, appliquer",
"applied": "Appliqué {{status}} — {{reason}}",
"applyFailed": "Impossible d'appliquer un statut recommandé."
}
}
}