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

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