""" Bilingual output: interleave source paragraphs with their translation. Given the ORIGINAL document and the TRANSLATED document (same structure — the pipeline only rewrites run texts), produce a copy of the translated document where each translated body paragraph is preceded by its source paragraph in gray italic. Tables, headers and footers keep the translated version only (duplicating them would double the layout). """ from pathlib import Path from typing import Optional from docx import Document from docx.text.paragraph import Paragraph from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Pt, RGBColor from core.logging import get_logger logger = get_logger(__name__) def make_bilingual_docx( source_path: Path, translated_path: Path, output_path: Path ) -> Optional[Path]: """Create a bilingual .docx (source paragraph above its translation). Returns the output path, or None when the pairing failed (structure mismatch) — callers fall back to the translated-only file. """ source_path = Path(source_path) translated_path = Path(translated_path) output_path = Path(output_path) try: src = Document(str(source_path)) tr = Document(str(translated_path)) except Exception as e: logger.warning("bilingual_open_failed", error=str(e)) return None src_children = list(src.element.body) tr_children = list(tr.element.body) # The pipeline preserves structure exactly; a drift beyond a small # tolerance means pairing by index is unsafe → bail out. if abs(len(src_children) - len(tr_children)) > 0: logger.warning( "bilingual_structure_mismatch", source_elements=len(src_children), translated_elements=len(tr_children), ) if len(src_children) != len(tr_children): return None inserted = 0 for src_el, tr_el in zip(src_children, tr_children): if tr_el.tag != qn("w:p") or src_el.tag != qn("w:p"): continue src_text = "".join( t.text or "" for t in src_el.iter(qn("w:t")) ).strip() if not src_text: continue # Insert a NEW paragraph directly above the translated one, inside # the translated document (keeps styles/sections untouched). new_p = OxmlElement("w:p") tr_el.addprevious(new_p) para = Paragraph(new_p, tr) run = para.add_run(src_text) run.font.size = Pt(9) run.font.italic = True run.font.color.rgb = RGBColor(0x80, 0x80, 0x80) inserted += 1 if inserted == 0: logger.info("bilingual_nothing_inserted") return None try: tr.save(str(output_path)) except Exception as e: logger.warning("bilingual_save_failed", error=str(e)) return None logger.info("bilingual_docx_created", paragraphs=inserted) return output_path