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:
@@ -0,0 +1,105 @@
|
||||
"""Segments, workspaces and shared glossaries
|
||||
|
||||
Revision ID: a1b2c3d4e5f6
|
||||
Revises: e1f2a3b4c5d6
|
||||
Create Date: 2026-08-29
|
||||
|
||||
New product foundations:
|
||||
- translation_segments: per-job (source, translation) pairs powering the
|
||||
side-by-side review editor, rebuild and XLIFF export/import
|
||||
- workspaces + workspace_members: team workspaces with seat-based roles
|
||||
- glossaries.workspace_id: shared glossaries inside a workspace
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "f7e8d9c0b1a2"
|
||||
down_revision: Union[str, None] = "e1f2a3b4c5d6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"translation_segments",
|
||||
sa.Column("id", sa.String(36), primary_key=True),
|
||||
sa.Column("job_id", sa.String(64), nullable=False),
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sa.String(36),
|
||||
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("segment_index", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("source_text", sa.Text(), nullable=False),
|
||||
sa.Column("translated_text", sa.Text(), nullable=False, server_default=""),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
|
||||
sa.Column("reviewed_text", sa.Text(), nullable=True),
|
||||
sa.Column("is_heading", sa.Boolean(), nullable=False, server_default=sa.text("0")),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.create_index("ix_segments_job_index", "translation_segments", ["job_id", "segment_index"])
|
||||
op.create_index("ix_segments_user", "translation_segments", ["user_id"])
|
||||
op.create_index("ix_segments_status", "translation_segments", ["job_id", "status"])
|
||||
|
||||
op.create_table(
|
||||
"workspaces",
|
||||
sa.Column("id", sa.String(36), primary_key=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column(
|
||||
"owner_id",
|
||||
sa.String(36),
|
||||
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.create_index("ix_workspaces_owner", "workspaces", ["owner_id"])
|
||||
|
||||
op.create_table(
|
||||
"workspace_members",
|
||||
sa.Column("id", sa.String(36), primary_key=True),
|
||||
sa.Column(
|
||||
"workspace_id",
|
||||
sa.String(36),
|
||||
sa.ForeignKey("workspaces.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sa.String(36),
|
||||
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("role", sa.String(20), nullable=False, server_default="member"),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.create_index("ix_workspace_members_workspace", "workspace_members", ["workspace_id"])
|
||||
op.create_index("ix_workspace_members_user", "workspace_members", ["user_id"])
|
||||
|
||||
with op.batch_alter_table("glossaries") as batch_op:
|
||||
batch_op.add_column(sa.Column("workspace_id", sa.String(36), nullable=True))
|
||||
op.create_index("ix_glossaries_workspace", "glossaries", ["workspace_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("glossaries") as batch_op:
|
||||
batch_op.drop_index("ix_glossaries_workspace")
|
||||
batch_op.drop_column("workspace_id")
|
||||
|
||||
op.drop_index("ix_workspace_members_user", table_name="workspace_members")
|
||||
op.drop_index("ix_workspace_members_workspace", table_name="workspace_members")
|
||||
op.drop_table("workspace_members")
|
||||
|
||||
op.drop_index("ix_workspaces_owner", table_name="workspaces")
|
||||
op.drop_table("workspaces")
|
||||
|
||||
op.drop_index("ix_segments_status", table_name="translation_segments")
|
||||
op.drop_index("ix_segments_user", table_name="translation_segments")
|
||||
op.drop_index("ix_segments_job_index", table_name="translation_segments")
|
||||
op.drop_table("translation_segments")
|
||||
Reference in New Issue
Block a user