fix(glossaries): restore selectable source language (data was already there)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m37s
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:
@@ -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'}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user