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
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:
@@ -331,6 +331,8 @@ class Glossary(Base):
|
||||
user_id = Column(
|
||||
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
# Optional: workspace this glossary is shared with (owner still administers it)
|
||||
workspace_id = Column(String(36), nullable=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
source_language = Column(String(10), nullable=False, default="fr")
|
||||
target_language = Column(String(10), nullable=True, default="en")
|
||||
@@ -347,12 +349,14 @@ class Glossary(Base):
|
||||
__table_args__ = (
|
||||
Index("ix_glossaries_user_id", "user_id"),
|
||||
Index("ix_glossaries_template_id", "template_id"),
|
||||
Index("ix_glossaries_workspace", "workspace_id"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"workspace_id": self.workspace_id,
|
||||
"name": self.name,
|
||||
"source_language": self.source_language,
|
||||
"target_language": self.target_language,
|
||||
@@ -423,3 +427,116 @@ class CustomPrompt(Base):
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class TranslationSegment(Base):
|
||||
"""One translatable unit of a finished job (review foundation).
|
||||
|
||||
Segments persist (source, translation) pairs per job so reviewers can
|
||||
edit/approve them side-by-side, rebuild the document with the reviewed
|
||||
text, and export/import XLIFF.
|
||||
"""
|
||||
|
||||
__tablename__ = "translation_segments"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=generate_uuid)
|
||||
job_id = Column(String(64), nullable=False)
|
||||
user_id = Column(
|
||||
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
segment_index = Column(Integer, nullable=False, default=0)
|
||||
source_text = Column(Text, nullable=False)
|
||||
translated_text = Column(Text, nullable=False, default="")
|
||||
# pending | approved | edited
|
||||
status = Column(String(20), nullable=False, default="pending")
|
||||
# Reviewer's edit (used for rebuild when status == "edited")
|
||||
reviewed_text = Column(Text, nullable=True)
|
||||
is_heading = Column(Boolean, nullable=False, default=False)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_segments_job_index", "job_id", "segment_index"),
|
||||
Index("ix_segments_user", "user_id"),
|
||||
Index("ix_segments_status", "job_id", "status"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"job_id": self.job_id,
|
||||
"segment_index": self.segment_index,
|
||||
"source_text": self.source_text,
|
||||
"translated_text": self.translated_text,
|
||||
"status": self.status,
|
||||
"reviewed_text": self.reviewed_text,
|
||||
"is_heading": self.is_heading,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class Workspace(Base):
|
||||
"""Team workspace: shared glossaries + seat-based membership.
|
||||
|
||||
Seats are enforced from the owner's plan (Business = 5 seats).
|
||||
"""
|
||||
|
||||
__tablename__ = "workspaces"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=generate_uuid)
|
||||
name = Column(String(255), nullable=False)
|
||||
owner_id = Column(
|
||||
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
|
||||
members = relationship(
|
||||
"WorkspaceMember", back_populates="workspace", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
__table_args__ = (Index("ix_workspaces_owner", "owner_id"),)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"owner_id": self.owner_id,
|
||||
"member_count": len(self.members) if self.members else 0,
|
||||
"members": [m.to_dict() for m in self.members] if self.members else [],
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class WorkspaceMember(Base):
|
||||
"""Membership row linking a user to a workspace with a role."""
|
||||
|
||||
__tablename__ = "workspace_members"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=generate_uuid)
|
||||
workspace_id = Column(
|
||||
String(36), ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
user_id = Column(
|
||||
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
# owner | admin | member
|
||||
role = Column(String(20), nullable=False, default="member")
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
|
||||
workspace = relationship("Workspace", back_populates="members")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_workspace_members_workspace", "workspace_id"),
|
||||
Index("ix_workspace_members_user", "user_id"),
|
||||
# UniqueConstraint imported lazily below if needed; enforced in app layer
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"workspace_id": self.workspace_id,
|
||||
"user_id": self.user_id,
|
||||
"role": self.role,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user