From 818eac5490b589260e4cd87a86cfc103be030dc8 Mon Sep 17 00:00:00 2001 From: sepehr Date: Mon, 1 Jun 2026 23:39:53 +0200 Subject: [PATCH] feat: prevent duplicate glossary presets + fix i18n source warning bug - Add template_id column to Glossary model (nullable, indexed) - Backend: return 409 Conflict if user already imported a template - Frontend: disable preset cards already imported, show 'Imported' badge - Fix duplicated text in GlossarySelector source warning (hardcoded FR text removed) - Complete i18n migration for glossaries page and GlossarySelector - Add glossaries.presets.alreadyImported key in all 13 locales --- database/models.py | 7 +++++- .../src/app/dashboard/glossaries/page.tsx | 25 +++++++++++++++---- .../src/app/dashboard/glossaries/types.ts | 2 ++ .../dashboard/translate/GlossarySelector.tsx | 2 +- frontend/src/lib/i18n.tsx | 13 ++++++++++ routes/glossary_routes.py | 20 ++++++++++++++- 6 files changed, 61 insertions(+), 8 deletions(-) diff --git a/database/models.py b/database/models.py index c8bae67..3897c3f 100644 --- a/database/models.py +++ b/database/models.py @@ -334,6 +334,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") + template_id = Column(String(50), nullable=True, index=True) created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) @@ -343,7 +344,10 @@ class Glossary(Base): ) # Indexes - __table_args__ = (Index("ix_glossaries_user_id", "user_id"),) + __table_args__ = ( + Index("ix_glossaries_user_id", "user_id"), + Index("ix_glossaries_template_id", "template_id"), + ) def to_dict(self) -> dict: return { @@ -352,6 +356,7 @@ class Glossary(Base): "name": self.name, "source_language": self.source_language, "target_language": self.target_language, + "template_id": self.template_id, "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/page.tsx b/frontend/src/app/dashboard/glossaries/page.tsx index 4700082..b74b109 100644 --- a/frontend/src/app/dashboard/glossaries/page.tsx +++ b/frontend/src/app/dashboard/glossaries/page.tsx @@ -124,6 +124,8 @@ export default function GlossariesPage() { count: String(glossary?.terms?.length ?? 0), }), }); + } else if (res.status === 409) { + toast({ title: t('glossaries.presets.alreadyImported') }); } else { toast({ variant: 'destructive', title: t('glossaries.toast.error'), description: t('glossaries.toast.errorCreate') }); } @@ -417,21 +419,33 @@ export default function GlossariesPage() {
- {PRESETS.map((p) => { + {(() => { + const importedTemplateIds = new Set( + glossaries + .map((g: GlossaryListItem) => g.template_id) + .filter(Boolean) as string[] + ); + return PRESETS.map((p) => { const Icon = p.icon; const isCreatingThis = creatingPreset === p.key; + const alreadyImported = importedTemplateIds.has(p.templateId); return ( ); - })} + }); + })()}
diff --git a/frontend/src/app/dashboard/glossaries/types.ts b/frontend/src/app/dashboard/glossaries/types.ts index 2730236..68a1af1 100644 --- a/frontend/src/app/dashboard/glossaries/types.ts +++ b/frontend/src/app/dashboard/glossaries/types.ts @@ -11,6 +11,7 @@ export interface Glossary { name: string; source_language: string; target_language: string; + template_id?: string | null; terms: GlossaryTerm[]; created_at: string; updated_at: string; @@ -21,6 +22,7 @@ export interface GlossaryListItem { name: string; source_language: string; target_language: string; + template_id?: string | null; terms_count: number; created_at: string; } diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index 45d9c26..ab7e707 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -338,7 +338,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
⚠️ - {t('translate.glossary.sourceWarning') || 'Attention :'} Ce glossaire utilise la langue source {getFlag(selected.source_language)} {selected.source_language.toUpperCase()}, {t('translate.glossary.sourceWarningBut') || 'mais votre document est configuré en'} {getFlag(sourceLang)} {sourceLang.toUpperCase()}. + {t('translate.glossary.sourceWarning')} {getFlag(selected.source_language)} {selected.source_language.toUpperCase()}, {t('translate.glossary.sourceWarningBut')} {getFlag(sourceLang)} {sourceLang.toUpperCase()}.
)} diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 657f21b..240aac1 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -381,6 +381,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Clicking a card creates a pre-filled glossary with domain-specific terms. This glossary will appear in your glossaries below, and you can manually select it on the Translate page to force precise term translations.", "glossaries.presets.clickHint": "Click a card → glossary created → select it in Translate", "glossaries.presets.creating": "Creating...", + "glossaries.presets.alreadyImported": "Imported", "glossaries.presets.it.title": "IT / Software", "glossaries.presets.it.desc": "Development, infrastructure, DevOps", "glossaries.presets.legal.title": "Legal / Contracts", @@ -1324,6 +1325,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -2253,6 +2255,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -3137,6 +3140,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -4021,6 +4025,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -4905,6 +4910,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -5789,6 +5795,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -6673,6 +6680,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -7559,6 +7567,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -8442,6 +8451,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -9325,6 +9335,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -10166,6 +10177,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", @@ -11016,6 +11028,7 @@ const messages: Record> = { "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", "glossaries.presets.creating": "Création…", + "glossaries.presets.alreadyImported": "Déjà importé", "glossaries.presets.it.title": "IT / Logiciel", "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", "glossaries.presets.legal.title": "Juridique / Contrats", diff --git a/routes/glossary_routes.py b/routes/glossary_routes.py index ce0642a..cac8520 100644 --- a/routes/glossary_routes.py +++ b/routes/glossary_routes.py @@ -58,6 +58,7 @@ def _format_glossary(glossary: Glossary) -> dict: "name": glossary.name, "source_language": glossary.source_language, "target_language": getattr(glossary, "target_language", "multi") or "multi", + "template_id": getattr(glossary, "template_id", None), "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, @@ -591,13 +592,30 @@ async def import_glossary_template( ) glossary_name = name or template_data.get("name", template_info.get("name", template_id)) - + with get_sync_session() as session: + # Check if user already imported this template + existing = ( + session.query(Glossary) + .filter_by(user_id=user.id, template_id=template_id) + .first() + ) + if existing: + return JSONResponse( + status_code=409, + content={ + "error": "TEMPLATE_ALREADY_IMPORTED", + "message": f"You already have a glossary from the '{template_id}' template.", + "data": _format_glossary(existing), + }, + ) + glossary = Glossary( user_id=user.id, name=glossary_name, source_language=template_info.get("source_lang", template_data.get("source_lang", "fr")), target_language=template_info.get("target_lang", template_data.get("target_lang", "multi")), + template_id=template_id, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), )