Files
office_translator/translators/segments.py
sepehr b4e873ad2c
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 2m14s
feat(review,teams): review foundation — segments, side-by-side editor, rebuild, XLIFF, team workspaces
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)
2026-08-29 19:04:32 +02:00

75 lines
2.4 KiB
Python

"""
Segment recording & overrides — the review foundation.
During a translation job, every (source → translation) pair is recorded so
it can be persisted per job (translation_segments table), reviewed and
edited side-by-side, and re-applied when rebuilding the document.
Overrides are the inverse path: human-reviewed translations (approved or
edited segments) applied verbatim on rebuild — no provider call, no drift.
"""
from typing import Dict, List, Optional, Tuple
from core.logging import get_logger
logger = get_logger(__name__)
class SegmentRecorder:
"""Collects unique (source, translation) pairs in document order."""
def __init__(self):
self._pairs: List[Tuple[str, str]] = []
self._seen: set = set()
def record_pair(self, source: str, translation: str) -> None:
if not source or not source.strip():
return
stripped = source.strip()
# Identity pairs carry no review value ( untranslated or unchanged
# text) — skip them so the review list stays actionable.
if not translation or not translation.strip() or translation.strip() == stripped:
return
if stripped in self._seen:
return
self._seen.add(stripped)
self._pairs.append((stripped, translation.strip()))
def record_pairs(self, pairs) -> None:
for source, translation in pairs:
self.record_pair(source, translation)
def get_pairs(self) -> List[Tuple[str, str]]:
return list(self._pairs)
def __len__(self) -> int:
return len(self._pairs)
def apply_overrides(
texts: List[str],
overrides: Optional[Dict[str, str]],
) -> Tuple[Dict[int, str], List[int]]:
"""Split a batch: ({index: override_translation}, [indices to translate]).
Overrides are keyed by the stripped source text (that is how segments
are persisted). Identity overrides are treated as "no override" so the
normal pipeline (TM → provider) stays in charge.
"""
if not overrides:
return {}, list(range(len(texts)))
hits: Dict[int, str] = {}
misses: List[int] = []
for i, text in enumerate(texts):
if not text or not text.strip():
misses.append(i)
continue
override = overrides.get(text.strip())
if override is not None and override.strip() and override.strip() != text.strip():
hits[i] = override.strip()
else:
misses.append(i)
return hits, misses