diff --git a/database/models.py b/database/models.py index 015b769..ce43787 100644 --- a/database/models.py +++ b/database/models.py @@ -165,7 +165,7 @@ class Translation(Base): # Translation details source_language = Column(String(10), default="auto") target_language = Column(String(10), nullable=False) - provider = Column(String(50), nullable=False) # google, deepl, ollama, etc. + provider = Column(String(50), nullable=False) # google, ollama, etc. # Processing info status = Column( diff --git a/frontend/src/components/landing/hero-visual.tsx b/frontend/src/components/landing/hero-visual.tsx index cfdc555..f8f2515 100644 --- a/frontend/src/components/landing/hero-visual.tsx +++ b/frontend/src/components/landing/hero-visual.tsx @@ -1,187 +1,986 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { motion, useReducedMotion, AnimatePresence } from 'framer-motion'; -import { Languages } from 'lucide-react'; +import { useEffect, useState, useCallback } from 'react'; +import { useReducedMotion } from 'framer-motion'; +import { + FileText, + FileSpreadsheet, + Presentation, + FileCheck, + Sparkles, + ArrowRight, + Clock, + Users, + Flame, + Check, + ShieldCheck, + Save, + Undo, + Redo, + Search, + Minus, + Square, + X, + Bold, + Italic, + Underline, + AlignLeft, + AlignCenter, + List, + ListOrdered, + Table as TableIcon, + ChevronDown, + ZoomIn, + Printer, + Maximize2, +} from 'lucide-react'; import { cn } from '@/lib/utils'; /** - * Landing hero visual — the product promise drawn instead of photographed: - * one document whose layout never moves while its language changes. - * Pure geometry (bars, table, chart) + one real translated heading per card. + * Wordly.art — Hero Visual Authentique Office & PDF + * + * Cette animation recrée fidèlement l'environnement réel d'un logiciel bureautique + * (Microsoft Word, Excel, PowerPoint et Adobe PDF) avec : + * - Le ruban, la barre de titre, la règle et la barre d'état officielles. + * - Une vraie page de document A4 flottante avec du vrai texte lisible (≥ 11px). + * - Une onde dorée de numérisation/traduction en temps réel (FR → EN → ES). + * - Une stabilité géométrique absolue : zéro décalage, zéro saut de mise en page. */ -const SOURCE_HEADING = 'Rapport annuel'; +type OfficeFormat = 'docx' | 'xlsx' | 'pptx' | 'pdf'; -const TARGETS = [ - { code: 'EN', heading: 'Annual report', widths: [0.94, 0.82, 0.88, 0.58, 0.76] }, - { code: 'ES', heading: 'Informe anual', widths: [0.9, 0.86, 0.84, 0.64, 0.8] }, - { code: 'DE', heading: 'Jahresbericht', widths: [0.98, 0.78, 0.92, 0.52, 0.7] }, -] as const; - -const SOURCE_WIDTHS = [0.92, 0.8, 0.86, 0.62, 0.78]; - -const CYCLE_MS = 3600; - -function TextLines({ widths, accent = false }: { widths: readonly number[]; accent?: boolean }) { - return ( -
- {widths.map((w, i) => ( - - ))} -
- ); +interface RecipeData { + lang: 'FR' | 'EN' | 'ES'; + langName: string; + langCodeDoc: string; + docTitle: string; + docSubtitle: string; + category: string; + timeLabel: string; + servingsLabel: string; + heatLabel: string; + ingredientsHeading: string; + ingredients: Array<{ amount: string; name: string }>; + tableTitle: string; + tableHeaders: [string, string, string]; + tableValues: [string, string, string]; + stepsHeading: string; + steps: Array<{ num: string; title: string; desc: string }>; + excelFormula: string; + excelHeaders: [string, string, string, string]; + excelRows: Array<[string, string, string, string]>; + excelTotalLabel: string; + excelTotalVal: string; + pptSlideTitle: string; + pptCard1Title: string; + pptCard1Text: string; + pptCard2Title: string; + pptCard2Text: string; + guaranteeText: string; } -function MiniTable() { +const RECIPES: RecipeData[] = [ + { + lang: 'FR', + langName: 'Français (France)', + langCodeDoc: 'FR-FR', + docTitle: 'Tarte fine aux pommes & cannelle', + docSubtitle: 'Recette traditionnelle • Pâtisserie maison de saison', + category: 'Pâtisserie artisanale • Recette n°04', + timeLabel: '15 min de préparation', + servingsLabel: '4 à 6 personnes', + heatLabel: 'Four chaud 180°C', + ingredientsHeading: 'Ingrédients nécessaires', + ingredients: [ + { amount: '1 rouleau', name: 'Pâte feuilletée pur beurre dorée' }, + { amount: '4 pièces', name: 'Pommes Golden finement tranchées' }, + { amount: '25 g', name: 'Beurre doux fondu tiédi' }, + { amount: '2 sachets', name: 'Sucre vanillé naturel des îles' }, + ], + tableTitle: 'Paramètres de cuisson au four', + tableHeaders: ['Position four', 'Préparation', 'Cuisson dorée'], + tableValues: ['Mi-hauteur 180°C', '15 minutes', '25 minutes'], + stepsHeading: 'Instructions de préparation', + steps: [ + { + num: '01', + title: 'Préparer la pâte', + desc: 'Dérouler la pâte sur la plaque et piquer le fond à la fourchette.', + }, + { + num: '02', + title: 'Dresser les pommes', + desc: 'Disposer les lamelles de pommes en rosace harmonieuse et serrée.', + }, + { + num: '03', + title: 'Enfourner et dorer', + desc: 'Badigeonner de beurre fondu puis saupoudrer de sucre vanillé.', + }, + ], + excelFormula: '=SOMME(C3:C6) & " g de garniture"', + excelHeaders: ['Ingrédient', 'Quantité', 'Unité', 'Remarque'], + excelRows: [ + ['Pâte feuilletée', '1', 'rouleau', 'Pur beurre'], + ['Pommes Golden', '4', 'pièces', 'Tranches fines'], + ['Beurre doux', '25', 'grammes', 'Fondu tiède'], + ['Sucre vanillé', '2', 'sachets', 'Arôme naturel'], + ], + excelTotalLabel: 'Temps de cuisson totale', + excelTotalVal: '25 min à 180°C', + pptSlideTitle: 'Recette gourmande : Tarte fine aux pommes', + pptCard1Title: 'Ingrédients clés', + pptCard1Text: 'Pâte feuilletée pur beurre, 4 pommes Golden, beurre doux fondu, sucre vanillé naturel.', + pptCard2Title: 'Cuisson & finition', + pptCard2Text: 'Disposition en rosace serrée, cuisson 25 min à 180°C jusqu’à dorure parfaite.', + guaranteeText: 'Mise en page originale 100% préservée', + }, + { + lang: 'EN', + langName: 'English (United States)', + langCodeDoc: 'EN-US', + docTitle: 'Crisp apple & cinnamon tart', + docSubtitle: 'Traditional recipe • Seasonal homemade pastry', + category: 'Artisanal pastry • Recipe #04', + timeLabel: '15 min preparation time', + servingsLabel: '4 to 6 servings', + heatLabel: 'Hot oven at 180°C', + ingredientsHeading: 'Required ingredients', + ingredients: [ + { amount: '1 sheet', name: 'Golden all-butter puff pastry sheet' }, + { amount: '4 units', name: 'Fresh Golden apples thinly sliced' }, + { amount: '25 g', name: 'Melted unsalted sweet butter' }, + { amount: '2 packs', name: 'Natural island vanilla sugar dust' }, + ], + tableTitle: 'Oven baking specifications', + tableHeaders: ['Oven position', 'Preparation', 'Golden baking'], + tableValues: ['Middle shelf 180°C', '15 minutes', '25 minutes'], + stepsHeading: 'Preparation instructions', + steps: [ + { + num: '01', + title: 'Prepare the crust', + desc: 'Unroll pastry on the tray and prick the base evenly with a fork.', + }, + { + num: '02', + title: 'Arrange the fruit', + desc: 'Place the apple slices in an elegant, tight spiral pattern.', + }, + { + num: '03', + title: 'Bake to golden', + desc: 'Brush with melted butter and dust evenly with vanilla sugar.', + }, + ], + excelFormula: '=SUM(C3:C6) & " g filling total"', + excelHeaders: ['Ingredient', 'Quantity', 'Unit', 'Note'], + excelRows: [ + ['Puff pastry', '1', 'sheet', 'All-butter'], + ['Golden apples', '4', 'units', 'Thin slices'], + ['Unsalted butter', '25', 'grams', 'Melted warm'], + ['Vanilla sugar', '2', 'packs', 'Natural flavor'], + ], + excelTotalLabel: 'Total baking duration', + excelTotalVal: '25 min at 180°C', + pptSlideTitle: 'Gourmet Recipe: Crisp Apple Tart', + pptCard1Title: 'Key Ingredients', + pptCard1Text: 'All-butter puff pastry, 4 Golden apples, melted unsalted butter, natural vanilla sugar.', + pptCard2Title: 'Baking & Finish', + pptCard2Text: 'Arrange in tight spiral, bake 25 min at 180°C until crisp and golden brown.', + guaranteeText: 'Original layout 100% preserved', + }, + { + lang: 'ES', + langName: 'Español (España)', + langCodeDoc: 'ES-ES', + docTitle: 'Tarta fina de manzana & canela', + docSubtitle: 'Receta tradicional • Repostería casera de otoño', + category: 'Repostería artesanal • Receta n°04', + timeLabel: '15 min de preparación', + servingsLabel: '4 a 6 personas', + heatLabel: 'Horno caliente 180°C', + ingredientsHeading: 'Ingredientes necesarios', + ingredients: [ + { amount: '1 lámina', name: 'Masa de hojaldre dorado con mantequilla' }, + { amount: '4 piezas', name: 'Manzanas Golden finamente laminadas' }, + { amount: '25 g', name: 'Mantequilla dulce derretida templada' }, + { amount: '2 sobres', name: 'Azúcar avainillado natural fino' }, + ], + tableTitle: 'Parámetros de horneado al horno', + tableHeaders: ['Posición horno', 'Preparación', 'Horneado dorado'], + tableValues: ['Altura media 180°C', '15 minutos', '25 minutes'], + stepsHeading: 'Instrucciones de preparación', + steps: [ + { + num: '01', + title: 'Preparar la base', + desc: 'Extender la masa sobre la bandeja y pinchar la base con tenedor.', + }, + { + num: '02', + title: 'Colocar la fruta', + desc: 'Disponer las láminas de manzana en una espiral bien apretada.', + }, + { + num: '03', + title: 'Hornear y dorar', + desc: 'Pincelar con mantequilla y espolvorear el azúcar avainillado.', + }, + ], + excelFormula: '=SUMA(C3:C6) & " g de relleno"', + excelHeaders: ['Ingrediente', 'Cantidad', 'Unidad', 'Detalle'], + excelRows: [ + ['Masa hojaldre', '1', 'lámina', 'Con mantequilla'], + ['Manzanas Golden', '4', 'piezas', 'Láminas finas'], + ['Mantequilla dulce', '25', 'gramos', 'Derretida tibia'], + ['Azúcar vainillado', '2', 'sobres', 'Aroma natural'], + ], + excelTotalLabel: 'Tiempo de cocción total', + excelTotalVal: '25 min a 180°C', + pptSlideTitle: 'Receta exquisita: Tarta fina de manzana', + pptCard1Title: 'Ingredientes clave', + pptCard1Text: 'Masa de hojaldre, 4 manzanas Golden, mantequilla dulce derretida, azúcar avainillado.', + pptCard2Title: 'Horneado y acabado', + pptCard2Text: 'Disposición en espiral apretada, hornear 25 min a 180°C hasta que esté dorada.', + guaranteeText: 'Estructura original 100% preservada', + }, +]; + +const FORMAT_CONFIG: Record< + OfficeFormat, + { + ext: string; + label: string; + appTitle: string; + accentColor: string; + headerBg: string; + headerText: string; + statusBg: string; + icon: typeof FileText; + defaultFile: string; + } +> = { + docx: { + ext: '.DOCX', + label: 'Word', + appTitle: 'Word', + accentColor: '#185ABD', + headerBg: 'bg-[#185ABD] dark:bg-[#10438F]', + headerText: 'text-white', + statusBg: 'bg-[#185ABD] dark:bg-[#10438F]', + icon: FileText, + defaultFile: 'Recette_Tarte_Pommes.docx', + }, + xlsx: { + ext: '.XLSX', + label: 'Excel', + appTitle: 'Excel', + accentColor: '#107C41', + headerBg: 'bg-[#107C41] dark:bg-[#0B5C30]', + headerText: 'text-white', + statusBg: 'bg-[#107C41] dark:bg-[#0B5C30]', + icon: FileSpreadsheet, + defaultFile: 'Fiche_Technique_Recette.xlsx', + }, + pptx: { + ext: '.PPTX', + label: 'PowerPoint', + appTitle: 'PowerPoint', + accentColor: '#C43E1C', + headerBg: 'bg-[#C43E1C] dark:bg-[#992F13]', + headerText: 'text-white', + statusBg: 'bg-[#C43E1C] dark:bg-[#992F13]', + icon: Presentation, + defaultFile: 'Presentation_Recette.pptx', + }, + pdf: { + ext: '.PDF', + label: 'PDF Reader', + appTitle: 'Adobe Acrobat Reader', + accentColor: '#E03E2D', + headerBg: 'bg-[#2A2A2A] dark:bg-[#1A1A1A]', + headerText: 'text-white', + statusBg: 'bg-[#2A2A2A] dark:bg-[#1A1A1A]', + icon: FileCheck, + defaultFile: 'Document_Recette_Officiel.pdf', + }, +}; + +const CYCLE_DURATION_MS = 6200; // ~6.2 secondes par cycle +const SCAN_DURATION_MS = 1400; // 1.4s onde dorée + +/** + * Rendu intérieur d'un document Word (.docx) avec vraie mise en page A4 + */ +function WordDocumentSheet({ data }: { data: RecipeData }) { return ( -
- {[0, 1, 2].map((c) => ( -
-
-
-
+
+ {/* En-tête avec repères de marge Word */} +
+
+ + + Style : Document Word Standard + + + Section 1 • Marges normales (2,5 cm) +
- ))} -
- ); -} -function MiniChart() { - const heights = [42, 68, 52, 84]; - return ( -
- {heights.map((h, i) => ( -
- ))} -
- ); -} - -function DocumentCard({ - side, - heading, - widths, - tag, -}: { - side: 'source' | 'target'; - heading: string; - widths: readonly number[]; - tag: string; -}) { - return ( -
- - {tag} - - -

- {heading} -

- - - -
- +
+

+ {data.docTitle} +

+ + {data.category} +
-
- + +

+ {data.docSubtitle} +

+ + {/* Métadonnées */} +
+
+ + {data.timeLabel} +
+ +
+ + {data.servingsLabel} +
+ +
+ + {data.heatLabel} +
- +
- {/* one shimmer sweep per language change — the "translation" moment */} - {side === 'target' && ( - - )} -
- ); -} - -export function HeroVisual() { - const prefersReduced = useReducedMotion(); - const [index, setIndex] = useState(0); - - useEffect(() => { - if (prefersReduced) return; - const id = setInterval(() => setIndex((i) => (i + 1) % TARGETS.length), CYCLE_MS); - return () => clearInterval(id); - }, [prefersReduced]); - - const target = TARGETS[index]; - - return ( -
-
-
- - - {/* connector: the translation itself, dots travelling left to right */} -
-
-
- -
- {!prefersReduced && ( -
- {[0, 1, 2].map((d) => ( - - ))} -
- )} - - - {target.code} - - + {/* Corps 2 colonnes */} +
+ {/* Colonne Ingrédients + Tableau Word */} +
+
+

+ + {data.ingredientsHeading} +

+
    + {data.ingredients.map((item, idx) => ( +
  • + + {item.name} + + + {item.amount} + +
  • + ))} +
- + {/* Vrai Tableau Word avec bordures officielles */} +
+
+ {data.tableTitle} +
+
+ {data.tableHeaders.map((header, hIdx) => ( +
+
+ {header} +
+
+ {data.tableValues[hIdx]} +
+
+ ))} +
+
+
+ + {/* Colonne Instructions numérotées Word */} +
+
+

+ + {data.stepsHeading} +

+
+ {data.steps.map((st, sIdx) => ( +
+ + {st.num} + +
+
+ {st.title} +
+
+ {st.desc} +
+
+
+ ))} +
+
+
+
+ + {/* Pied de page Word */} +
+
+ + {data.guaranteeText} +
+
+ Wordly Engine • Microsoft Word XML
); } + +/** + * Rendu intérieur d'une feuille Excel (.xlsx) avec vraie grille de cellules + */ +function ExcelDocumentSheet({ data }: { data: RecipeData }) { + return ( +
+ {/* Barre de formule Excel */} +
+ + A1:D8 + + fx + + {data.excelFormula} + +
+ + {/* Grille Excel avec entêtes de colonnes A, B, C, D */} +
+ {/* Ligne des colonnes A B C D */} +
+
#
+
A ({data.excelHeaders[0]})
+
B ({data.excelHeaders[1]})
+
C ({data.excelHeaders[2]})
+
D ({data.excelHeaders[3]})
+
+ + {/* Ligne 1 : Titre fusionné */} +
+
+ 1 +
+
+ {data.docTitle} +
+
+ + {/* Lignes de données Excel */} + {data.excelRows.map((row, rIdx) => ( +
+
+ {rIdx + 2} +
+
+ {row[0]} +
+
+ {row[1]} +
+
+ {row[2]} +
+
+ {row[3]} +
+
+ ))} + + {/* Ligne de total / formule */} +
+
+ 7 +
+
+ {data.excelTotalLabel} +
+
+ {data.excelTotalVal} +
+
+
+ + {/* Onglets de classeur Excel en bas */} +
+
+ Feuil1_Recette +
+
+ Feuil2_Calculs +
+
+ + Formules & Cellules 100% intactes +
+
+
+ ); +} + +/** + * Rendu intérieur d'une diapositive PowerPoint (.pptx) + */ +function PptxDocumentSheet({ data }: { data: RecipeData }) { + return ( +
+ {/* Diapositive 16:9 avec cartouche PowerPoint */} +
+
+ + Diapositive 1 / 1 + + + {data.pptSlideTitle} + +
+ + Thème Officiel 16:9 + +
+ + {/* Cartes de contenu de présentation */} +
+
+
+ + {data.pptCard1Title} +
+

+ {data.pptCard1Text} +

+
+ +
+
+ + {data.pptCard2Title} +
+

+ {data.pptCard2Text} +

+
+
+ +
+ + + Blocs textes & vecteurs PowerPoint préservés + + + Affichage Normal • 100% + +
+
+ ); +} + +/** + * Rendu intérieur d'un document PDF vectoriel officiel + */ +function PdfDocumentSheet({ data }: { data: RecipeData }) { + return ( +
+ {/* Barre d'outils Acrobat */} +
+
+ + PDF/A-1b + + Page 1 / 1 +
+
+ 100% + Vectoriel +
+
+ + {/* Page PDF avec repères de coupe */} +
+
+
+

+ {data.docTitle} +

+ + DOCUMENT CERTIFIÉ + +
+ +

+ {data.docSubtitle} — {data.timeLabel} • {data.servingsLabel} • {data.heatLabel}. +

+ +
+
+ {data.ingredientsHeading} + {data.ingredients.slice(0, 3).map((ing, i) => ( +
+ {ing.name} + {ing.amount} +
+ ))} +
+
+ {data.stepsHeading} + {data.steps.slice(0, 2).map((st, i) => ( +
+ {st.num}. {st.desc} +
+ ))} +
+
+
+ +
+ Adobe PostScript • Calque vectoriel préservé + 100% Fidélité PDF +
+
+
+ ); +} + +/** + * Composant principal de démonstration + */ +export function HeroVisual() { + const prefersReduced = useReducedMotion(); + const [format, setFormat] = useState('docx'); + const [langIndex, setLangIndex] = useState(0); + const [prevLangIndex, setPrevLangIndex] = useState(0); + const [isTranslating, setIsTranslating] = useState(false); + const [scanProgress, setScanProgress] = useState(0); + + const cfg = FORMAT_CONFIG[format]; + const currentRecipe = RECIPES[langIndex]; + const prevRecipe = RECIPES[prevLangIndex]; + + // Déclencher une traduction manuelle ou automatique + const triggerTranslation = useCallback((targetLangIndex: number) => { + if (targetLangIndex === langIndex || isTranslating) return; + + setPrevLangIndex(langIndex); + setLangIndex(targetLangIndex); + setIsTranslating(true); + setScanProgress(0); + + const startTime = performance.now(); + const animateScan = (currentTime: number) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / SCAN_DURATION_MS, 1); + setScanProgress(progress * 100); + + if (progress < 1) { + requestAnimationFrame(animateScan); + } else { + setIsTranslating(false); + setPrevLangIndex(targetLangIndex); + } + }; + + requestAnimationFrame(animateScan); + }, [langIndex, isTranslating]); + + // Boucle de rotation automatique des langues (toutes les ~6.2 secondes) + useEffect(() => { + if (prefersReduced) return; + + const interval = setInterval(() => { + setLangIndex((current) => { + const next = (current + 1) % RECIPES.length; + setPrevLangIndex(current); + setIsTranslating(true); + setScanProgress(0); + + const startTime = performance.now(); + const animateScan = (currentTime: number) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / SCAN_DURATION_MS, 1); + setScanProgress(progress * 100); + + if (progress < 1) { + requestAnimationFrame(animateScan); + } else { + setIsTranslating(false); + setPrevLangIndex(next); + } + }; + + requestAnimationFrame(animateScan); + return next; + }); + }, CYCLE_DURATION_MS); + + return () => clearInterval(interval); + }, [prefersReduced]); + + // Sélecteur de rendu selon le format actif + const renderSheet = (data: RecipeData) => { + switch (format) { + case 'xlsx': + return ; + case 'pptx': + return ; + case 'pdf': + return ; + case 'docx': + default: + return ; + } + }; + + return ( +
+ {/* Conteneur de fenêtre logicielle Office / PDF ultra-réaliste */} +
+ + {/* ============================================================ */} + {/* 1. BARRE DE TITRE OFFICIELLE DU LOGICIEL (Word/Excel/PPT/PDF) */} + {/* ============================================================ */} +
+ + {/* Gauche : Icônes d'accès rapide (Enregistrer, Annuler, Rétablir) */} +
+
+ + + +
+ +
+ + + +
+ + + + {cfg.defaultFile} — {cfg.appTitle} + +
+ + {/* Centre : Recherche Office */} +
+ + Rechercher ou demander de l'aide (Alt+Q) +
+ + {/* Droite : Boutons de fenêtre Windows / Mac */} +
+ + + +
+
+ + {/* ============================================================ */} + {/* 2. RUBAN OFFICE / SÉLECTEUR DE FORMAT RÉEL */} + {/* ============================================================ */} +
+ + {/* Onglets des 4 formats bureautiques supportés */} +
+ {(['docx', 'xlsx', 'pptx', 'pdf'] as const).map((fmtKey) => { + const item = FORMAT_CONFIG[fmtKey]; + const isSelected = format === fmtKey; + const Icon = item.icon; + return ( + + ); + })} +
+ + {/* Outils du ruban (Police, Gras, Italique, Alignement) */} +
+
+ {format === 'docx' ? 'Calibri' : format === 'xlsx' ? 'Segoe UI' : 'Arial'} + +
+
+ + + +
+
+
+ + + + +
+
+ + {/* Sélecteur de langue en direct */} +
+ + Langue : + +
+ {RECIPES.map((r, rIdx) => { + const isActive = rIdx === langIndex; + return ( + + ); + })} +
+
+
+ + {/* ============================================================ */} + {/* 3. RÈGLE GRADUÉE WORD (Visible en mode .docx) */} + {/* ============================================================ */} + {format === 'docx' && ( +
+ ◄ 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 cm ► +
+ )} + + {/* ============================================================ */} + {/* 4. PLAN DE TRAVAIL & DOCUMENT A4 FLOTTANT */} + {/* ============================================================ */} +
+ + {/* Feuille de document flottante avec ombre de papier réelle */} +
+ + {/* Conteneur Superposé Grid garantissant 0px de décalage */} +
+ + {/* Couche 1 : Document de base (Langue précédente ou active) */} +
+ {renderSheet(isTranslating ? prevRecipe : currentRecipe)} +
+ + {/* Couche 2 : Document traduit révélé par découpe (Clip-path) */} + {isTranslating && !prefersReduced && ( +
+ {renderSheet(currentRecipe)} +
+ )} + + {/* ============================================================ */} + {/* 5. ONDE DORÉE DE NUMÉRISATION / TRADUCTION */} + {/* ============================================================ */} + {isTranslating && !prefersReduced && ( +
+ {/* Halo lumineux doré au-dessus */} +
+ + {/* Ligne laser dorée principale */} +
+ + {/* Badge central d'indication de traduction */} +
+ + Traduction en direct : {prevRecipe.lang} → {currentRecipe.lang} +
+
+ )} +
+
+
+ + {/* ============================================================ */} + {/* 6. BARRE D'ÉTAT OFFICIELLE EN BAS (Statistiques & Langue) */} + {/* ============================================================ */} +
+ + {/* Statistiques documentaires officielles */} +
+ Page 1 sur 1 + | + 184 mots + | + + {currentRecipe.langName} + +
+ + {/* Sceau de conformité Wordly.art */} +
+ + Mise en page 100% inviolée : + 0 pixel de décalage +
+ + {/* Indicateur de Zoom */} +
+ Mode Page + 100% +
+
+ +
+
+ ); +} diff --git a/translators/word_translator.py b/translators/word_translator.py index 96f285d..0b46ff3 100644 --- a/translators/word_translator.py +++ b/translators/word_translator.py @@ -1378,7 +1378,7 @@ class WordTranslator: is translated whole — not fragment by fragment — and the result is written into the first run while the sibling runs are blanked. This is what keeps mid-sentence bold spans ("This is *very* - important") coherent in the target language, like DeepL's inline + important") coherent in the target language, keeping inline tag handling. Note: python-docx's `paragraph.runs` only returns DIRECT child