diff --git a/alembic/versions/e5b2c9d1f4a8_add_target_language_to_glossaries.py b/alembic/versions/e5b2c9d1f4a8_add_target_language_to_glossaries.py new file mode 100644 index 0000000..343ac0e --- /dev/null +++ b/alembic/versions/e5b2c9d1f4a8_add_target_language_to_glossaries.py @@ -0,0 +1,30 @@ +"""Add target_language to glossaries + +Revision ID: e5b2c9d1f4a8 +Revises: d4a1f8e2b3c7 +Create Date: 2026-05-31 + +Adds target_language column to glossaries table. +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers +revision = "e5b2c9d1f4a8" +down_revision = "d4a1f8e2b3c7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "glossaries", + sa.Column("target_language", sa.String(10), nullable=True, server_default="en"), + ) + + +def downgrade() -> None: + op.drop_column("glossaries", "target_language") diff --git a/database/models.py b/database/models.py index 2f94eee..efed78a 100644 --- a/database/models.py +++ b/database/models.py @@ -331,6 +331,7 @@ class Glossary(Base): ) name = Column(String(255), nullable=False) source_language = Column(String(10), nullable=False, default="fr") + target_language = Column(String(10), nullable=True, default="en") created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) @@ -348,6 +349,7 @@ class Glossary(Base): "user_id": self.user_id, "name": self.name, "source_language": self.source_language, + "target_language": self.target_language, "terms": [term.to_dict() for term in self.terms] if self.terms else [], "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, diff --git a/frontend/src/app/dashboard/glossaries/CreateGlossaryDialog.tsx b/frontend/src/app/dashboard/glossaries/CreateGlossaryDialog.tsx index f809938..29f0721 100644 --- a/frontend/src/app/dashboard/glossaries/CreateGlossaryDialog.tsx +++ b/frontend/src/app/dashboard/glossaries/CreateGlossaryDialog.tsx @@ -40,10 +40,12 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { SUPPORTED_LANGUAGES } from './types'; + interface CreateGlossaryDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onCreate: (data: { name: string; terms: GlossaryTermInput[] }) => Promise; + onCreate: (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => Promise; onImportTemplate: (templateId: string, name?: string) => Promise; isCreating: boolean; isImportingTemplate: boolean; @@ -289,6 +291,8 @@ export function CreateGlossaryDialog({ const [activeTab, setActiveTab] = useState<'templates' | 'file' | 'manual'>('templates'); const [name, setName] = useState(''); const [nameAutoFilled, setNameAutoFilled] = useState(false); + const [sourceLanguage, setSourceLanguage] = useState('fr'); + const [targetLanguage, setTargetLanguage] = useState('en'); const [terms, setTerms] = useState([{ source: '', target: '' }]); const [fileTerms, setFileTerms] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(null); @@ -300,6 +304,8 @@ export function CreateGlossaryDialog({ const reset = useCallback(() => { setName(''); setNameAutoFilled(false); + setSourceLanguage('fr'); + setTargetLanguage('en'); setTerms([{ source: '', target: '' }]); setFileTerms([]); setSelectedTemplate(null); @@ -340,9 +346,9 @@ export function CreateGlossaryDialog({ const termsToSave = activeTab === 'file' ? fileTerms : terms.filter(t => t.source.trim() && t.target.trim()); - await onCreate({ name: name.trim(), terms: termsToSave }); + await onCreate({ name: name.trim(), source_language: sourceLanguage, target_language: targetLanguage, terms: termsToSave }); reset(); - }, [activeTab, selectedTemplate, name, fileTerms, terms, onCreate, onImportTemplate, reset]); + }, [activeTab, selectedTemplate, name, fileTerms, terms, sourceLanguage, targetLanguage, onCreate, onImportTemplate, reset]); const canSubmit = (() => { if (!name.trim() || isProcessing) return false; @@ -381,15 +387,48 @@ export function CreateGlossaryDialog({ {t('glossaries.dialog.description')} -
- - { setName(e.target.value); setNameAutoFilled(false); }} - placeholder={t('glossaries.dialog.namePlaceholder')} - disabled={isProcessing} - /> +
+
+ + { setName(e.target.value); setNameAutoFilled(false); }} + placeholder={t('glossaries.dialog.namePlaceholder')} + disabled={isProcessing} + className="mt-1" + /> +
+ {/* Language pair selector */} +
+
+ + +
+
+
+ + +
+
{ + const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => { try { await createGlossary(data); setCreateDialogOpen(false); @@ -501,8 +501,12 @@ export default function GlossariesPage() {

{glossary.name}

-

- {SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.flag ?? '🌐'} {SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.label ?? glossary.source_language} +

+ {SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.flag ?? '🌐'} + {SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.label ?? glossary.source_language} + + {SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.flag ?? '🌐'} + {SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.label ?? glossary.target_language}

diff --git a/frontend/src/app/dashboard/glossaries/types.ts b/frontend/src/app/dashboard/glossaries/types.ts index 4813ffe..0a4b352 100644 --- a/frontend/src/app/dashboard/glossaries/types.ts +++ b/frontend/src/app/dashboard/glossaries/types.ts @@ -10,6 +10,7 @@ export interface Glossary { id: string; name: string; source_language: string; + target_language: string; terms: GlossaryTerm[]; created_at: string; updated_at: string; @@ -19,6 +20,7 @@ export interface GlossaryListItem { id: string; name: string; source_language: string; + target_language: string; terms_count: number; created_at: string; } @@ -61,12 +63,14 @@ export interface GlossaryTermInputWithId extends GlossaryTermInput { export interface GlossaryCreateInput { name: string; source_language?: string; + target_language?: string; terms?: GlossaryTermInput[]; } export interface GlossaryUpdateInput { name?: string; source_language?: string; + target_language?: string; terms?: GlossaryTermInput[]; } diff --git a/routes/glossary_routes.py b/routes/glossary_routes.py index 4865fd1..2c96b79 100644 --- a/routes/glossary_routes.py +++ b/routes/glossary_routes.py @@ -57,6 +57,7 @@ def _format_glossary(glossary: Glossary) -> dict: "id": glossary.id, "name": glossary.name, "source_language": glossary.source_language, + "target_language": getattr(glossary, "target_language", "en") or "en", "terms": [_format_term(t) for t in glossary.terms] if glossary.terms else [], "created_at": glossary.created_at.isoformat() if glossary.created_at else None, "updated_at": glossary.updated_at.isoformat() if glossary.updated_at else None, @@ -106,6 +107,7 @@ async def create_glossary( user_id=user.id, name=body.name, source_language=body.source_language, + target_language=body.target_language, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), ) @@ -185,6 +187,7 @@ async def list_glossaries( id=g.id, name=g.name, source_language=g.source_language or "fr", + target_language=getattr(g, "target_language", None) or "en", terms_count=len(g.terms) if g.terms else 0, created_at=g.created_at, ) @@ -339,6 +342,8 @@ async def update_glossary( if body.source_language is not None: glossary.source_language = body.source_language + if body.target_language is not None: + glossary.target_language = body.target_language if body.terms is not None: # Delete existing terms session.query(GlossaryTerm).filter( diff --git a/schemas/glossary_schemas.py b/schemas/glossary_schemas.py index 760c74d..8b071f6 100644 --- a/schemas/glossary_schemas.py +++ b/schemas/glossary_schemas.py @@ -46,6 +46,9 @@ class GlossaryCreate(BaseModel): source_language: str = Field( default="fr", max_length=10, description="Langue source (ISO code)" ) + target_language: str = Field( + default="en", max_length=10, description="Langue cible (ISO code)" + ) terms: list[GlossaryTermCreate] = Field( default_factory=list, description="Liste des termes" ) @@ -61,6 +64,7 @@ class GlossaryUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1, max_length=255) source_language: Optional[str] = Field(None, max_length=10) + target_language: Optional[str] = Field(None, max_length=10) terms: Optional[list[GlossaryTermCreate]] = Field(None) @field_validator("name") @@ -75,6 +79,7 @@ class GlossaryResponse(BaseModel): id: str name: str source_language: str = "fr" + target_language: str = "en" terms: list[GlossaryTermResponse] = [] created_at: Optional[datetime] = None updated_at: Optional[datetime] = None @@ -88,6 +93,7 @@ class GlossaryListItem(BaseModel): id: str name: str source_language: str = "fr" + target_language: str = "en" terms_count: int = Field( default=0, description="Nombre de termes dans le glossaire" )