perf+security: fix build, secure downloads, dedupe translations, refactor i18n
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m49s

Frontend:
- Fix Framer Motion / motion-dom build error by pinning framer-motion to
  11.18.2 (compatible with React 19 and Next.js 16).
- Add cross-env and build:local script to bypass standalone symlink errors
  on Windows without Developer Mode.
- Allow NEXT_OUTPUT=default to disable standalone output for local builds.
- Refactor i18n: split 14,177-line src/lib/i18n.tsx into per-locale,
  per-namespace JSON files under src/lib/i18n/messages/.
- Load English synchronously; other locales loaded on demand via dynamic
  imports (reduces initial bundle, improves maintainability).
- Remove unused next-intl message files src/messages/en.json and fr.json.

Backend:
- Remove insecure legacy /api/v1/download/{filename} and /api/v1/cleanup/{filename}
  endpoints. The job-based /api/v1/download/{job_id} already enforces ownership.
- Deduplicate texts in TranslationService.translate_batch before sending them
  to the provider, reducing API calls for repeated strings.
- Pin httpx to <0.28 to fix TestClient incompatibility with starlette 0.35.1.
- Add pytest-cov and ruff dev dependencies/config.

DevOps:
- Remove hardcoded Grafana password from docker-compose.yml and
  docker-compose.monitoring.yml; use GRAFANA_PASSWORD env var.
- Change default TRANSLATION_SERVICE from ollama to google in
  docker-compose.yml (Ollama is an optional profile).
- Add GRAFANA_PASSWORD to .env.example.
- Add .coverage and frontend/pnpm-workspace.yaml to .gitignore.

Tests:
- Update API versioning tests for removed legacy endpoints.
- Add tests/test_translation_service.py for deduplication behavior.

