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:
@@ -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'
|
||||||
|
""")
|
||||||
@@ -1,68 +1,68 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"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.",
|
"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": {
|
"categories": {
|
||||||
"legal": {
|
"legal": {
|
||||||
"name": "Juridique - Français → Anglais",
|
"name": "Juridique - Français → Multilingue",
|
||||||
"description": "Terminologie juridique française vers anglaise pour contrats, statuts et documents légaux",
|
"description": "Terminologie juridique française traduite en 12 langues pour contrats, statuts et documents légaux",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "legal_fr_en.json",
|
"file": "legal_fr_en.json",
|
||||||
"terms_count": 105
|
"terms_count": 105
|
||||||
},
|
},
|
||||||
"technology": {
|
"technology": {
|
||||||
"name": "Technologie / IT - Français → Anglais",
|
"name": "Technologie / IT - Français → Multilingue",
|
||||||
"description": "Terminologie technique et informatique française vers anglaise pour documentation, spécifications et rapports IT",
|
"description": "Terminologie technique et informatique française traduite en 12 langues pour documentation, spécifications et rapports IT",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "tech_fr_en.json",
|
"file": "tech_fr_en.json",
|
||||||
"terms_count": 155
|
"terms_count": 155
|
||||||
},
|
},
|
||||||
"finance": {
|
"finance": {
|
||||||
"name": "Finance & Comptabilité - Français → Anglais",
|
"name": "Finance & Comptabilité - Français → Multilingue",
|
||||||
"description": "Terminologie financière et comptable française vers anglaise pour rapports annuels, bilans et documents financiers",
|
"description": "Terminologie financière et comptable française traduite en 12 langues pour rapports annuels, bilans et documents financiers",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "finance_fr_en.json",
|
"file": "finance_fr_en.json",
|
||||||
"terms_count": 150
|
"terms_count": 150
|
||||||
},
|
},
|
||||||
"medical": {
|
"medical": {
|
||||||
"name": "Médical & Santé - Français → Anglais",
|
"name": "Médical & Santé - Français → Multilingue",
|
||||||
"description": "Terminologie médicale française vers anglaise pour rapports médicaux, publications scientifiques et documents de santé",
|
"description": "Terminologie médicale française traduite en 12 langues pour rapports médicaux, publications scientifiques et documents de santé",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "medical_fr_en.json",
|
"file": "medical_fr_en.json",
|
||||||
"terms_count": 165
|
"terms_count": 165
|
||||||
},
|
},
|
||||||
"marketing": {
|
"marketing": {
|
||||||
"name": "Marketing & Communication - Français → Anglais",
|
"name": "Marketing & Communication - Français → Multilingue",
|
||||||
"description": "Terminologie marketing et communication française vers anglaise pour campagnes, stratégies et contenus marketing",
|
"description": "Terminologie marketing et communication française traduite en 12 langues pour campagnes, stratégies et contenus marketing",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "marketing_fr_en.json",
|
"file": "marketing_fr_en.json",
|
||||||
"terms_count": 175
|
"terms_count": 175
|
||||||
},
|
},
|
||||||
"hr": {
|
"hr": {
|
||||||
"name": "RH & Ressources Humaines - Français → Anglais",
|
"name": "RH & Ressources Humaines - Français → Multilingue",
|
||||||
"description": "Terminologie des ressources humaines française vers anglaise pour contrats, politiques et documents RH",
|
"description": "Terminologie des ressources humaines française traduite en 12 langues pour contrats, politiques et documents RH",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "hr_fr_en.json",
|
"file": "hr_fr_en.json",
|
||||||
"terms_count": 160
|
"terms_count": 160
|
||||||
},
|
},
|
||||||
"scientific": {
|
"scientific": {
|
||||||
"name": "Scientifique & Recherche - Français → Anglais",
|
"name": "Scientifique & Recherche - Français → Multilingue",
|
||||||
"description": "Terminologie scientifique française vers anglaise pour publications, articles de recherche et thèses",
|
"description": "Terminologie scientifique française traduite en 12 langues pour publications, articles de recherche et thèses",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "scientific_fr_en.json",
|
"file": "scientific_fr_en.json",
|
||||||
"terms_count": 160
|
"terms_count": 160
|
||||||
},
|
},
|
||||||
"ecommerce": {
|
"ecommerce": {
|
||||||
"name": "E-commerce & Vente - Français → Anglais",
|
"name": "E-commerce & Vente - Français → Multilingue",
|
||||||
"description": "Terminologie e-commerce et vente française vers anglaise pour boutiques en ligne, catalogues et documents commerciaux",
|
"description": "Terminologie e-commerce et vente française traduite en 12 langues pour boutiques en ligne, catalogues et documents commerciaux",
|
||||||
"source_lang": "fr",
|
"source_lang": "fr",
|
||||||
"target_lang": "en",
|
"target_lang": "multi",
|
||||||
"file": "ecommerce_fr_en.json",
|
"file": "ecommerce_fr_en.json",
|
||||||
"terms_count": 195
|
"terms_count": 195
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,13 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
|||||||
const sourceFlag = useMemo(() => sourceLang === 'auto' ? '' : getFlag(sourceLang), [sourceLang, getFlag]);
|
const sourceFlag = useMemo(() => sourceLang === 'auto' ? '' : getFlag(sourceLang), [sourceLang, getFlag]);
|
||||||
const targetFlag = useMemo(() => getFlag(targetLang), [targetLang, 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(() => {
|
const filteredGlossaries = useMemo(() => {
|
||||||
if (!filterByLang || sourceLang === 'auto') {
|
if (!filterByLang || sourceLang === 'auto') {
|
||||||
return glossaries;
|
return glossaries;
|
||||||
@@ -242,12 +249,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
|||||||
return glossaries
|
return glossaries
|
||||||
.filter(g => g.source_language === sourceLang)
|
.filter(g => g.source_language === sourceLang)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// Glossaries matching source + target come first
|
// Compatible glossaries first, then incompatible
|
||||||
const aMatch = a.target_language === targetLang ? 0 : 1;
|
const aOk = isCompatible(a.target_language) ? 0 : 1;
|
||||||
const bMatch = b.target_language === targetLang ? 0 : 1;
|
const bOk = isCompatible(b.target_language) ? 0 : 1;
|
||||||
return aMatch - bMatch;
|
return aOk - bOk;
|
||||||
});
|
});
|
||||||
}, [glossaries, filterByLang, sourceLang, targetLang]);
|
}, [glossaries, filterByLang, sourceLang, isCompatible]);
|
||||||
|
|
||||||
const filteredTemplates = useMemo(() => {
|
const filteredTemplates = useMemo(() => {
|
||||||
if (!filterByLang || sourceLang === 'auto') {
|
if (!filterByLang || sourceLang === 'auto') {
|
||||||
@@ -285,12 +292,11 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
|||||||
if (!nextVal) {
|
if (!nextVal) {
|
||||||
onChange(null);
|
onChange(null);
|
||||||
} else if (!glossaryId) {
|
} else if (!glossaryId) {
|
||||||
// Only auto-select if a glossary matches BOTH source and target language
|
// Auto-select first compatible glossary (exact target match or multilingual)
|
||||||
const matching = filteredGlossaries.find(g => g.target_language === targetLang);
|
const matching = filteredGlossaries.find(g => isCompatible(g.target_language));
|
||||||
if (matching) {
|
if (matching) {
|
||||||
onChange(matching.id);
|
onChange(matching.id);
|
||||||
}
|
}
|
||||||
// If no glossary matches the target, leave unselected — user picks manually
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -337,8 +343,8 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mismatch Warning — target language */}
|
{/* Mismatch Warning — target language (skip for multilingual glossaries) */}
|
||||||
{selected && selected.target_language && selected.target_language !== targetLang && (
|
{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">
|
<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 className="shrink-0">🎯</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -365,7 +371,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
|
<span className="text-[10px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
|
||||||
{selected
|
{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")
|
: (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? "Sélectionnez un glossaire" : "Aucun glossaire disponible")
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
@@ -427,7 +433,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
|||||||
<div className="text-left min-w-0 flex-1 pr-2">
|
<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-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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isSelected && <Check size={12} className="text-brand-accent shrink-0" />}
|
{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 isAlreadySelected = existingGlossary?.id === glossaryId;
|
||||||
const flag = getFlag(tmpl.source_lang);
|
const flag = getFlag(tmpl.source_lang);
|
||||||
const tFlag = getFlag(tmpl.target_lang);
|
const tFlag = tmpl.target_lang === 'multi' ? '🌐 MULTILINGUE' : getFlag(tmpl.target_lang);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def _format_glossary(glossary: Glossary) -> dict:
|
|||||||
"id": glossary.id,
|
"id": glossary.id,
|
||||||
"name": glossary.name,
|
"name": glossary.name,
|
||||||
"source_language": glossary.source_language,
|
"source_language": glossary.source_language,
|
||||||
"target_language": getattr(glossary, "target_language", "en") or "en",
|
"target_language": getattr(glossary, "target_language", "multi") or "multi",
|
||||||
"terms": [_format_term(t) for t in glossary.terms] if glossary.terms else [],
|
"terms": [_format_term(t) for t in glossary.terms] if glossary.terms else [],
|
||||||
"created_at": glossary.created_at.isoformat() if glossary.created_at else None,
|
"created_at": glossary.created_at.isoformat() if glossary.created_at else None,
|
||||||
"updated_at": glossary.updated_at.isoformat() if glossary.updated_at else None,
|
"updated_at": glossary.updated_at.isoformat() if glossary.updated_at else None,
|
||||||
|
|||||||
Reference in New Issue
Block a user