feat(glossaries): add target_language support - DB migration, API, and UI language pair display
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m52s

This commit is contained in:
2026-05-31 13:25:12 +02:00
parent fecb3f7abb
commit 85cd5456f8
7 changed files with 105 additions and 15 deletions

View File

@@ -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")

View File

@@ -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,

View File

@@ -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<void>;
onCreate: (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => Promise<void>;
onImportTemplate: (templateId: string, name?: string) => Promise<void>;
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<GlossaryTermInput[]>([{ source: '', target: '' }]);
const [fileTerms, setFileTerms] = useState<GlossaryTermInput[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<GlossaryTemplate | null>(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({
<DialogDescription>{t('glossaries.dialog.description')}</DialogDescription>
</DialogHeader>
<div className="shrink-0 space-y-2 pt-2">
<Label htmlFor="glossary-name">{t('glossaries.dialog.nameLabel')}</Label>
<Input
id="glossary-name"
value={name}
onChange={(e) => { setName(e.target.value); setNameAutoFilled(false); }}
placeholder={t('glossaries.dialog.namePlaceholder')}
disabled={isProcessing}
/>
<div className="shrink-0 space-y-3 pt-2">
<div>
<Label htmlFor="glossary-name">{t('glossaries.dialog.nameLabel')}</Label>
<Input
id="glossary-name"
value={name}
onChange={(e) => { setName(e.target.value); setNameAutoFilled(false); }}
placeholder={t('glossaries.dialog.namePlaceholder')}
disabled={isProcessing}
className="mt-1"
/>
</div>
{/* Language pair selector */}
<div className="flex items-center gap-2">
<div className="flex-1">
<Label className="text-xs text-muted-foreground mb-1 block">Langue source</Label>
<select
value={sourceLanguage}
onChange={e => setSourceLanguage(e.target.value)}
disabled={isProcessing}
className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
{SUPPORTED_LANGUAGES.map(l => (
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
))}
</select>
</div>
<div className="pt-5 text-muted-foreground font-bold"></div>
<div className="flex-1">
<Label className="text-xs text-muted-foreground mb-1 block">Langue cible</Label>
<select
value={targetLanguage}
onChange={e => setTargetLanguage(e.target.value)}
disabled={isProcessing}
className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
{SUPPORTED_LANGUAGES.map(l => (
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
))}
</select>
</div>
</div>
</div>
<Tabs

View File

@@ -142,7 +142,7 @@ export default function GlossariesPage() {
setDeleteDialogOpen(true);
};
const handleCreateGlossary = async (data: { name: string; terms: GlossaryTermInput[] }) => {
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() {
<h3 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight mb-1 truncate">
{glossary.name}
</h3>
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-medium">
{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.flag ?? '🌐'} {SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.label ?? glossary.source_language}
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-medium flex items-center gap-1.5">
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.flag ?? '🌐'}</span>
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.label ?? glossary.source_language}</span>
<span className="text-brand-accent font-bold"></span>
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.flag ?? '🌐'}</span>
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.label ?? glossary.target_language}</span>
</p>
<div className="flex justify-between items-center pt-4 mt-5 border-t border-black/5 dark:border-white/10 text-xs text-brand-dark/40 dark:text-white/40">
<span className="flex items-center gap-1">

View File

@@ -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[];
}

View File

@@ -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(

View File

@@ -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"
)