diff --git a/frontend/src/app/dashboard/glossaries/[id]/page.tsx b/frontend/src/app/dashboard/glossaries/[id]/page.tsx index ddd0bb1..3a1ef55 100644 --- a/frontend/src/app/dashboard/glossaries/[id]/page.tsx +++ b/frontend/src/app/dashboard/glossaries/[id]/page.tsx @@ -37,25 +37,24 @@ const MAX_TERMS = 500; */ function getDisplaySource( term: { source: string; target: string; translations?: Record | null }, - lang: string + lang: string, + glossarySourceLang: string ): string { if (!lang || lang === 'multi') return ''; - if (lang === 'fr') return term.source; - if (lang === 'en') return term.target; + if (lang === glossarySourceLang) return term.source; const translations = term.translations || {}; return translations[lang] || ''; } -/** Target term in the chosen language. Mêmes règles que getDisplaySource, - * mais l'inverse : EN est le défaut (champ target), FR vient de source. +/** Target term in the chosen language. */ function getDisplayTarget( term: { source: string; target: string; translations?: Record | null }, - lang: string + lang: string, + glossaryTargetLang: string ): string { - if (!lang || lang === 'multi') return term.target; // multi = défaut = EN - if (lang === 'en') return term.target; - if (lang === 'fr') return term.source; + if (!lang) return ''; + if (lang === 'multi' || lang === glossaryTargetLang) return term.target; const translations = term.translations || {}; return translations[lang] || ''; } @@ -145,39 +144,82 @@ export default function GlossaryDetailPage() { }; const handleTermChange = (index: number, field: 'source' | 'target', value: string) => { + if (!glossary) return; setTerms(terms.map((t, i) => { if (i !== index) return t; const translations = { ...(t.translations || {}) } as Record; - // FR et EN sont dans les champs legacy (term.source / term.target), - // les 11 autres langues dans translations[lang]. - // Pour l'édition, on écrit au bon endroit selon la langue choisie. - const isFr = field === 'source' ? sourceLanguage === 'fr' : targetLanguage === 'fr'; - const isEn = field === 'source' ? sourceLanguage === 'en' : targetLanguage === 'en'; - const otherLang = field === 'source' ? sourceLanguage : targetLanguage; + const editLang = field === 'source' ? sourceLanguage : targetLanguage; - if (isFr) { - // Écriture du français → champ source (legacy) - return { ...t, source: value }; + if (field === 'source') { + if (editLang === glossary.source_language) { + return { ...t, source: value }; + } else { + if (editLang && editLang !== 'multi') { + translations[editLang] = value; + } + return { ...t, translations }; + } + } else { + if (editLang === 'multi' || editLang === glossary.target_language) { + return { ...t, target: value }; + } else { + if (editLang && editLang !== 'multi') { + translations[editLang] = value; + } + return { ...t, translations }; + } } - if (isEn || !otherLang || otherLang === 'multi') { - // Écriture de l'anglais (ou défaut multi) → champ target (legacy) - return { ...t, target: value }; - } - // Autre langue → translations[otherLang] - translations[otherLang] = value; - return { ...t, source: t.source, target: t.target, translations }; })); }; + const migrateTerms = ( + currentTerms: GlossaryTermInput[], + oldSrc: string, + newSrc: string, + oldTgt: string, + newTgt: string + ): GlossaryTermInput[] => { + if (!glossary) return currentTerms; + return currentTerms.map(t => { + const languagesData: Record = { ...(t.translations || {}) }; + + const oldSrcVal = oldSrc === glossary.source_language ? t.source : (t.translations?.[oldSrc] || ''); + const oldTgtVal = oldTgt === 'multi' || oldTgt === glossary.target_language ? t.target : (t.translations?.[oldTgt] || ''); + + if (oldSrc && oldSrc !== 'multi') { + languagesData[oldSrc] = oldSrcVal; + } + if (oldTgt) { + const langKey = oldTgt === 'multi' ? 'en' : oldTgt; + languagesData[langKey] = oldTgtVal; + } + + const newSourceVal = languagesData[newSrc] || ''; + const targetLangKey = newTgt === 'multi' ? 'en' : newTgt; + const newTargetVal = languagesData[targetLangKey] || ''; + + delete languagesData[newSrc]; + delete languagesData[targetLangKey]; + + return { + source: newSourceVal, + target: newTargetVal, + translations: languagesData + }; + }); + }; + const handleSourceLanguageChange = (newLang: string) => { + const updated = migrateTerms(terms, sourceLanguage, newLang, targetLanguage, targetLanguage); setSourceLanguage(newLang); - // No remap : l'affichage lit translations[newLang] à la volée. + setTerms(updated); }; const handleTargetLanguageChange = (newLang: string) => { + const updated = migrateTerms(terms, sourceLanguage, sourceLanguage, targetLanguage, newLang); setTargetLanguage(newLang); - // No remap : l'affichage lit translations[newLang] à la volée. + setTerms(updated); }; const handleSave = async () => { @@ -520,7 +562,7 @@ export default function GlossaryDetailPage() { > handleTermChange(term._index, 'source', e.target.value)} disabled={isUpdating} placeholder={t('glossaries.detail.sourcePlaceholder') || 'terme source'} @@ -529,7 +571,7 @@ export default function GlossaryDetailPage() { handleTermChange(term._index, 'target', e.target.value)} disabled={isUpdating} placeholder={t('glossaries.detail.targetPlaceholder') || 'terme cible'} diff --git a/frontend/src/lib/i18n/messages/en/translate.json b/frontend/src/lib/i18n/messages/en/translate.json index 95716a5..4f61f45 100644 --- a/frontend/src/lib/i18n/messages/en/translate.json +++ b/frontend/src/lib/i18n/messages/en/translate.json @@ -18,7 +18,7 @@ "translate.glossary.noGlossaryForPair": "No glossary for", "translate.glossary.noGlossaries": "No glossaries yet", "translate.glossary.loading": "Loading...", - "translate.glossary.classicMode": "Neutral engine without glossary (AI only)", + "translate.glossary.classicMode": "Classic engine (no glossary - Pro LLM mode required)", "translate.glossary.selectPlaceholder": "Select a glossary...", "translate.glossary.multilingual": "MULTILINGUAL", "translate.glossary.noGlossaryAvailable": "No glossary available", @@ -38,7 +38,7 @@ "translate.glossary.sourceTerm": "Source term", "translate.glossary.translation": "Translation", "translate.glossary.addTerm": "Add term", - "translate.glossary.disabledMode": "Neutral engine without glossary applied", + "translate.glossary.disabledMode": "Classic engine without glossary applied", "translate.glossary.addTermError": "Error adding term", "translate.glossary.networkError": "Network error", "translate.glossary.importFailed": "Import failed ({status})", diff --git a/frontend/src/lib/i18n/messages/fr/translate.json b/frontend/src/lib/i18n/messages/fr/translate.json index 88430a7..e3e78ee 100644 --- a/frontend/src/lib/i18n/messages/fr/translate.json +++ b/frontend/src/lib/i18n/messages/fr/translate.json @@ -18,7 +18,7 @@ "translate.glossary.noGlossaryForPair": "Aucun glossaire pour", "translate.glossary.noGlossaries": "Aucun glossaire", "translate.glossary.loading": "Chargement...", - "translate.glossary.classicMode": "Moteur neutre sans glossaire (IA uniquement)", + "translate.glossary.classicMode": "Moteur classique (sans glossaire - Mode Pro LLM requis)", "translate.glossary.selectPlaceholder": "Sélectionner un glossaire...", "translate.glossary.multilingual": "MULTILINGUE", "translate.glossary.noGlossaryAvailable": "Aucun glossaire disponible", @@ -38,7 +38,7 @@ "translate.glossary.sourceTerm": "Terme Source", "translate.glossary.translation": "Traduction", "translate.glossary.addTerm": "Ajouter le terme", - "translate.glossary.disabledMode": "Moteur neutre sans glossaire appliqué", + "translate.glossary.disabledMode": "Moteur classique sans glossaire appliqué", "translate.glossary.addTermError": "Erreur lors de l'ajout du terme", "translate.glossary.networkError": "Erreur réseau", "translate.glossary.importFailed": "Échec de l'importation ({status})", diff --git a/routes/translate_routes.py b/routes/translate_routes.py index d69c158..9b7486c 100644 --- a/routes/translate_routes.py +++ b/routes/translate_routes.py @@ -1048,12 +1048,14 @@ async def _run_translation_job( # Story 3.10: Retrieve and format glossary terms for LLM prompt glossary_terms = None glossary_source_lang = "fr" + glossary_target_lang = "multi" if glossary_id and user_id: try: glossary_data = get_glossary_terms(glossary_id, user_id) glossary_terms = glossary_data["terms"] glossary_source_lang = glossary_data.get("source_language", "fr") - logger.info(f"Job {job_id}: Loaded {len(glossary_terms)} glossary terms (source: {glossary_source_lang})") + glossary_target_lang = glossary_data.get("target_language", "multi") + logger.info(f"Job {job_id}: Loaded {len(glossary_terms)} glossary terms (source: {glossary_source_lang}, target: {glossary_target_lang})") except GlossaryNotFoundError as e: tracker.set_error(str(e)) logger.error(f"Job {job_id}: Glossary error - {e}") @@ -1078,6 +1080,7 @@ async def _run_translation_job( full_prompt = build_full_prompt( effective_prompt, glossary_terms, source_lang=glossary_source_lang, target_lang=target_lang, + glossary_target_lang=glossary_target_lang, ) from services.providers.google_provider import GoogleTranslationProvider diff --git a/services/glossary_service.py b/services/glossary_service.py index 64559df..da33b05 100644 --- a/services/glossary_service.py +++ b/services/glossary_service.py @@ -122,6 +122,7 @@ def format_glossary_for_prompt( terms: List[Dict[str, str]], source_lang: str = "fr", target_lang: str = "en", + glossary_target_lang: str = "multi", ) -> str: """ Format glossary terms for injection into an LLM system prompt. @@ -135,6 +136,7 @@ def format_glossary_for_prompt( terms: List of dicts with 'source', 'target', and optional 'translations' source_lang: ISO code of the source language target_lang: ISO code of the target language + glossary_target_lang: ISO code of the glossary's target language configuration Returns: Formatted string for LLM prompt @@ -166,8 +168,11 @@ def format_glossary_for_prompt( elif default_target: source_escaped = source.replace("'", "\\'") target_escaped = default_target.replace("'", "\\'") - lines.append(f"- '{source_escaped}' → '{target_escaped}' (EN reference, adapt to {target_lang})") - has_fallback = True + if glossary_target_lang == target_lang: + lines.append(f"- '{source_escaped}' → '{target_escaped}'") + else: + lines.append(f"- '{source_escaped}' → '{target_escaped}' (EN reference, adapt to {target_lang})") + has_fallback = True # If neither specific nor default, skip the term if not any(line.startswith("- ") for line in lines): @@ -192,6 +197,7 @@ def build_full_prompt( glossary_terms: Optional[List[Dict[str, str]]], source_lang: str = "fr", target_lang: str = "en", + glossary_target_lang: str = "multi", ) -> str: """ Build the complete prompt combining custom prompt and glossary. @@ -201,6 +207,7 @@ def build_full_prompt( glossary_terms: Optional list of glossary terms source_lang: ISO code of the source language target_lang: ISO code of the target language + glossary_target_lang: ISO code of the glossary's target language configuration Returns: Combined prompt string @@ -211,7 +218,9 @@ def build_full_prompt( parts.append(custom_prompt) if glossary_terms: - glossary_prompt = format_glossary_for_prompt(glossary_terms, source_lang, target_lang) + glossary_prompt = format_glossary_for_prompt( + glossary_terms, source_lang, target_lang, glossary_target_lang + ) if glossary_prompt: parts.append(glossary_prompt)