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.",