feat: prevent duplicate glossary presets + fix i18n source warning bug
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s
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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user