Verified:
- pnpm run build:local passes.
- uv run pytest tests/test_providers/* tests/test_translation_service.py
  tests/test_story_3_5_api_versioning.py tests/test_download_endpoint.py
  tests/test_translators/test_excel_translator.py: provider/translator tests
  pass; one pre-existing French error-message test still fails (message is
  returned in English, unrelated to this change).
This commit is contained in:
2026-06-14 16:44:18 +02:00
parent eda6821632
commit fa637abff0
365 changed files with 23910 additions and 14955 deletions

View File

@@ -0,0 +1,115 @@
{
"dashboard.nav.translate": "Traduire",
"dashboard.nav.profile": "Mon Profil",
"dashboard.nav.settings": "Paramètres",
"dashboard.nav.context": "Contexte",
"dashboard.nav.services": "Services",
"dashboard.nav.apiKeys": "Clés API",
"dashboard.nav.glossaries": "Glossaires & Contexte",
"dashboard.header.title": "Dashboard",
"dashboard.header.subtitle": "Gérez vos traductions",
"dashboard.header.toggleMenu": "Menu",
"dashboard.header.profileTitle": "Mon profil",
"dashboard.sidebar.theme": "Thème",
"dashboard.sidebar.signOut": "Déconnexion",
"dashboard.sidebar.backHome": "Retour à l'accueil",
"dashboard.sidebar.upgradeToPro": "Passer Pro →",
"dashboard.translate.pageTitle": "Traduire un document",
"dashboard.translate.pageSubtitle": "Importez un fichier et choisissez la langue cible",
"dashboard.translate.errorNotificationTitle": "Erreur",
"dashboard.translate.dropzone.uploadAria": "Zone de dépôt de fichier",
"dashboard.translate.dropzone.title": "Glissez-déposez votre fichier ici",
"dashboard.translate.dropzone.subtitle": "ou cliquez pour sélectionner (DOCX, XLSX, PPTX, PDF)",
"dashboard.translate.dropzone.replaceFile": "Remplacer le fichier",
"dashboard.translate.language.source": "Langue source",
"dashboard.translate.language.target": "Langue cible",
"dashboard.translate.language.loading": "Chargement des langues…",
"dashboard.translate.language.autoDetect": "Auto-détection",
"dashboard.translate.language.selectPlaceholder": "Sélectionner…",
"dashboard.translate.language.loadErrorPrefix": "Erreur de chargement des langues",
"dashboard.translate.provider.loading": "Chargement des fournisseurs…",
"dashboard.translate.provider.noneConfigured": "Aucun fournisseur configuré",
"dashboard.translate.provider.modelTitle": "Modèle",
"dashboard.translate.provider.sectionTitle": "Fournisseur",
"dashboard.translate.provider.llmDivider": "IA · Context-Aware",
"dashboard.translate.provider.llmDividerPro": "IA · Context-Aware (Pro)",
"dashboard.translate.provider.upgrade": "Passer Pro",
"dashboard.translate.provider.upgradeSuffix": "pour débloquer la traduction IA",
"dashboard.translate.provider.tabStandard": "Standard",
"dashboard.translate.provider.tabLLM": "Multi-Modèles IA",
"dashboard.translate.translateImages": "Traduire les images",
"dashboard.translate.translateImagesDesc": "Extraire et traduire le texte des images (vision requise)",
"dashboard.translate.trust.zeroRetention": "Rétention zéro",
"dashboard.translate.trust.deletedAfter": "Fichiers supprimés après traitement",
"dashboard.translate.actions.uploading": "Chargement…",
"dashboard.translate.actions.translate": "Traduire",
"dashboard.translate.actions.filePrefix": "Fichier : ",
"dashboard.translate.actions.cancel": "Annuler",
"dashboard.translate.actions.tryAgain": "Réessayer",
"dashboard.translate.steps.uploading": "Chargement du fichier…",
"dashboard.translate.steps.starting": "Démarrage de la traduction…",
"dashboard.translate.complete.title": "Traduction terminée !",
"dashboard.translate.complete.descNamed": "Votre fichier {name} a été traduit avec succès.",
"dashboard.translate.complete.descGeneric": "Votre fichier a été traduit avec succès.",
"dashboard.translate.complete.downloading": "Téléchargement…",
"dashboard.translate.complete.download": "Télécharger",
"dashboard.translate.complete.newTranslation": "Nouvelle traduction",
"dashboard.translate.complete.toastOkTitle": "Succès",
"dashboard.translate.complete.toastOkDesc": "{name} a été téléchargé avec succès.",
"dashboard.translate.complete.toastFailTitle": "Échec",
"dashboard.translate.complete.toastFailDesc": "La traduction a échoué. Veuillez réessayer.",
"dashboard.translate.sourceDocument": "Document source",
"dashboard.translate.configuration": "Configuration",
"dashboard.translate.translating": "Traduction en cours",
"dashboard.translate.liveMonitor": "Moniteur en direct",
"dashboard.translate.summary": "Résumé",
"dashboard.translate.engine": "Moteur",
"dashboard.translate.confidence": "Confiance",
"dashboard.translate.cancel": "Annuler",
"dashboard.translate.segments": "Segments",
"dashboard.translate.characters": "Caractères",
"dashboard.translate.elapsed": "Écoulé",
"dashboard.translate.segPerMin": "Seg/min",
"dashboard.translate.highQuality": "Haute qualité",
"dashboard.translate.quality": "Qualité",
"dashboard.translate.completed": "Traduction terminée",
"dashboard.translate.replace": "Remplacer",
"dashboard.translate.pdfMode.title": "Mode de traduction PDF",
"dashboard.translate.pdfMode.preserveLayout": "Préserver la mise en page",
"dashboard.translate.pdfMode.textOnly": "Texte uniquement",
"dashboard.translate.pdfMode.preserveLayoutDesc": "Conserve les images, tableaux et formatage. Idéal pour les PDF simples.",
"dashboard.translate.pdfMode.textOnlyDesc": "Traduit tout le texte parfaitement. Sortie propre, sans problème de mise en page.",
"dashboard.translate.pipeline.upload": "Upload",
"dashboard.translate.pipeline.analyze": "Analyse",
"dashboard.translate.pipeline.translate": "Traduction",
"dashboard.translate.pipeline.rebuild": "Reconstruction",
"dashboard.translate.pipeline.finalize": "Finalisation",
"dashboard.translate.progress.processingFallback": "Traitement en cours…",
"dashboard.translate.progress.connectionLost": "Connexion perdue. Reconnexion…",
"dashboard.translate.progress.failedTitle": "Traduction échouée",
"dashboard.translate.error.unexpected": "Une erreur inattendue est survenue. Réessayez.",
"dashboard.translate.error.noResult": "La traduction n'a produit aucun résultat. Vérifiez que le document contient du texte, puis réessayez ou choisissez un autre moteur.",
"dashboard.translate.error.apiKey": "Clé API invalide ou manquante. Contactez l'administrateur pour configurer les clés.",
"dashboard.translate.error.quota": "Limite d'utilisation atteinte. Réessayez dans quelques minutes ou choisissez un autre moteur.",
"dashboard.translate.error.timeout": "La connexion au service de traduction a expiré. Vérifiez votre réseau et réessayez.",
"dashboard.translate.error.sessionExpired": "La session a expiré. Cliquez sur Réessayer pour relancer la traduction.",
"dashboard.translate.error.empty": "Le document semble vide ou ne contient pas de texte traduisible (PDF image ?).",
"dashboard.translate.error.unsupported": "Format de fichier non supporté ou fichier corrompu.",
"dashboard.translate.error.connection": "Connexion perdue. Vérifiez votre réseau et réessayez.",
"dashboard.translate.error.generic": "Traduction échouée : {detail}",
"dashboard.translate.error.title": "Traduction échouée",
"dashboard.translate.retry": "Réessayer la traduction",
"dashboard.translate.newFile": "Nouveau fichier",
"dashboard.translate.modeAI": "Mode IA",
"dashboard.translate.modeClassic": "Mode Classique",
"dashboard.translate.glossaryLLMHint": "Glossaires disponibles en mode IA",
"dashboard.translate.submitting": "Envoi en cours...",
"dashboard.translate.submit": "Lancer la traduction",
"dashboard.translate.noFile": "Déposez d'abord un fichier",
"dashboard.translate.noTargetLang": "Sélectionnez une langue cible",
"dashboard.topbar.interfaceLabel": "Interface de traduction",
"dashboard.topbar.premiumAccess": "Accès Premium",
"dashboard.checkoutSyncError": "Erreur lors de la synchronisation du paiement.",
"dashboard.networkRefresh": "Erreur réseau. Veuillez rafraîchir la page.",
"dashboard.continueToTranslate": "Continuer vers la traduction"
}