diff --git a/alembic/versions/a1b2c3d4e5f6_set_multilingual_target_language.py b/alembic/versions/a1b2c3d4e5f6_set_multilingual_target_language.py new file mode 100644 index 0000000..bd861d8 --- /dev/null +++ b/alembic/versions/a1b2c3d4e5f6_set_multilingual_target_language.py @@ -0,0 +1,54 @@ +"""Set multilingual glossaries target_language to 'multi' + +Revision ID: a1b2c3d4e5f6 +Revises: e5b2c9d1f4a8 +Create Date: 2026-05-31 + +Glossary templates that were enriched with multilingual translations +(via enrich_glossary_templates.py) contain translations for 11 languages +(de, es, it, pt, nl, ru, ja, ko, zh, ar, fa) in each term's translations +field. These should be marked as target_language='multi' instead of 'en'. + +This migration detects glossaries whose terms have multilingual translations +and sets their target_language to 'multi'. +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers +revision = "a1b2c3d4e5f6" +down_revision = "e5b2c9d1f4a8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Glossaries with terms containing 5+ translation keys are multilingual templates + # (enriched glossaries have 11 translations: de, es, it, pt, nl, ru, ja, ko, zh, ar, fa) + op.execute(""" + UPDATE glossaries + SET target_language = 'multi' + WHERE id IN ( + SELECT DISTINCT g.id + FROM glossaries g + JOIN glossary_terms gt ON gt.glossary_id = g.id + WHERE gt.translations IS NOT NULL + AND jsonb_typeof(gt.translations) = 'object' + AND ( + SELECT count(*) + FROM jsonb_object_keys(gt.translations) + ) >= 5 + ) + """) + + +def downgrade() -> None: + # Revert multilingual glossaries back to 'en' + op.execute(""" + UPDATE glossaries + SET target_language = 'en' + WHERE target_language = 'multi' + """) diff --git a/data/glossaries/index.json b/data/glossaries/index.json index 1804138..c99d8a2 100644 --- a/data/glossaries/index.json +++ b/data/glossaries/index.json @@ -1,68 +1,68 @@ { - "version": "1.0.0", - "description": "Index des glossaires pré-définis pour la traduction. Ces modèles sont utilisés comme templates par les utilisateurs Pro.", + "version": "1.1.0", + "description": "Index des glossaires pré-définis pour la traduction. Ces modèles sont utilisés comme templates par les utilisateurs Pro. Tous les templates contiennent des traductions multilingues (de, es, it, pt, nl, ru, ja, ko, zh, ar, fa).", "categories": { "legal": { - "name": "Juridique - Français → Anglais", - "description": "Terminologie juridique française vers anglaise pour contrats, statuts et documents légaux", + "name": "Juridique - Français → Multilingue", + "description": "Terminologie juridique française traduite en 12 langues pour contrats, statuts et documents légaux", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "legal_fr_en.json", "terms_count": 105 }, "technology": { - "name": "Technologie / IT - Français → Anglais", - "description": "Terminologie technique et informatique française vers anglaise pour documentation, spécifications et rapports IT", + "name": "Technologie / IT - Français → Multilingue", + "description": "Terminologie technique et informatique française traduite en 12 langues pour documentation, spécifications et rapports IT", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "tech_fr_en.json", "terms_count": 155 }, "finance": { - "name": "Finance & Comptabilité - Français → Anglais", - "description": "Terminologie financière et comptable française vers anglaise pour rapports annuels, bilans et documents financiers", + "name": "Finance & Comptabilité - Français → Multilingue", + "description": "Terminologie financière et comptable française traduite en 12 langues pour rapports annuels, bilans et documents financiers", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "finance_fr_en.json", "terms_count": 150 }, "medical": { - "name": "Médical & Santé - Français → Anglais", - "description": "Terminologie médicale française vers anglaise pour rapports médicaux, publications scientifiques et documents de santé", + "name": "Médical & Santé - Français → Multilingue", + "description": "Terminologie médicale française traduite en 12 langues pour rapports médicaux, publications scientifiques et documents de santé", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "medical_fr_en.json", "terms_count": 165 }, "marketing": { - "name": "Marketing & Communication - Français → Anglais", - "description": "Terminologie marketing et communication française vers anglaise pour campagnes, stratégies et contenus marketing", + "name": "Marketing & Communication - Français → Multilingue", + "description": "Terminologie marketing et communication française traduite en 12 langues pour campagnes, stratégies et contenus marketing", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "marketing_fr_en.json", "terms_count": 175 }, "hr": { - "name": "RH & Ressources Humaines - Français → Anglais", - "description": "Terminologie des ressources humaines française vers anglaise pour contrats, politiques et documents RH", + "name": "RH & Ressources Humaines - Français → Multilingue", + "description": "Terminologie des ressources humaines française traduite en 12 langues pour contrats, politiques et documents RH", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "hr_fr_en.json", "terms_count": 160 }, "scientific": { - "name": "Scientifique & Recherche - Français → Anglais", - "description": "Terminologie scientifique française vers anglaise pour publications, articles de recherche et thèses", + "name": "Scientifique & Recherche - Français → Multilingue", + "description": "Terminologie scientifique française traduite en 12 langues pour publications, articles de recherche et thèses", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "scientific_fr_en.json", "terms_count": 160 }, "ecommerce": { - "name": "E-commerce & Vente - Français → Anglais", - "description": "Terminologie e-commerce et vente française vers anglaise pour boutiques en ligne, catalogues et documents commerciaux", + "name": "E-commerce & Vente - Français → Multilingue", + "description": "Terminologie e-commerce et vente française traduite en 12 langues pour boutiques en ligne, catalogues et documents commerciaux", "source_lang": "fr", - "target_lang": "en", + "target_lang": "multi", "file": "ecommerce_fr_en.json", "terms_count": 195 } diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index 3bffa45..326674d 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -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 )} - {/* 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 && (
🎯 @@ -365,7 +371,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {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") } @@ -427,7 +433,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{g.name} - {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
{isSelected && } @@ -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 (