fix(glossaries): restore selectable source language (data was already there)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m37s

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.
This commit is contained in:
Sepehr
2026-06-07 10:11:19 +02:00
parent e11a6b16a0
commit 79848230c0
2 changed files with 87 additions and 20 deletions

View File

@@ -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<string, string> | 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<string, string> | 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<string, string>;
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() {
<label className="text-[10px] font-bold uppercase tracking-widest text-brand-dark/50 dark:text-white/50 mb-1.5 block">
{t('glossaries.detail.sourceLang') || 'Langue source'}
</label>
<div className="h-10 rounded-lg border border-input bg-muted/40 px-3 flex items-center gap-2 text-sm text-brand-dark/70 dark:text-white/70">
<span className="text-base leading-none">🇫🇷</span>
<span>Français</span>
<span className="ml-auto text-[10px] text-brand-dark/40 dark:text-white/40 uppercase tracking-wider font-medium">
{t('glossaries.detail.sourceLocked') || 'fixé'}
</span>
</div>
<select
value={sourceLanguage}
onChange={(e) => handleSourceLanguageChange(e.target.value)}
disabled={isUpdating}
className="w-full h-10 rounded-lg border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent/20"
>
{SUPPORTED_LANGUAGES.filter((l) => l.code !== 'multi').map((l) => (
<option key={l.code} value={l.code}>
{l.flag} {l.label}
</option>
))}
</select>
<p className="text-[10px] text-brand-dark/40 dark:text-white/40 font-light mt-1.5">
{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).'}
</p>
</div>
<div>
@@ -462,7 +518,7 @@ export default function GlossaryDetailPage() {
>
<td className="px-6 py-1.5">
<input
value={term.source}
value={getDisplaySource(term, sourceLanguage)}
onChange={(e) => handleTermChange(term._index, 'source', e.target.value)}
disabled={isUpdating}
placeholder={t('glossaries.detail.sourcePlaceholder') || 'terme source'}
@@ -471,9 +527,7 @@ export default function GlossaryDetailPage() {
</td>
<td className="px-3 py-1.5">
<input
value={targetLanguage === 'multi' || targetLanguage === 'en'
? term.target
: (term.translations?.[targetLanguage] || term.target)}
value={getDisplayTarget(term, targetLanguage)}
onChange={(e) => handleTermChange(term._index, 'target', e.target.value)}
disabled={isUpdating}
placeholder={t('glossaries.detail.targetPlaceholder') || 'terme cible'}

View File

@@ -438,6 +438,7 @@ const messages: Record<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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.",