Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
153 lines
6.2 KiB
TypeScript
153 lines
6.2 KiB
TypeScript
/**
|
|
* Translates raw backend error messages (Symfony, Doctrine, API Platform)
|
|
* into user-friendly French messages for display in toasts/alerts.
|
|
*/
|
|
|
|
const EXACT_MATCHES: Record<string, string> = {
|
|
// UniqueConstraintSubscriber (HTTP 409)
|
|
'nom duplique': 'Un élément avec ce nom existe déjà.',
|
|
|
|
// English backend messages → French
|
|
'Machine not found.': 'Machine introuvable.',
|
|
'Composant not found.': 'Composant introuvable.',
|
|
'Piece not found.': 'Pièce introuvable.',
|
|
'Product not found.': 'Produit introuvable.',
|
|
'Site not found.': 'Site introuvable.',
|
|
'Custom field not found.': 'Champ personnalisé introuvable.',
|
|
'Custom field value not found.': 'Valeur du champ personnalisé introuvable.',
|
|
'Document not found.': 'Document introuvable.',
|
|
'File not found on disk.': 'Le fichier n\'a pas été trouvé sur le serveur.',
|
|
'Invalid document data.': 'Les données du document sont invalides.',
|
|
'Invalid JSON payload.': 'Les données envoyées sont invalides.',
|
|
'Unsupported entity type.': 'Type d\'entité non supporté.',
|
|
'Entity target is missing.': 'La cible de l\'entité est manquante.',
|
|
'customFieldId or customFieldName is required.': 'L\'identifiant du champ personnalisé est requis.',
|
|
|
|
// Symfony validator messages
|
|
'This value should not be blank.': 'Ce champ ne peut pas être vide.',
|
|
'This value is not a valid email address.': 'L\'adresse email n\'est pas valide.',
|
|
'This value is already used.': 'Cette valeur est déjà utilisée.',
|
|
'This field is missing.': 'Un champ obligatoire est manquant.',
|
|
|
|
// HTTP status texts (used in "Erreur XXX: StatusText" fallback)
|
|
'Internal Server Error': 'Erreur interne du serveur. Veuillez réessayer.',
|
|
'Bad Request': 'Requête invalide.',
|
|
'Not Found': 'Ressource introuvable.',
|
|
'Conflict': 'Un élément similaire existe déjà.',
|
|
'Unprocessable Entity': 'Données invalides.',
|
|
'Unprocessable Content': 'Données invalides.',
|
|
'Service Unavailable': 'Service temporairement indisponible. Veuillez réessayer.',
|
|
'Gateway Timeout': 'Le serveur met trop de temps à répondre. Veuillez réessayer.',
|
|
}
|
|
|
|
const TECHNICAL_PATTERNS: Array<[RegExp, string]> = [
|
|
// Database / Doctrine errors
|
|
[/SQLSTATE\[/i, 'Une erreur est survenue. Veuillez réessayer.'],
|
|
[/An exception occurred/i, 'Une erreur est survenue. Veuillez réessayer.'],
|
|
[/Duplicate entry/i, 'Un élément avec ces données existe déjà.'],
|
|
[/unique.*constraint.*violation/i, 'Un élément avec ces données existe déjà.'],
|
|
[/foreign key constraint/i, 'Impossible de supprimer cet élément car il est utilisé ailleurs.'],
|
|
[/violates not-null constraint/i, 'Un champ obligatoire n\'a pas été renseigné.'],
|
|
[/violates check constraint/i, 'Une valeur saisie est invalide.'],
|
|
|
|
// Symfony / API Platform internal messages
|
|
[/Expected argument of type/i, 'Les données envoyées sont invalides.'],
|
|
[/Could not denormalize/i, 'Les données envoyées sont invalides.'],
|
|
[/The JSON value could not be decoded/i, 'Les données envoyées sont invalides.'],
|
|
[/Syntax error.*JSON/i, 'Les données envoyées sont invalides.'],
|
|
[/No route found/i, 'Ressource introuvable.'],
|
|
[/Access Denied/i, 'Permissions insuffisantes pour cette action.'],
|
|
]
|
|
|
|
/**
|
|
* Detects if a message contains technical jargon that should not be shown to users.
|
|
*/
|
|
function containsTechnicalJargon(message: string): boolean {
|
|
const patterns = [
|
|
/stack trace/i,
|
|
/exception/i,
|
|
/\bat\s+[\w\\]+::/,
|
|
/vendor\//,
|
|
/\.php/,
|
|
/doctrine/i,
|
|
/symfony/i,
|
|
/SQLSTATE/i,
|
|
/PDOException/i,
|
|
/DBALException/i,
|
|
/RuntimeException/i,
|
|
/TypeError/i,
|
|
/LogicException/i,
|
|
/InvalidArgumentException/i,
|
|
/UnexpectedValueException/i,
|
|
/constraint.*violation/i,
|
|
/entity.*manager/i,
|
|
/Hydra error/i,
|
|
]
|
|
return patterns.some((p) => p.test(message))
|
|
}
|
|
|
|
/**
|
|
* Translates a raw backend error message into a user-friendly French message.
|
|
*
|
|
* Usage:
|
|
* import { humanizeError } from '~/shared/utils/errorMessages'
|
|
* showError(humanizeError(rawMessage))
|
|
*/
|
|
export function humanizeError(rawMessage: string | undefined | null): string {
|
|
if (!rawMessage) return 'Une erreur est survenue.'
|
|
|
|
const trimmed = rawMessage.trim()
|
|
if (!trimmed) return 'Une erreur est survenue.'
|
|
|
|
// 1. Exact match
|
|
if (EXACT_MATCHES[trimmed]) return EXACT_MATCHES[trimmed]
|
|
|
|
// 2. "Erreur XXX: StatusText" pattern — translate the status text
|
|
const httpMatch = trimmed.match(/^Erreur (\d{3})\s*:\s*(.+)$/)
|
|
if (httpMatch) {
|
|
const statusText = httpMatch[2]!.trim()
|
|
if (EXACT_MATCHES[statusText]) return EXACT_MATCHES[statusText]
|
|
return `Erreur serveur (${httpMatch[1]}). Veuillez réessayer.`
|
|
}
|
|
|
|
// 3. Regex patterns for technical errors
|
|
for (const [pattern, replacement] of TECHNICAL_PATTERNS) {
|
|
if (pattern.test(trimmed)) return replacement
|
|
}
|
|
|
|
// 4. If it contains technical jargon, replace with generic message
|
|
if (containsTechnicalJargon(trimmed)) {
|
|
return 'Une erreur est survenue. Veuillez réessayer.'
|
|
}
|
|
|
|
// 5. Already user-friendly — return as-is
|
|
return trimmed
|
|
}
|
|
|
|
/**
|
|
* Extracts the best error message from various backend response formats.
|
|
* Handles: { message }, { error }, { detail }, { "hydra:description" }
|
|
*/
|
|
export function extractApiErrorMessage(errorData: Record<string, unknown>): string | null {
|
|
if (!errorData || typeof errorData !== 'object') return null
|
|
|
|
// Symfony validator violations — priorité max (message propre sans préfixe champ)
|
|
if (Array.isArray(errorData.violations) && errorData.violations.length > 0) {
|
|
const first = errorData.violations[0] as Record<string, unknown>
|
|
if (typeof first?.message === 'string') return first.message
|
|
}
|
|
|
|
// UniqueConstraintSubscriber format ({ success: false, error: "nom duplique" })
|
|
if (typeof errorData.error === 'string') return errorData.error
|
|
|
|
// Custom controllers format
|
|
if (typeof errorData.message === 'string') return errorData.message
|
|
if (Array.isArray(errorData.message) && typeof errorData.message[0] === 'string') return errorData.message[0]
|
|
|
|
// API Platform hydra format (fallback — peut contenir "propertyPath: message")
|
|
if (typeof errorData['hydra:description'] === 'string') return errorData['hydra:description']
|
|
if (typeof errorData.detail === 'string') return errorData.detail
|
|
|
|
return null
|
|
}
|