feat: mark glossary templates as multilingual — support 11 target languages
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m30s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user