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

@@ -107,6 +107,7 @@ class ExcelTranslator:
self._provider = provider
self.formula_pattern = re.compile(r"=.*")
self._custom_prompt: Optional[str] = None
self._tm_scope = None # set via set_tm_scope (per-user translation memory)
self._translation_stats = {"attempted": 0, "changed": 0}
def set_provider(self, provider: TranslationProvider) -> None:
@@ -116,6 +117,14 @@ class ExcelTranslator:
def set_custom_prompt(self, prompt: Optional[str]) -> None:
"""Set custom system prompt for LLM providers."""
self._custom_prompt = prompt
def set_tm_scope(self, user_id, prompt=None) -> None:
"""Enable the per-user translation memory for this job."""
from services.translation_tm import TMScope
self._tm_scope = TMScope.from_prompt(
user_id, prompt or getattr(self, "_custom_prompt", None)
)
def translate_file(
self,
@@ -318,6 +327,13 @@ class ExcelTranslator:
new_name=new_name,
)
# openpyxl does NOT rewrite references on rename: cell
# formulas, defined names and chart refs pointing at the old
# sheet would break (#REF!/#NAME?). Charts are handled later
# via ZIP re-injection; fix cells + defined names here.
if sheet_name_mapping:
self._rewrite_sheet_refs_in_workbook(workbook, sheet_name_mapping)
if translate_images:
_log_info("excel_image_translation_start", sheets=len(workbook.sheetnames))
for sheet_name in workbook.sheetnames:
@@ -427,12 +443,30 @@ class ExcelTranslator:
non_empty = [t for t in texts if t and t.strip()]
self._translation_stats["attempted"] += len(non_empty)
from services.translation_tm import translate_with_tm
provider_name = (
self._provider.get_name() if hasattr(self._provider, "get_name")
else type(self._provider).__name__
) if self._provider is not None else "legacy"
if self._provider is not None:
translated = self._translate_with_provider(
texts, target_language, source_language
)
def _do_translate(miss_texts):
return self._translate_with_provider(
miss_texts, target_language, source_language
)
else:
translated = self._translate_with_legacy(texts, target_language, source_language)
def _do_translate(miss_texts):
return self._translate_with_legacy(
miss_texts, target_language, source_language
)
# Translation memory: reuse this user's previous translations
# (identical context/prompt) before hitting the provider.
translated = translate_with_tm(
texts, target_language, source_language,
provider_name, getattr(self, "_tm_scope", None), _do_translate,
)
changed = sum(1 for orig, trans in zip(texts, translated) if orig != trans and trans.strip())
self._translation_stats["changed"] += changed
@@ -931,6 +965,140 @@ class ExcelTranslator:
rewritten += 1
return rewritten
@staticmethod
def _rewrite_sheet_refs_in_formula(
formula: str, sheet_name_mapping: Dict[str, str]
) -> str:
"""Rewrite every sheet reference inside a formula string.
Handles quoted ('Ventes 2026'!), unquoted (Ventes!) and 3D refs
(Sheet1!A1:Sheet2!B2 — each end is rewritten independently).
Longest names are replaced first so a name that is a prefix of
another is not corrupted.
"""
if not formula or not sheet_name_mapping or "!" not in formula:
return formula
result = formula
# longest first to avoid partial-name collisions
for old in sorted(sheet_name_mapping, key=len, reverse=True):
new = sheet_name_mapping[old]
if new == old:
continue
new_quoted = ExcelTranslator._quote_sheet_name_for_ref(new)
# Quoted form: inner apostrophes are escaped as ''
escaped = old.replace("'", "''")
result = re.sub(
rf"'{re.escape(escaped)}'!",
new_quoted + "!",
result,
)
# Unquoted form: only when the old name needs no quotes; guard
# against matching the tail of a longer name.
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_.]*", old):
result = re.sub(
rf"(?<![A-Za-z0-9_.']){re.escape(old)}!",
new_quoted + "!",
result,
)
return result
@classmethod
def _rewrite_sheet_refs_in_workbook(
cls, workbook, sheet_name_mapping: Dict[str, str]
) -> None:
"""After a sheet rename, fix every reference that still points at the
old names: cell formulas, defined names, data validations and
conditional-formatting rules. openpyxl does none of this on rename.
Best-effort: individual failures are logged and skipped — a broken
rename must never lose the whole file.
"""
if not sheet_name_mapping:
return
cells_fixed = names_fixed = validations_fixed = cf_fixed = 0
try:
for worksheet in workbook.worksheets:
# 1) Cell formulas
for row in worksheet.iter_rows():
for cell in row:
value = cell.value
if isinstance(value, str) and value.startswith("=") and "!" in value:
updated = cls._rewrite_sheet_refs_in_formula(
value, sheet_name_mapping
)
if updated != value:
cell.value = updated
cells_fixed += 1
# 2) Data validations (list sources / custom formulas)
try:
for dv in worksheet.data_validations.dataValidation:
for attr in ("formula1", "formula2"):
fx = getattr(dv, attr, None)
if isinstance(fx, str) and "!" in fx:
updated = cls._rewrite_sheet_refs_in_formula(
fx, sheet_name_mapping
)
if updated != fx:
setattr(dv, attr, updated)
validations_fixed += 1
except Exception as e:
_log_warning(
"excel_sheet_refs_validations_failed",
sheet=worksheet.title,
error=str(e),
)
# 3) Conditional formatting rules
try:
for cf in worksheet.conditional_formatting:
for rule in cf.rules:
formulas = getattr(rule, "formula", None) or []
for idx, fx in enumerate(formulas):
if isinstance(fx, str) and "!" in fx:
updated = cls._rewrite_sheet_refs_in_formula(
fx, sheet_name_mapping
)
if updated != fx:
formulas[idx] = updated
cf_fixed += 1
except Exception as e:
_log_warning(
"excel_sheet_refs_condfmt_failed",
sheet=worksheet.title,
error=str(e),
)
# 4) Workbook-level defined names
try:
for name in list(workbook.defined_names.values()):
attr_text = getattr(name, "attr_text", None)
if isinstance(attr_text, str) and "!" in attr_text:
updated = cls._rewrite_sheet_refs_in_formula(
attr_text, sheet_name_mapping
)
if updated != attr_text:
name.attr_text = updated
names_fixed += 1
except Exception as e:
_log_warning("excel_sheet_refs_names_failed", error=str(e))
except Exception as e:
_log_error("excel_sheet_refs_rewrite_failed", error=str(e))
return
if cells_fixed or names_fixed or validations_fixed or cf_fixed:
_log_info(
"excel_sheet_refs_rewritten",
cells=cells_fixed,
defined_names=names_fixed,
validations=validations_fixed,
conditional_formats=cf_fixed,
)
def _translate_images(self, worksheet: Worksheet, target_language: str) -> None:
"""
Translate text in images using vision model.