feat: mark glossary templates as multilingual — support 11 target languages
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m30s

Templates enriched by enrich_glossary_templates.py already contain
translations for de, es, it, pt, nl, ru, ja, ko, zh, ar, fa (including
Persian). But they were labeled FR→EN, causing incorrect filtering and
warnings when translating to other languages.

Changes:
- index.json: set target_lang='multi' for all 8 templates
- GlossarySelector: treat target_language='multi' as compatible with
  any target language (no false warnings, auto-select works)
- GlossarySelector: display '🌐 MULTILINGUE' badge instead of EN flag
- glossary_routes: default target_language to 'multi' instead of 'en'
- Migration: detect existing multilingual glossaries in DB (5+ keys in
  translations JSON) and set their target_language to 'multi'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:32:27 +02:00
parent ad8ac089a4
commit c66252bed4
4 changed files with 100 additions and 40 deletions

View File

@@ -235,6 +235,13 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
const sourceFlag = useMemo(() => sourceLang === 'auto' ? '' : getFlag(sourceLang), [sourceLang, getFlag]);
const targetFlag = useMemo(() => getFlag(targetLang), [targetLang, getFlag]);
// A glossary is compatible with the target language if:
// - its target_language exactly matches, OR
// - it's a multilingual glossary (target_language === 'multi')
const isCompatible = useCallback((gTargetLang: string) => {
return gTargetLang === targetLang || gTargetLang === 'multi';
}, [targetLang]);
const filteredGlossaries = useMemo(() => {
if (!filterByLang || sourceLang === 'auto') {
return glossaries;
@@ -242,12 +249,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
return glossaries
.filter(g => g.source_language === sourceLang)
.sort((a, b) => {
// Glossaries matching source + target come first
const aMatch = a.target_language === targetLang ? 0 : 1;
const bMatch = b.target_language === targetLang ? 0 : 1;
return aMatch - bMatch;
// Compatible glossaries first, then incompatible
const aOk = isCompatible(a.target_language) ? 0 : 1;
const bOk = isCompatible(b.target_language) ? 0 : 1;
return aOk - bOk;
});
}, [glossaries, filterByLang, sourceLang, targetLang]);
}, [glossaries, filterByLang, sourceLang, isCompatible]);
const filteredTemplates = useMemo(() => {
if (!filterByLang || sourceLang === 'auto') {
@@ -285,12 +292,11 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
if (!nextVal) {
onChange(null);
} else if (!glossaryId) {
// Only auto-select if a glossary matches BOTH source and target language
const matching = filteredGlossaries.find(g => g.target_language === targetLang);
// Auto-select first compatible glossary (exact target match or multilingual)
const matching = filteredGlossaries.find(g => isCompatible(g.target_language));
if (matching) {
onChange(matching.id);
}
// If no glossary matches the target, leave unselected — user picks manually
}
}}
className={cn(
@@ -337,8 +343,8 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
</div>
)}
{/* Mismatch Warning — target language */}
{selected && selected.target_language && selected.target_language !== targetLang && (
{/* Mismatch Warning — target language (skip for multilingual glossaries) */}
{selected && selected.target_language !== 'multi' && selected.target_language && selected.target_language !== targetLang && (
<div className="flex items-start gap-1.5 p-2 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400 text-[10px] leading-normal font-medium animate-fade-in">
<span className="shrink-0">🎯</span>
<span>
@@ -365,7 +371,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
</span>
<span className="text-[10px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
{selected
? `${getFlag(selected.source_language)} ➜ ${getFlag(selected.target_language || targetLang)} • ${selected.terms_count} termes`
? `${getFlag(selected.source_language)} ➜ ${selected.target_language === 'multi' ? '🌐 MULTILINGUE' : getFlag(selected.target_language || targetLang)} • ${selected.terms_count} termes`
: (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? "Sélectionnez un glossaire" : "Aucun glossaire disponible")
}
</span>
@@ -427,7 +433,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-xs font-bold block leading-none truncate">{g.name}</span>
<span className="text-[10px] uppercase tracking-wider text-brand-dark/40 dark:text-white/45 font-bold block mt-1">
{flag} ➜ {getFlag(g.target_language || targetLang)} • {g.terms_count} termes
{flag} ➜ {g.target_language === 'multi' ? '🌐 MULTILINGUE' : getFlag(g.target_language || targetLang)} • {g.terms_count} termes
</span>
</div>
{isSelected && <Check size={12} className="text-brand-accent shrink-0" />}
@@ -451,7 +457,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
);
const isAlreadySelected = existingGlossary?.id === glossaryId;
const flag = getFlag(tmpl.source_lang);
const tFlag = getFlag(tmpl.target_lang);
const tFlag = tmpl.target_lang === 'multi' ? '🌐 MULTILINGUE' : getFlag(tmpl.target_lang);
return (
<button