diff --git a/frontend/src/app/dashboard/translate/page.tsx b/frontend/src/app/dashboard/translate/page.tsx index f0308de..b049131 100644 --- a/frontend/src/app/dashboard/translate/page.tsx +++ b/frontend/src/app/dashboard/translate/page.tsx @@ -81,12 +81,36 @@ export default function TranslatePage() { const isPdf = upload.file?.name.toLowerCase().endsWith('.pdf') ?? false; + /* ── Human-friendly error messages ──────────────────────────── */ + const humanFriendlyError = (raw: string | null): string => { + if (!raw) return 'Une erreur inattendue est survenue. Réessayez.'; + const r = raw.toLowerCase(); + if (r.includes('0 out of') || r.includes('0 textes') || r.includes('0 texts')) + return "La traduction n'a produit aucun résultat. Vérifiez que le document contient du texte, puis réessayez ou choisissez un autre moteur."; + if (r.includes('api key') || r.includes('api_key') || r.includes('unauthorized') || r.includes('401')) + return "Clé API invalide ou manquante. Contactez l'administrateur pour configurer les clés."; + if (r.includes('quota') || r.includes('rate limit') || r.includes('429')) + return "Limite d'utilisation atteinte. Réessayez dans quelques minutes ou choisissez un autre moteur."; + if (r.includes('timeout') || r.includes('timed out') || r.includes('connexion')) + return "La connexion au service de traduction a expiré. Vérifiez votre réseau et réessayez."; + if (r.includes('not found') || r.includes('404')) + return "La session a expiré. Cliquez sur Réessayer pour relancer la traduction."; + if (r.includes('empty') || r.includes('vide') || r.includes('no translatable')) + return "Le document semble vide ou ne contient pas de texte traduisible (PDF image ?)."; + if (r.includes('unsupported') || r.includes('format')) + return "Format de fichier non supporté ou fichier corrompu."; + if (r.includes('lost connection') || r.includes('internet')) + return "Connexion perdue. Vérifiez votre réseau et réessayez."; + // Generic fallback — show raw but readable + return `Traduction échouée : ${raw.charAt(0).toUpperCase() + raw.slice(1)}`; + }; + useEffect(() => { if (submit.error && submit.error !== lastErrorRef.current) { lastErrorRef.current = submit.error; - showError({ title: t('dashboard.translate.errorNotificationTitle'), description: submit.error }); + showError({ title: 'Traduction échouée', description: humanFriendlyError(submit.error) }); } - }, [submit.error, showError, t]); + }, [submit.error, showError]); // Elapsed timer useEffect(() => { @@ -106,6 +130,13 @@ export default function TranslatePage() { await submit.submitTranslation(upload.file, cfg); }; + const handleRetry = async () => { + submit.reset(); + // Small delay to ensure reset completes + await new Promise(r => setTimeout(r, 50)); + await handleTranslate(); + }; + const handleNewTranslation = () => { submit.reset(); upload.removeFile(); setElapsed(0); }; const handleDownload = async () => { if (!submit.jobId) return; @@ -356,21 +387,46 @@ export default function TranslatePage() { {/* ── FAILED STATE ───────────────────────────────────── */} {showFailed && (
-
-
- -
-

{t('dashboard.translate.progress.failedTitle')}

-

{submit.error}

+ {/* Error message — friendly language */} +
+
+
+ +
+
+

Traduction échouée

+

{humanFriendlyError(submit.error)}

+ + {/* File strip */} {(submit.fileName || upload.file?.name) && upload.file && ( -
+
replaceInputRef.current?.click()} t={t} />
)} + + {/* Action buttons */} +
+ {upload.file && config.isConfigValid && ( + + )} + +
)}
@@ -383,95 +439,111 @@ export default function TranslatePage() { {/* ── CONFIG (upload / configuring / failed) ──────────── */} {(showUpload || showConfiguring || showFailed) && (
-
-

- {t('landing.translate.configuration')} -

+
+
+

+ {t('landing.translate.configuration')} +

-
- +
+ - + - + {/* Glossary — Pro + LLM mode only */} + {config.isPro && config.mode === 'llm' && ( + + )} - {/* PDF mode selector */} - {isPdf && ( -
- -
- - + {/* PDF mode selector */} + {isPdf && ( +
+ +
+ + +
-
- )} + )} +
+
+ {/* ── TRANSLATE BUTTON — sticky at bottom, always visible ── */} +
+ {!upload.file && ( +

↑ Déposez d'abord un fichier

+ )} + {upload.file && !config.targetLang && ( +

↑ Sélectionnez une langue cible

+ )}
diff --git a/routes/translate_routes.py b/routes/translate_routes.py index 3f8907e..07a2ec9 100644 --- a/routes/translate_routes.py +++ b/routes/translate_routes.py @@ -1188,14 +1188,23 @@ async def _run_translation_job( attempted = stats.get("attempted", 0) changed = stats.get("changed", 0) + if attempted == 0 and file_extension in ('.docx', '.xlsx', '.pptx'): + error_msg = ( + "Aucun texte traduisible détecté dans le document. " + "Le fichier est peut-être vide, protégé, ou ne contient que des images." + ) + logger.error(f"Job {job_id}: {error_msg}") + tracker.set_error(error_msg) + return + if attempted > 0: ratio = changed / attempted logger.info(f"Job {job_id}: translation stats — {changed}/{attempted} texts changed ({ratio:.0%})") if changed == 0: error_msg = ( - f"Translation failed: 0 out of {attempted} texts were translated. " - f"The provider ({provider}) may be unavailable or misconfigured. " - f"Check your API keys in admin settings." + f"0 textes sur {attempted} ont été traduits. " + f"Le moteur ({provider}) est peut-être indisponible ou mal configuré. " + f"Vérifiez les clés API dans les paramètres admin." ) logger.error(f"Job {job_id}: {error_msg}") tracker.set_error(error_msg)