fix(direction): le sens du document traduit suit la langue cible, dans les deux sens
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m3s

Persan -> francais : le document traduit restait en lecture de droite a
gauche, heritee de la source. Trois causes :

- PDF : regression du renommage precedent — le choix d'alignement
  testait la fonction is_rtl (toujours vraie) au lieu du parametre :
  tout texte traduit s'alignait a droite, quelle que soit la cible.
  Corrige + test epinglant l'alignement gauche pour une cible latine.
- Word : les marques RTL heritees (bidi, rtl, bidiVisual, notes et
  commentaires compris) sont desormais retirees quand la cible est
  latine ; retrait fait sur une liste figee (l'iterateur lxml sautait
  des elements pendant la suppression).
- Excel : les feuilles heritees d'un affichage droite-a-gauche sont
  remises en lecture gauche-a-droite pour une cible latine.
- PowerPoint : les attributs rtl herites sont retires pour une cible
  latine (alignements visuels conserves).

3 tests de bout en bout nouveaux : document RTL traduit vers le
francais ressort en lecture gauche-a-droite dans les trois formats.
This commit is contained in:
2026-09-01 21:28:46 +02:00
parent 2abd0c0b26
commit 5f3d57b4f7
5 changed files with 268 additions and 26 deletions

View File

@@ -855,6 +855,21 @@ class TestPdfLayoutModeRtl:
assert ok is True
assert page.calls[0]["text"] == "Bonjour le monde"
def test_ltr_target_is_left_aligned(self):
"""Regression guard: a Latin target (e.g. Persian → French) must be
left-aligned. A previous rename once made the alignment test the
truthiness of the imported is_rtl *function* — always true — and
every translated PDF came out right-aligned."""
import fitz
translator = PDFTranslator()
page = _FakePage()
ok = translator._write_translated_block(
page, self._block("Bonjour le monde"), font_path=None, rtl_target=False
)
assert ok is True
assert page.calls[0]["align"] == fitz.TEXT_ALIGN_LEFT
class TestPdfFontSelection:
def test_arabic_script_target_prefers_arabic_font(self, tmp_path, monkeypatch):
@@ -1054,3 +1069,85 @@ class TestPdfCleanModeRtl:
# Presentation forms in the output prove both the shaping and the
# Arabic-capable font reached the PDF (Helvetica cannot encode them).
assert any(0xFB50 <= ord(c) <= 0xFEFF for c in extracted)
# ---------------------------------------------------------------------------
# Normalisation LTR — un document source RTL traduit vers une cible latine
# (ex. persan → français) doit ressortir en lecture gauche-à-droite
# ---------------------------------------------------------------------------
class TestLtrNormalization:
def test_word_rtl_source_latin_target_strips_rtl(self, tmp_path):
"""Persan → français : bidi/rtl/bidiVisual hérités de la source
sont retirés (corps, section, note de bas de page, tableau)."""
# 1) Fabrication d'un document RTL complet (cible fa)
t_fa = WordTranslator(
provider=MockTranslationProvider({"Hello": "FA1", "Extra": "FA2"})
)
doc = Document()
doc.add_paragraph("Hello")
table = doc.add_table(rows=1, cols=2)
table.cell(0, 0).text = "Extra"
fa_in = tmp_path / "in.docx"
doc.save(fa_in)
_inject_footnotes_part(fa_in)
fa_out = tmp_path / "fa.docx"
t_fa.translate_file(fa_in, fa_out, "fa")
# Sanity : la sortie fa porte bien les marques RTL
fa_doc = etree.fromstring(_read_zip_entry(fa_out, "word/document.xml"))
assert fa_doc.find(f".//{{{W_NS}}}bidi") is not None
fa_notes = etree.fromstring(_read_zip_entry(fa_out, "word/footnotes.xml"))
assert fa_notes.find(f".//{{{W_NS}}}bidi") is not None
# 2) Traduction de ce document RTL vers le français
t_fr = WordTranslator(
provider=MockTranslationProvider({"FA1": "Bonjour", "FA2": "ExtraFR"})
)
fr_out = tmp_path / "fr.docx"
t_fr.translate_file(fa_out, fr_out, "fr")
fr_doc = etree.fromstring(_read_zip_entry(fr_out, "word/document.xml"))
assert fr_doc.find(f".//{{{W_NS}}}bidi") is None
assert fr_doc.find(f".//{{{W_NS}}}rtl") is None
assert fr_doc.find(f".//{{{W_NS}}}bidiVisual") is None
fr_notes = etree.fromstring(_read_zip_entry(fr_out, "word/footnotes.xml"))
assert fr_notes.find(f".//{{{W_NS}}}bidi") is None
# La traduction française est bien là
assert "Bonjour" in "".join(t.text or "" for t in fr_doc.iter(f"{{{W_NS}}}t"))
def test_excel_rtl_source_latin_target_restores_ltr(self, tmp_path):
wb = Workbook()
wb.active["A1"] = "سلام"
wb.active.sheet_view.rightToLeft = True # feuille d'origine persane
input_file = tmp_path / "in.xlsx"
wb.save(input_file)
out = tmp_path / "out.xlsx"
ExcelTranslator(provider=MockTranslationProvider({"سلام": "Bonjour"})).translate_file(
input_file, out, "fr"
)
assert not load_workbook(out).active.sheet_view.rightToLeft
def test_pptx_rtl_source_latin_target_strips_rtl(self, tmp_path):
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])
box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
para = box.text_frame.paragraphs[0]
para.text = "سلام"
# Simule un paragraphe d'origine persane
pPr = para._p.get_or_add_pPr()
pPr.set("rtl", "1")
input_file = tmp_path / "in.pptx"
prs.save(input_file)
out = tmp_path / "out.pptx"
PowerPointTranslator(
provider=MockTranslationProvider({"سلام": "Bonjour"})
).translate_file(input_file, out, "fr")
out_para = _find_pptx_paragraph(Presentation(str(out)), "Bonjour")
assert out_para is not None
out_pPr = out_para._p.find(f"{{{A_NS}}}pPr")
assert out_pPr is None or out_pPr.get("rtl") != "1"