feat(translation): quality pipeline overhaul + new features (audit 2026-08-29)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s

Translation quality & format preservation:
- Word: merge adjacent same-format runs into one unit (sentence-level
  coherence like inline-tag handling); translate comments/balloons;
  dedupe textbox collection (was translated twice); RTL no longer
  overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs)
- PPTX: chart translations now actually reach the output file
  (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP);
  CJK typeface hints (a:ea)
- Excel: sheet renames no longer break references — rewrite cell
  formulas (3D/quoted), defined names, data validations, cond. formats
- PDF: bold/italic honored (hebo/heit/hebi); table cells never merge;
  unchanged blocks left untouched (typography preserved, fixes duplicate
  hyperlinks); attempted/changed stats + route gate now cover PDF;
  CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings)

Features:
- formality param (formal/informal) + automatic regional-variant prompts
- output_mode=bilingual docx (source above translation)
- per-user translation memory on Redis (falls back to LRU), context-hashed
- QA report + 0-100 confidence score in job status; L0 on by default
- OpenAI-compatible providers: whole chunk in ONE numbered-JSON request
  (~15x fewer calls) with per-item fallback; base prompt always present
  (custom prompt no longer replaces translation instructions)

Infra & marketing alignment:
- plan-based engine gating + vision gating (closes paid-engine leak);
  /providers/available filtered per plan; 107 languages exposed
- zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash)
- admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin
  (TestClient breakage); Prometheus test fixture fixed
- marketing docs aligned with code (PDF+OCR, retention, engines, pricing)
- security: .env.ionos/.env.production/provider_settings.json removed

Tests: 1173 passed / 0 failed (6 network tests deselected: free Google
endpoint temporarily blocked from this machine)
This commit is contained in:
2026-08-29 18:38:09 +02:00
parent 992f13d53c
commit 526c87348f
87 changed files with 6996 additions and 1024 deletions

View File

