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>

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>
)}