feat: prevent duplicate glossary presets + fix i18n source warning bug
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s

- 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
This commit is contained in:
2026-06-01 23:39:53 +02:00
parent ce53f0df16
commit 818eac5490
6 changed files with 61 additions and 8 deletions

View File

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

View File

@@ -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() {
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{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 (
<button
key={p.key}
onClick={() => handleCreatePresetGlossary(p)}
disabled={!!creatingPreset}
className="p-4 bg-brand-muted/40 dark:bg-white/5 hover:bg-brand-accent/5 dark:hover:bg-brand-accent/10 border border-black/5 dark:border-white/5 rounded-xl text-left transition-all hover:border-brand-accent/30 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed group min-h-[105px] flex flex-col justify-between"
onClick={() => !alreadyImported && handleCreatePresetGlossary(p)}
disabled={!!creatingPreset || alreadyImported}
className="relative p-4 bg-brand-muted/40 dark:bg-white/5 hover:bg-brand-accent/5 dark:hover:bg-brand-accent/10 border border-black/5 dark:border-white/5 rounded-xl text-left transition-all hover:border-brand-accent/30 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed group min-h-[105px] flex flex-col justify-between"
>
<div className="flex items-center justify-between mb-2">
<div className="p-1.5 bg-brand-accent/10 rounded-lg text-brand-accent group-hover:scale-110 transition-transform">
{isCreatingThis ? <Loader2 size={16} className="animate-spin" /> : <Icon size={16} />}
</div>
{isCreatingThis && <span className="text-[10px] text-brand-accent font-bold uppercase">{t('glossaries.presets.creating')}</span>}
{alreadyImported && (
<span className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20">
<CheckCircle2 size={9} /> {t('glossaries.presets.alreadyImported')}
</span>
)}
</div>
<div>
<h4 className="text-xs font-bold text-brand-dark dark:text-white mb-1">
@@ -443,7 +457,8 @@ export default function GlossariesPage() {
</div>
</button>
);
})}
});
})()}
</div>
</section>

View File

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

View File

@@ -338,7 +338,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<div className="flex items-start gap-1.5 p-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 text-[10px] leading-normal font-medium animate-fade-in">
<span className="shrink-0 text-amber-500"></span>
<span>
<strong>{t('translate.glossary.sourceWarning') || 'Attention :'} Ce glossaire utilise la langue source</strong> <strong>{getFlag(selected.source_language)} {selected.source_language.toUpperCase()}</strong>, {t('translate.glossary.sourceWarningBut') || 'mais votre document est configuré en'} <strong>{getFlag(sourceLang)} {sourceLang.toUpperCase()}</strong>.
<strong>{t('translate.glossary.sourceWarning')}</strong> <strong>{getFlag(selected.source_language)} {selected.source_language.toUpperCase()}</strong>, {t('translate.glossary.sourceWarningBut')} <strong>{getFlag(sourceLang)} {sourceLang.toUpperCase()}</strong>.
</span>
</div>
)}

View File

@@ -381,6 +381,7 @@ const messages: Record<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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",

View File

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