feat(review,teams): review foundation — segments, side-by-side editor, rebuild, XLIFF, team workspaces
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 2m14s

Foundations:
- TranslationSegment model + migration f7e8d9c0b1a2 (segments, workspaces,
  workspace_members, glossaries.workspace_id)
- SegmentRecorder injected into all 4 translators: unique (source,
  translation) pairs captured per job and persisted (best-effort)
- set_segment_overrides: human-reviewed translations applied verbatim on
  rebuild — top priority over TM and provider, zero API calls

Review API (routes/review_routes.py):
- GET /translations/{id}/segments (owner or job token)
- PATCH /segments/{id} edit/approve — feeds the per-user TM so approved
  translations are reused in later jobs
- POST /translations/{id}/rebuild — rebuild document with reviewed text
- GET/POST /translations/{id}/xliff — XLIFF 1.2 export/import (edited
  segments export their reviewed text)

Review editor (frontend /dashboard/reviews/[jobId]):
- side-by-side source/translation table, inline edit, approve (single or
  all), rebuild & download (auth blob), XLIFF export/import, 13 locales
- 'Relire et corriger' link on the translation-complete screen

Team workspaces (routes/workspace_routes.py + /dashboard/teams):
- Workspace/WorkspaceMember models, roles owner/admin/member
- create (Business plan), list with seat usage, invite by email with
  seat-limit enforcement (Business=5, Enterprise unlimited), removal
- shared glossaries: workspace members can use a glossary shared to their
  workspace (access check extended)

Tests: 1184 passed / 0 failed (11 new: recorder, overrides, docx
capture->rebuild e2e, XLIFF structure/escaping, seats, workspace CRUD,
shared glossary access)
This commit is contained in:
2026-08-29 19:04:32 +02:00
parent 526c87348f
commit b4e873ad2c
31 changed files with 2399 additions and 54 deletions

View File

@@ -172,6 +172,18 @@ class PDFTranslator:
if enabled is not None:
self._ocr_enabled = enabled
def set_segment_recorder(self, recorder) -> None:
"""Attach a segment recorder (review foundation)."""
self._segment_recorder = recorder
def set_segment_overrides(self, overrides) -> None:
"""Human-reviewed translations applied verbatim on rebuild."""
self._segment_overrides = overrides or {}
def get_recorded_segments(self):
recorder = getattr(self, "_segment_recorder", None)
return recorder.get_pairs() if recorder is not None else []
def _get_font_path(self) -> Optional[str]:
"""Resolve a Unicode-capable TTF/OTF font file."""
if self._font_path is not None:
@@ -1555,16 +1567,31 @@ class PDFTranslator:
Also feeds the job-level attempted/changed stats so the route can
detect a total provider failure (changed == 0) on PDFs too.
Reviewer overrides (approved/edited segments) short-circuit the
provider entirely — verbatim reviewed text, zero drift.
"""
overrides = getattr(self, "_segment_overrides", None)
if overrides:
override = overrides.get((text or "").strip())
if override and override.strip() and override.strip() != (text or "").strip():
recorder = getattr(self, "_segment_recorder", None)
if recorder is not None:
recorder.record_pair(text, override)
return override
if text and text.strip():
self._translation_stats["attempted"] += 1
if self._provider is not None:
try:
results = self._translate_with_provider([text], target_language, source_language)
if results and results[0].strip():
if results[0].strip() != text.strip():
result = results[0]
if result.strip() != text.strip():
self._translation_stats["changed"] += 1
return results[0]
recorder = getattr(self, "_segment_recorder", None)
if recorder is not None:
recorder.record_pair(text, result)
return result
except Exception as e:
logger.warning("provider_single_failed", error=str(e))
@@ -1573,6 +1600,9 @@ class PDFTranslator:
result = translation_service.translate_text(text, target_language, source_language)
if result and result.strip() and result.strip() != text.strip():
self._translation_stats["changed"] += 1
recorder = getattr(self, "_segment_recorder", None)
if recorder is not None:
recorder.record_pair(text, result)
return result
except Exception as e:
logger.warning("legacy_single_failed", error=str(e))