From 79848230c0765223c337b7a40bf66910f8805bb9 Mon Sep 17 00:00:00 2001 From: Sepehr Date: Sun, 7 Jun 2026 10:11:19 +0200 Subject: [PATCH] fix(glossaries): restore selectable source language (data was already there) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert du commit e11a6b1 : la langue source doit etre selectionnable (car l'utilisateur peut vouloir traduire depuis n'importe quelle des 12 langues supportees, pas seulement le francais). Le data modele support deja le cas : chaque terme a un champ \ ranslations\ (dict de 11 langues) qui contient la traduction du terme source. Donc pour traduire depuis l'italien, on lit \ erm.translations.it\ comme source, et \ erm.translations.es\ comme cible si la cible est l'espagnol. Changements : - Le combobox 'Langue source' est restaure (12 langues) - Nouvelle fonction \getDisplaySource(term, lang)\ : * 'fr' ou 'multi' → term.source (le francais original) * autre → term.translations[lang] (la traduction dans la langue) * fallback → term.source si la traduction manque - handleTermChange ecrit au bon endroit selon la langue : * source FR → term.source * autre source → term.translations[sourceLanguage] * target 'multi'/'en' → term.target * autre target → term.translations[targetLanguage] - hasUnsavedChanges compare aussi le dict translations (avant il ne comparait que source|target, donc un edit dans une autre langue ne declenchait pas l'alerte 'non enregistre') - Note sous le combobox source explique la regle (FR = source originale, autre = champ translations) - i18n : nouvelle cle \glossaries.detail.sourceLangNote\ ajoutee aux 13 locales (FR + EN traduit) L'utilisateur peut maintenant choisir 'Italien' comme source et 'Espagnol' comme cible, et voir les termes correspondants. --- .../app/dashboard/glossaries/[id]/page.tsx | 94 +++++++++++++++---- frontend/src/lib/i18n.tsx | 13 +++ 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/dashboard/glossaries/[id]/page.tsx b/frontend/src/app/dashboard/glossaries/[id]/page.tsx index a1bbd01..8bfb140 100644 --- a/frontend/src/app/dashboard/glossaries/[id]/page.tsx +++ b/frontend/src/app/dashboard/glossaries/[id]/page.tsx @@ -35,6 +35,21 @@ function getDisplayTarget( return translations[lang] || term.target; } +/** Source term in the given language. + * - 'fr' → term.source (the original French) + * - 'multi' / '' / undefined → term.source (no language chosen, default to FR) + * - other langs → term.translations[lang] (the translated source) + * - falls back to term.source if missing + */ +function getDisplaySource( + term: { source: string; translations?: Record | null }, + lang: string +): string { + if (!lang || lang === 'multi' || lang === 'fr') return term.source; + const translations = term.translations || {}; + return translations[lang] || term.source; +} + export default function GlossaryDetailPage() { const params = useParams(); const router = useRouter(); @@ -84,12 +99,18 @@ export default function GlossaryDetailPage() { if (name.trim() !== glossary.name) return true; if (sourceLanguage !== (glossary.source_language || 'fr')) return true; if (targetLanguage !== (glossary.target_language || 'multi')) return true; - const currentTerms = terms - .filter((t) => t.source.trim() && t.target.trim()) - .map((t) => `${t.source}|${t.target}`).sort().join(';;'); - const originalTerms = glossary.terms - .map((t) => `${t.source}|${t.target}`).sort().join(';;'); - return currentTerms !== originalTerms; + // Compare normalized term payloads (source + target + translations dict). + const normalize = (termList: Array<{ source: string; target: string; translations?: Record | null }>) => + termList + .filter((t) => t.source.trim() && t.target.trim()) + .map((t) => { + const tr = t.translations || {}; + const trKeys = Object.keys(tr).sort(); + return `${t.source}|${t.target}|${trKeys.map((k) => `${k}=${tr[k]}`).join(',')}`; + }) + .sort() + .join(';;'); + return normalize(terms) !== normalize(glossary.terms); }, [glossary, name, sourceLanguage, targetLanguage, terms]); const validTerms = terms.filter((t) => t.source.trim() && t.target.trim()); @@ -114,7 +135,37 @@ export default function GlossaryDetailPage() { }; const handleTermChange = (index: number, field: 'source' | 'target', value: string) => { - setTerms(terms.map((t, i) => (i === index ? { ...t, [field]: value } : t))); + setTerms(terms.map((t, i) => { + if (i !== index) return t; + const translations = { ...(t.translations || {}) } as Record; + + if (field === 'source') { + if (!sourceLanguage || sourceLanguage === 'fr') { + // French source → write to term.source + return { ...t, source: value }; + } + // Other source languages → write to translations[sourceLanguage] + translations[sourceLanguage] = value; + return { ...t, source: t.source, translations }; + } + + if (field === 'target') { + if (!targetLanguage || targetLanguage === 'multi' || targetLanguage === 'en') { + // Default target → write to term.target + return { ...t, target: value }; + } + // Other target languages → write to translations[targetLanguage] + translations[targetLanguage] = value; + return { ...t, target: t.target, translations }; + } + + return t; + })); + }; + + const handleSourceLanguageChange = (newLang: string) => { + setSourceLanguage(newLang); + // No term remapping needed — the display reads translations[newLang] on the fly. }; const handleTargetLanguageChange = (newLang: string) => { @@ -363,16 +414,21 @@ export default function GlossaryDetailPage() { -
- 🇫🇷 - Français - - {t('glossaries.detail.sourceLocked') || 'fixé'} - -
+

- {t('glossaries.detail.sourceLockedNote') || - 'Les templates ne stockent la source qu\'en français. Le multilingue source est sur la roadmap.'} + {t('glossaries.detail.sourceLangNote') || + 'La source originale est en français. Pour les autres langues, on lit le champ translations du terme (s\'il existe).'}

@@ -462,7 +518,7 @@ export default function GlossaryDetailPage() { > handleTermChange(term._index, 'source', e.target.value)} disabled={isUpdating} placeholder={t('glossaries.detail.sourcePlaceholder') || 'terme source'} @@ -471,9 +527,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.tsx b/frontend/src/lib/i18n.tsx index a5f17f5..fd4e585 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -438,6 +438,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -1425,6 +1426,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirmer la suppression ?", "glossaries.detail.confirm": "Confirmer", "glossaries.detail.cancel": "Annuler", + "glossaries.detail.sourceLangNote": "'La source originale est en francais. Pour les autres langues, on lit le champ translations du terme (si disponible).'", "glossaries.detail.sourceLocked": "fixé", "glossaries.detail.sourceLockedNote": "Les templates ne stockent la source qu'en français. Le multilingue source est sur la roadmap.", "glossaries.detail.targetLangNote": "Choisissez une langue pour voir les traductions correspondantes, ou « Multilingue » pour la valeur par défaut.", @@ -2398,6 +2400,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -3326,6 +3329,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -4254,6 +4258,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -5182,6 +5187,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -6110,6 +6116,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -7038,6 +7045,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -7968,6 +7976,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -8895,6 +8904,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -9822,6 +9832,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -10707,6 +10718,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.", @@ -11601,6 +11613,7 @@ const messages: Record> = { "glossaries.detail.confirmDelete": "Confirm deletion?", "glossaries.detail.confirm": "Confirm", "glossaries.detail.cancel": "Cancel", + "glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'", "glossaries.detail.sourceLocked": "fixed", "glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.", "glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.",