---
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)
