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

@@ -1487,6 +1487,10 @@ async def _run_translation_job(
job_translator.set_custom_prompt(full_prompt)
if hasattr(job_translator, "set_tm_scope"):
job_translator.set_tm_scope(user_id, full_prompt)
if hasattr(job_translator, "set_segment_recorder"):
from translators.segments import SegmentRecorder
job_translator.set_segment_recorder(SegmentRecorder())
await asyncio.to_thread(
job_translator.translate_file,
input_path,
@@ -1502,6 +1506,10 @@ async def _run_translation_job(
job_translator.set_custom_prompt(full_prompt)
if hasattr(job_translator, "set_tm_scope"):
job_translator.set_tm_scope(user_id, full_prompt)
if hasattr(job_translator, "set_segment_recorder"):
from translators.segments import SegmentRecorder
job_translator.set_segment_recorder(SegmentRecorder())
await asyncio.to_thread(
job_translator.translate_file,
input_path,
@@ -1517,6 +1525,10 @@ async def _run_translation_job(
job_translator.set_custom_prompt(full_prompt)
if hasattr(job_translator, "set_tm_scope"):
job_translator.set_tm_scope(user_id, full_prompt)
if hasattr(job_translator, "set_segment_recorder"):
from translators.segments import SegmentRecorder
job_translator.set_segment_recorder(SegmentRecorder())
await asyncio.to_thread(
job_translator.translate_file,
input_path,
@@ -1533,6 +1545,10 @@ async def _run_translation_job(
job_translator.set_custom_prompt(full_prompt)
if hasattr(job_translator, "set_tm_scope"):
job_translator.set_tm_scope(user_id, full_prompt)
if hasattr(job_translator, "set_segment_recorder"):
from translators.segments import SegmentRecorder
job_translator.set_segment_recorder(SegmentRecorder())
# OCR (PDF scannés) : réglages admin > variables d'env.
mistral_cfg = getattr(_admin_cfg, "mistral", None)
mistral_key = _cfg(
@@ -1761,6 +1777,48 @@ async def _run_translation_job(
f"Job {job_id}: quality L2 layer failed: {l2_err}"
)
# ------------------------------------------------------------------
# Persist segments (review foundation): (source, translation) pairs
# stored per job so reviewers can edit/approve, rebuild and export
# XLIFF. Best-effort — a persistence failure never breaks the job.
# ------------------------------------------------------------------
if user_id and hasattr(job_translator, "get_recorded_segments"):
try:
recorded = job_translator.get_recorded_segments()
if recorded:
from database.connection import get_sync_session
from database.models import TranslationSegment
from translators.segments import SegmentRecorder # noqa: F401
def _persist_segments():
from database.models import TranslationSegment as Seg
rows = [
Seg(
job_id=job_id,
user_id=str(user_id),
segment_index=i,
source_text=src_text,
translated_text=tr_text,
status="pending",
)
for i, (src_text, tr_text) in enumerate(recorded)
if tr_text # identities are not reviewable segments
]
if not rows:
return 0
with get_sync_session() as session:
session.add_all(rows)
session.commit()
return len(rows)
stored = await asyncio.to_thread(_persist_segments)
logger.info(
f"Job {job_id}: persisted {stored} review segments"
)
except Exception as seg_err:
logger.warning(f"Job {job_id}: segment persistence failed: {seg_err}")
# ------------------------------------------------------------------
# QA report (pure heuristics — numbers fidelity, untranslated
# ratio, 0-100 score). Never blocks the job; surfaced in the job