@@ -564,60 +564,46 @@ class TestPptxChartWhitespace:
</c:chartSpace>
"""
def test_padded_chart_text_via_internal_method(self):
"""The internal chart apply logic should preserve whitespace."""
def test_chart_translation_reaches_output_file(self, tmp_path):
"""Chart translations must reach the OUTPUT FILE (real ChartPart).
python-pptx's ChartPart.blob is read-only — the previous in-memory
`blob = ...` write never landed in the saved .pptx. This end-to-end
test uses a real chart and verifies the chart XML inside the
output ZIP.
"""
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
provider = MockProvider({"Padded chart title": "Titre avec espaces"})
translator = PowerPointTranslator(provider=provider)
# Build a chart entry by hand (simulating collect time)
chart_xml = etree.fromstring(self.CHART_PADDED_XML.encode("utf-8"))
entries = []
for t_elem in chart_xml.iter(f"{{{_NS_A}}}t"):
text_raw = t_elem.text or ""
text = text_raw.strip()
if not text:
continue
entry = {
"element": t_elem,
"original": text,
"original_raw": text_raw,
"translated": "Titre avec espaces",
"tag": "a:t",
"element_path": translator._get_element_path(t_elem),
}
entries.append(entry)
if not hasattr(translator, "_chart_entries"):
translator._chart_entries = []
class _FakePart:
def __init__(self, blob):
self._blob = blob
@property
def blob(self):
return self._blob
@blob.setter
def blob(self, value):
self._blob = value
fake_part = _FakePart(
etree.tostring(
chart_xml, xml_declaration=True, encoding="UTF-8", standalone=True
)
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
chart_data = CategoryChartData()
chart_data.categories = ["A", "B"]
chart_data.add_series("Series 1", (1, 2))
graphic_frame = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED, 10, 10, 400, 300, chart_data
)
translator._chart_entries.append({
"chart_part": fake_part,
"entries": entries,
})
chart = graphic_frame.chart
chart.has_title = True
chart.chart_title.text_frame.text = "Padded chart title"
# Apply
translator._apply_chart_translations(Path("dummy"))
input_file = tmp_path / "chart_in.pptx"
output_file = tmp_path / "chart_out.pptx"
prs.save(str(input_file))
# Re-parse and check whitespace preserved
updated_xml = etree.fromstring(fake_part._blob)
all_t = list(updated_xml.iter(f"{{{_NS_A}}}t"))
# Find the title text
title_text = all_t[0].text or ""
assert " Titre avec espaces " in title_text, (
f"Chart whitespace not preserved: {title_text!r}"
translator.translate_file(input_file, output_file, "fr", "en")
with zipfile.ZipFile(output_file, "r") as zf:
chart_parts = [
n for n in zf.namelist() if n.startswith("ppt/charts/chart")
]
assert chart_parts, "chart part missing from output file"
chart_xml = zf.read(chart_parts[0]).decode("utf-8")
assert "Titre avec espaces" in chart_xml, (
"Chart title translation never reached the output file"
)
assert "Padded chart title" not in chart_xml

View File

@@ -0,0 +1,139 @@
"""PDF quality fixes: table-cell merge guard, bold/italic fonts, unchanged
blocks left untouched (no redaction/rewrite), and stats propagation."""
import fitz
import pytest
from translators.pdf_translator import PDFTranslator
class TestTableCellMergeGuard:
def test_table_cells_never_merge(self):
t = PDFTranslator(provider=None)
a = {
"bbox": (72, 100, 200, 115),
"font_size": 11,
"_is_table_cell": True,
}
b = {
"bbox": (72, 118, 200, 133), # same column, next row
"font_size": 11,
"_is_table_cell": True,
}
assert t._should_merge_blocks(a, b) is False
def test_table_cell_and_paragraph_do_not_merge(self):
t = PDFTranslator(provider=None)
a = {"bbox": (72, 100, 200, 115), "font_size": 11, "_is_table_cell": True}
b = {"bbox": (72, 118, 200, 133), "font_size": 11}
assert t._should_merge_blocks(a, b) is False
def test_regular_paragraphs_still_merge(self):
t = PDFTranslator(provider=None)
a = {"bbox": (72, 100, 300, 115), "font_size": 11}
b = {"bbox": (72, 118, 302, 133), "font_size": 11}
assert t._should_merge_blocks(a, b) is True
class TestBoldItalicFontSelection:
def _capturing_page(self):
page = fitz.open().new_page()
calls = []
original = page.insert_textbox
def capture(rect, text, fontname=None, fontfile=None, fontsize=None, **kw):
calls.append({"fontname": fontname, "fontfile": fontfile})
return 0
page.insert_textbox = capture
return page, calls
def _block(self, **kwargs):
block = {
"bbox": (72, 100, 400, 130),
"text": "Bold heading",
"translated": "Titre en gras",
"font_size": 20,
"color": 0,
"line_count": 1,
"sub_bboxes": [(72, 100, 400, 130)],
}
block.update(kwargs)
return block
def test_bold_block_uses_hebo(self):
page, calls = self._capturing_page()
t = PDFTranslator(provider=None)
t._font_path = None # force base-14 selection
t._write_translated_block(page, self._block(is_bold=True), None, False)
assert calls and calls[0]["fontname"] == "hebo"
def test_italic_block_uses_heit(self):
page, calls = self._capturing_page()
t = PDFTranslator(provider=None)
t._font_path = None
t._write_translated_block(page, self._block(is_italic=True), None, False)
assert calls and calls[0]["fontname"] == "heit"
def test_bold_italic_uses_hebi(self):
page, calls = self._capturing_page()
t = PDFTranslator(provider=None)
t._font_path = None
t._write_translated_block(
page, self._block(is_bold=True, is_italic=True), None, False
)
assert calls and calls[0]["fontname"] == "hebi"
def test_regular_block_keeps_helv(self):
page, calls = self._capturing_page()
t = PDFTranslator(provider=None)
t._font_path = None
t._write_translated_block(page, self._block(), None, False)
assert calls and calls[0]["fontname"] == "helv"
class _IdentityProvider:
"""Returns the input text unchanged (new-style provider)."""
def get_name(self):
return "identity"
def is_available(self):
return True
def translate_text(self, request):
from services.providers.schemas import TranslationResponse
return TranslationResponse(
translated_text=request.text,
provider_name="identity",
from_cache=False,
)
class TestUnchangedBlocksUntouched:
def test_identity_translation_stats_and_no_rewrite(self, tmp_path):
"""Provider returning the source text: stats stay changed=0 (so the
route gate detects it) and blocks are left un-redacted."""
doc = fitz.open()
page = doc.new_page()
page.insert_text((72, 100), "Already in English, nothing to do.", fontsize=11)
src = tmp_path / "en.pdf"
doc.save(str(src))
doc.close()
t = PDFTranslator(provider=_IdentityProvider())
out = tmp_path / "out.pdf"
result = t.translate_file(src, out, "en", "auto")
assert result.exists()
stats = t.get_translation_stats()
assert stats["attempted"] >= 1
assert stats["changed"] == 0
# The text is still there, byte-for-byte same rendering (block was
# not redacted + rewritten in the substitute font).
check = fitz.open(str(result))
text = check[0].get_text("text")
check.close()
assert "Already in English" in text

View File

@@ -190,20 +190,23 @@ class TestParagraphTranslation:
"""Tests for paragraph text translation (AC1)."""
def test_translate_paragraph_runs(self, tmp_path):
"""Test that paragraph runs are translated."""
"""Adjacent runs with identical formatting merge into ONE unit.
"Hello" + " " + "World" (same formatting, rsid-style splits) must be
translated as the whole sentence, not as separate fragments.
"""
mock_provider = MockTranslationProvider(
{
"Hello": "Bonjour",
"World": "Monde",
"Hello World": "Bonjour le monde",
}
)
translator = WordTranslator(provider=mock_provider)
doc = Document()
para = doc.add_paragraph()
run1 = para.add_run("Hello")
run2 = para.add_run(" ")
run3 = para.add_run("World")
para.add_run("Hello")
para.add_run(" ")
para.add_run("World")
input_file = tmp_path / "input.docx"
output_file = tmp_path / "output.docx"
@@ -214,8 +217,41 @@ class TestParagraphTranslation:
doc_out = Document(output_file)
text = doc_out.paragraphs[0].text
assert "Bonjour" in text
assert "Monde" in text
assert text == "Bonjour le monde"
# One merged unit → a single provider call
assert mock_provider._call_count == 1
def test_bold_span_kept_separate_and_coherent(self, tmp_path):
"""A formatting change mid-sentence splits the units; spaces survive."""
mock_provider = MockTranslationProvider(
{
"This is": "Ceci est",
"very important": "très important",
}
)
translator = WordTranslator(provider=mock_provider)
doc = Document()
para = doc.add_paragraph()
para.add_run("This is ")
bold = para.add_run("very important")
bold.bold = True
input_file = tmp_path / "input.docx"
output_file = tmp_path / "output.docx"
doc.save(input_file)
translator.translate_file(input_file, output_file, "fr")
doc_out = Document(output_file)
text = doc_out.paragraphs[0].text
assert text == "Ceci est très important"
# Bold formatting survives on the right span
runs = [r for r in doc_out.paragraphs[0].runs if r.text.strip()]
assert len(runs) == 2
assert runs[1].bold is True
assert runs[1].text == "très important"
def test_empty_paragraphs_not_translated(self, tmp_path):
"""Test that empty paragraphs are not translated."""