feat(rtl): rendu droite-a-gauche complet pour Word, PowerPoint, Excel et PDF
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m37s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m37s
- source unique RTL_LANGUAGES/is_rtl dans core/languages.py (fin des 3 copies) - Word: bidi partout (corps, tableaux bidiVisual, notes/fin/commentaires, zones de texte, 6 zones d'en-tetes/pieds), insertion OOXML ordonnee, polices cs elargies aux 11 langues - PowerPoint: alignements explicites preserves, notes du presentateur, indice de police <a:cs> insert a sa place - Excel: feuilles affichees de droite a gauche, feuilles graphiques ignorees - PDF: faconnage bidi (arabic-reshaper + python-bidi), polices par ecriture (arabe/hebreu), TTF enregistree pour le PDF recompose - 46 tests nouveaux (tests/test_translators/test_rtl_layout.py), 253 au total
This commit is contained in:
33
_bmad-output/implementation-artifacts/deferred-work.md
Normal file
33
_bmad-output/implementation-artifacts/deferred-work.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Deferred Work
|
||||
|
||||
Entrées ajoutées lors du cadrage de `spec-rendu-fidele-langues-rtl.md` (2026-09-01) :
|
||||
améliorations de qualité identifiées pendant l'audit du pipeline de traduction,
|
||||
volontairement hors du périmètre de la spec RTL pour garder un objectif unique.
|
||||
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Fusionner les runs adjacents de même style dans PowerPoint (et les notes Word traduites run par run) pour traduire des phrases complètes au lieu de fragments.
|
||||
evidence: Word fusionne déjà les runs par signature rPr (word_translator.py l.1368-1475) ; PowerPoint traduit chaque run isolément (pptx_translator.py l.810-834) et les footnotes Word aussi (l.836-846, 909-919, 961-971) — une phrase coupée en plusieurs runs produit des traductions incohérentes. Interdit par la spec RTL (« Never : pas de refonte de la collecte des runs »).
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Rendre les contrôles qualité actionnables : retenter automatiquement les lots dont le script livré ne correspond pas à la langue cible (ex. arabe livré au lieu du persan).
|
||||
evidence: services/quality/ détecte déjà le mauvais script et la confusion arabe/persan (script_detector.py l.89-171, l.236-256) mais tous les contrôles sont en log-only (routes/translate_routes.py l.1644-1779) — aucune traduction n'est jamais corrigée ni retentée.
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Étendre le mode batch JSON (~15 textes par requête) aux providers DeepSeek et MiniMax, comme le fait déjà le provider OpenAI.
|
||||
evidence: openai_provider.py l.511-625 batch via JSON numéroté ; deepseek_provider.py et minimax_provider.py traduisent texte par texte (base.py l.60-74) — coût et latence plus élevés pour ces moteurs.
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Conserver le formatage intra-bloc des PDF (un mot en gras au milieu d'une phrase applique aujourd'hui le gras à tout le bloc).
|
||||
evidence: pdf_translator.py _extract_text_blocks l.541-734 calcule des flags gras/italique globaux par bloc et la réécriture utilise une police unique par bloc (l.905-922).
|
||||
|
||||
Entrées ajoutées après la revue croisée de `spec-rendu-fidele-langues-rtl.md` (2026-09-01) :
|
||||
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Utiliser insert_htmlbox (PyMuPDF) pour le texte RTL des PDF afin de garder un texte copiable et cherchable (ordre logique), au lieu du façonnage en formes de présentation.
|
||||
evidence: arabic_reshaper + python-bidi écrivent les lettres arabes en formes de présentation (U+FB50–U+FEFF) en ordre visuel : un copier-coller depuis le PDF donne un texte inversé et la recherche ne fonctionne plus ; PyMuPDF ≥ 1.22 (déjà dépendance) propose insert_htmlbox qui gère nativement la direction bidi.
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Passer en droite-à-gauche les textes des graphiques et SmartArt Word (parties chart réinjectées après traduction).
|
||||
evidence: les textes de graphiques/diagrammes sont traduits et réinjectés (word_translator.py l.1030-1186, l.1229-1356) mais aucune marque de direction n'est posée sur ces parties — étiquettes arabes affichées gauche-à-droite dans un document RTL. Exclu volontairement par la spec (« Never : pas d'inversion des graphiques »).
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Résoudre l'alignement hérité des masques/dispositions PowerPoint pour ne pas forcer à droite un titre centré par le modèle (sans algn explicite).
|
||||
evidence: _set_pptx_paragraph_rtl pose algn="r" quand algn est absent — or l'alignement réel peut venir du masque ; détecter l'héritage exige de remonter au layout/master (chantier de résolution de placeholder dédié).
|
||||
- source_spec: spec-rendu-fidele-langues-rtl.md
|
||||
summary: Harmoniser RTL_LOCALES du frontend (frontend/src/lib/i18n.tsx, liste ["ar","fa"]) avec la liste serveur à onze langues.
|
||||
evidence: la liste d'affichage RTL de l'interface est indépendante de core/languages.RTL_LANGUAGES ; toute nouvelle langue d'interface RTL (hébreu, ourdou) devra être ajoutée aux deux endroits sans qu'aucun contrôle ne le signale.
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: 'Rendu fidèle des traductions pour les langues RTL (persan, arabe, hébreu…)'
|
||||
type: 'feature'
|
||||
created: '2026-09-01'
|
||||
status: 'done'
|
||||
review_loop_iteration: 0
|
||||
baseline_commit: fddd7b74282f6c1e51d20a70b1ca1e5cb89d5331
|
||||
context: []
|
||||
---
|
||||
|
||||
<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">
|
||||
|
||||
## Intent
|
||||
|
||||
**Problem:** Un document traduit vers une langue de droite à gauche sort dégradé : Excel ignore totalement le sens, PowerPoint force l'alignement à droite même sur les titres centrés et oublie les notes du présentateur, les tableaux Word ne sont pas inversés (ni notes de bas de page, ni zones de texte), et le PDF n'applique ni le façonnage des lettres arabes ni de police adaptée (lettres déconnectées, ordre inversé, carrés). Pour un lecteur persanophone ou arabophone, le résultat est parfois illisible.
|
||||
|
||||
**Approach:** Source de vérité unique pour le RTL dans `core/languages.py`, puis corrections ciblées dans les 4 traducteurs : direction et alignement corrects sans écraser les alignements explicites, inversion des tableaux, couverture des zones oubliées (notes, zones de texte), et pour le PDF façonnage bidi (`arabic-reshaper` + `python-bidi`) avec police couvrant l'écriture arabe.
|
||||
|
||||
## Boundaries & Constraints
|
||||
|
||||
**Always:** Comportement inchangé pour toute cible non-RTL ; alignements explicites (centré, justifié) jamais écrasés ; aucun texte re-traduit ni altéré (seuls direction/alignement/indices de police sont ajoutés) ; `RTL_LANGUAGES` n'existe plus qu'une fois ; si `arabic-reshaper`/`python-bidi` manquent ou échouent, la traduction réussit quand même (texte tel quel + log d'avertissement).
|
||||
|
||||
**Ask First:** Ajouter une dépendance autre que `arabic-reshaper` et `python-bidi` ; modifier le `Dockerfile` (le paquet `fonts-noto` de Debian devrait déjà fournir Noto Naskh Arabic — vérifier dans l'image avant tout changement).
|
||||
|
||||
**Never:** Pas de refonte de la collecte des runs (fusion des runs PowerPoint / notes Word = chantier différé) ; pas de retentative automatique sur les contrôles qualité (différé) ; pas de modification des providers, de l'interface ni du frontend ; pas d'inversion des images ou graphiques vectoriels.
|
||||
|
||||
## I/O & Edge-Case Matrix
|
||||
|
||||
| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|
||||
|----------|--------------|---------------------------|----------------|
|
||||
| Word cible RTL | docx : corps, tableau, note de bas de page, zone de texte | `w:bidi` sur tous les paragraphes, `w:bidiVisual` sur les tableaux, `w:sectPr/w:bidi` | N/A |
|
||||
| PPTX titre centré, cible ar | `algn="ctr"` existant | `rtl="1"` posé, `algn` reste `"ctr"` ; si `algn` absent ou `"l"` → `algn="r"` | N/A |
|
||||
| PPTX avec notes | diapositive avec notes du présentateur | notes traduites ET passées en RTL | N/A |
|
||||
| Excel cible he | xlsx multi-feuilles | `sheet_view.rightToLeft = True` sur chaque feuille | N/A |
|
||||
| PDF mode mise en page, cible fa/ar | pdf avec blocs de texte | texte façonné avant insertion, police couvrant l'arabe, alignement à droite | Police introuvable → police actuelle + warning ; bibliothèque absente → texte non façonné + warning ; jamais d'échec |
|
||||
| PDF scanné / mode texte, cible RTL | OCR ou mode text_only | reportlab : police TTF enregistrée, `wordWrap="RTL"` sur le style | Pas de TTF → Helvetica + warning (dégradation documentée) |
|
||||
| Cible non-RTL (fr, zh…) | tous formats | Aucun attribut RTL ajouté, sortie identique à aujourd'hui | N/A |
|
||||
| Code régional | `fa-IR`, `ar-EG` | `is_rtl()` normalise le préfixe → RTL appliqué | N/A |
|
||||
|
||||
</frozen-after-approval>
|
||||
|
||||
## Code Map
|
||||
|
||||
- `core/languages.py` -- `LANGUAGE_NAMES` l.12-123, `language_name()` l.126-137 : y ajouter `RTL_LANGUAGES` (frozenset) et `is_rtl(code)`.
|
||||
- `translators/word_translator.py` -- copie `RTL_LANGUAGES` l.30-32 (supprimer) ; `CS_FONTS` l.45-50 (ar/he/fa/ur seulement) ; `_set_paragraph_rtl` l.126-153 (modèle : `w:jc` touché seulement sans alignement explicite) ; `_apply_rtl_to_document` l.156-182 (corps + tableaux + en-têtes/pieds ; manque `w:bidiVisual`, zones de texte, notes) ; déclencheur l.422-424 ; notes/comments réécrits en ZIP post-save l.811-995 (parts sans RTL).
|
||||
- `translators/pptx_translator.py` -- copie l.28-30 (supprimer) ; `_set_pptx_paragraph_rtl` l.57-71 (`algn="r"` inconditionnel — bug) ; `_apply_rtl_to_presentation` l.74-78 (pas de `has_notes_slide`) ; `_apply_ea_font_hints` l.114-158 (modèle exact pour `<a:cs>`) ; notes collectées l.313-319 ; déclencheur l.399-401.
|
||||
- `translators/excel_translator.py` -- `translate_file` l.141-175 ; aucun support RTL (0 occurrence) ; sauvegarde en fin de méthode.
|
||||
- `translators/pdf_translator.py` -- copie l.63-64 (supprimer) ; `_FONT_SEARCH_PATHS` l.115-134 (aucune police arabe) ; `_get_font_path` l.187-196 (première trouvée, insensible au script) ; alignement RTL l.903 ; `insert_textbox` l.1017-1049 (pas de façonnage) ; `_generate_clean_pdf` l.1434-1516 (reportlab, aucune police enregistrée).
|
||||
- `requirements.txt` + `pyproject.toml` (dependencies l.7+) -- dépendances dupliquées : mettre les deux à jour.
|
||||
- `docker/backend/Dockerfile` l.30-40 -- `fonts-noto` installé (vérifier le chemin de Noto Naskh Arabic dans l'image avant tout ajout).
|
||||
- `tests/test_translators/` -- modèles : `test_word_translator.py`, `test_pptx_translator.py`, `test_excel_translator.py`, `test_b3_pdf_fixes.py`.
|
||||
- `services/quality/script_detector.py` -- lecture seule (distingue déjà arabe/persan — réutilisable plus tard, hors périmètre).
|
||||
|
||||
## Tasks & Acceptance
|
||||
|
||||
**Execution:**
|
||||
- [x] `core/languages.py` -- Ajouter `RTL_LANGUAGES` ({ar, he, fa, ur, ku, ps, ug, sd, yi, dv, ckb}) et `is_rtl(code) -> bool` (casse et préfixe régional ignorés) -- source unique, fin des 3 copies.
|
||||
- [x] `translators/word_translator.py` -- Importer depuis `core.languages` ; `_apply_rtl_to_document` : `w:bidiVisual` sur chaque `w:tbl` du corps et des en-têtes/pieds (XPath `.//w:tbl`, couvre l'imbriqué), bidi/rtl sur les paragraphes des `w:txbxContent` ; réécriture ZIP (l.811-995) : `w:bidi` sur les `w:pPr` de `footnotes.xml`/`endnotes.xml`/`comments.xml` quand cible RTL ; `CS_FONTS` élargi à ps, ku, sd, ug, yi, dv, ckb (Arial).
|
||||
- [x] `translators/pptx_translator.py` -- Importer ; `algn="r"` seulement si `algn` absent ou `"l"` ; `_apply_rtl_to_presentation` traite aussi `notes_text_frame` quand `has_notes_slide` ; ajouter `_apply_cs_font_hints` (modèle `_apply_ea_font_hints`) posant `<a:cs typeface="Arial">` pour les cibles RTL.
|
||||
- [x] `translators/excel_translator.py` -- Importer `is_rtl` ; avant sauvegarde : `ws.sheet_view.rightToLeft = True` par feuille quand cible RTL.
|
||||
- [x] `translators/pdf_translator.py` -- Importer ; chemins Noto Naskh Arabic / Noto Sans Arabic (Linux) + `C:/Windows/Fonts/arial.ttf` ; `_get_font_path` prend la langue et choisit une police couvrant l'arabe pour les cibles RTL ; `_shape_rtl(text)` (import protégé, fallback texte tel quel + warning) appliquée aux blocs du mode mise en page et à `_generate_clean_pdf` ; dans ce dernier : `pdfmetrics.registerFont` de la TTF si présente + `wordWrap="RTL"` + `fontName`.
|
||||
- [x] `requirements.txt` + `pyproject.toml` -- Ajouter `arabic-reshaper` et `python-bidi` (versions pinnées comme le reste).
|
||||
- [x] `tests/test_translators/test_rtl_layout.py` -- Couvrir la matrice : docx (bidi corps/note, bidiVisual), pptx (algn conservé/posé, notes, `<a:cs>`), xlsx (`rightToLeft`), pdf (façonnage, police, fallback sans bibliothèque), `is_rtl("fa-IR")`/`is_rtl("fr")`.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Given un docx avec tableau et note de bas de page, when traduction vers `fa`, then `w:bidi` sur corps/note/zone de texte et `w:bidiVisual` sur le tableau.
|
||||
- Given un pptx avec un titre `algn="ctr"`, when traduction vers `ar`, then le titre garde `algn="ctr"` et reçoit `rtl="1"`.
|
||||
- Given un xlsx de 3 feuilles, when traduction vers `he`, then les 3 `sheet_view.rightToLeft` sont vrais.
|
||||
- Given un pdf textuel traduit vers `fa`, when mode mise en page, then texte façonné inséré et police arabe sélectionnée si présente.
|
||||
- Given une traduction vers `fr`, then aucun attribut RTL ajouté (sortie inchangée).
|
||||
|
||||
## Spec Change Log
|
||||
|
||||
## Verification
|
||||
|
||||
**Commands:**
|
||||
- `python -m pytest tests/test_translators/test_rtl_layout.py -v` -- expected: tous les tests passent.
|
||||
- `python -m pytest tests/test_translators/ tests/test_language_validation.py -q` -- expected: aucune régression.
|
||||
- `python -c "import arabic_reshaper, bidi.algorithm; print('ok')"` -- expected: ok.
|
||||
|
||||
## Suggested Review Order
|
||||
|
||||
**Source de vérité des langues**
|
||||
|
||||
- Liste unique partagée par les 4 traducteurs, fin des 3 copies locales
|
||||
[`languages.py:142`](../../core/languages.py#L142)
|
||||
|
||||
- Normalisation casse/préfixe régional (fa-IR, AR → RTL ; fr, None → non)
|
||||
[`languages.py:147`](../../core/languages.py#L147)
|
||||
|
||||
**Word — direction et ordre OOXML**
|
||||
|
||||
- Insertion positionnée selon le schéma (bidi/jc/rtl/bidiVisual jamais en fin)
|
||||
[`word_translator.py:187`](../../translators/word_translator.py#L187)
|
||||
|
||||
- Paragraphe RTL : bidi + miroir d'alignement sans toucher centré/justifié
|
||||
[`word_translator.py:202`](../../translators/word_translator.py#L202)
|
||||
|
||||
- Tableaux inversés visuellement (w:bidiVisual)
|
||||
[`word_translator.py:236`](../../translators/word_translator.py#L236)
|
||||
|
||||
- Notes/commentaires : bidi + rtl sur les runs + tableaux, pendant la réécriture ZIP
|
||||
[`word_translator.py:254`](../../translators/word_translator.py#L254)
|
||||
|
||||
- Les 6 zones d'en-têtes/pieds (défaut, première page, pages paires) couvertes
|
||||
[`word_translator.py:288`](../../translators/word_translator.py#L288)
|
||||
|
||||
**PowerPoint — alignements et polices**
|
||||
|
||||
- rtl=1 posé, algn forcé à droite seulement si absent ou « l »
|
||||
[`pptx_translator.py:71`](../../translators/pptx_translator.py#L71)
|
||||
|
||||
- Notes du présentateur incluses dans le passage RTL
|
||||
[`pptx_translator.py:91`](../../translators/pptx_translator.py#L91)
|
||||
|
||||
- Indice de police <a:ea>/<a:cs> inséré à sa place, rPr en tête du run, notes incluses
|
||||
[`pptx_translator.py:136`](../../translators/pptx_translator.py#L136)
|
||||
|
||||
**Excel — sens des feuilles**
|
||||
|
||||
- rightToLeft par feuille, protégé (feuilles graphiques sans sheet_view ignorées)
|
||||
[`excel_translator.py:369`](../../translators/excel_translator.py#L369)
|
||||
|
||||
**PDF — façonnage et polices**
|
||||
|
||||
- Façonnage bidi (lettres liées, bon ordre), repli sans échec si bibliothèques absentes
|
||||
[`pdf_translator.py:69`](../../translators/pdf_translator.py#L69)
|
||||
|
||||
- Police enregistrée une seule fois par chemin, nom unique par processus
|
||||
[`pdf_translator.py:106`](../../translators/pdf_translator.py#L106)
|
||||
|
||||
- Groupe d'écriture (arabe / hébreu / générique) avant le choix de police
|
||||
[`pdf_translator.py:185`](../../translators/pdf_translator.py#L185)
|
||||
|
||||
- Sélection de police sensible à l'écriture cible, cache par groupe
|
||||
[`pdf_translator.py:309`](../../translators/pdf_translator.py#L309)
|
||||
|
||||
- PDF recomposé : TTF enregistrée, wordWrap RTL, texte façonné
|
||||
[`pdf_translator.py:1597`](../../translators/pdf_translator.py#L1597)
|
||||
|
||||
**Périphérie**
|
||||
|
||||
- 46 tests couvrant la matrice (docx/pptx/xlsx/pdf + endnotes/comments/chartsheet)
|
||||
[`test_rtl_layout.py:1`](../../tests/test_translators/test_rtl_layout.py#L1)
|
||||
|
||||
- Dépendances façonnage (requirements.txt + pyproject.toml + uv.lock alignés)
|
||||
[`requirements.txt:11`](../../requirements.txt#L11)
|
||||
Reference in New Issue
Block a user