From 58d9d8a74c0a75d0b38ba19396920d50fbaac5a5 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 31 May 2026 11:18:57 +0200 Subject: [PATCH] fix(frontend): increase translation config legibility and integrate templates in glossary selector --- .../dashboard/translate/GlossarySelector.tsx | 275 ++++++++++-------- .../dashboard/translate/LanguageSelector.tsx | 18 +- .../dashboard/translate/ProviderSelector.tsx | 17 +- 3 files changed, 168 insertions(+), 142 deletions(-) diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index 2fb9200..03731ff 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -7,6 +7,7 @@ import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import { Switch } from '@/components/ui/switch'; import { SUPPORTED_LANGUAGES } from '../glossaries/types'; +import { languages as API_LANGUAGES } from '@/lib/api'; interface GlossaryOption { id: string; @@ -45,7 +46,6 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary const [importingId, setImportingId] = useState(null); const [error, setError] = useState(null); const [isOpen, setIsOpen] = useState(false); - const [showTemplates, setShowTemplates] = useState(false); const containerRef = useRef(null); const [filterByLang, setFilterByLang] = useState(sourceLang !== 'auto'); @@ -225,8 +225,14 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary } }; - const sourceFlag = SUPPORTED_LANGUAGES.find(l => l.code === sourceLang)?.flag ?? ''; - const targetFlag = SUPPORTED_LANGUAGES.find(l => l.code === targetLang)?.flag ?? ''; + const getFlag = useCallback((code: string) => { + return API_LANGUAGES.find(l => l.code === code)?.flag ?? + SUPPORTED_LANGUAGES.find(l => l.code === code)?.flag ?? + code.toUpperCase(); + }, []); + + const sourceFlag = useMemo(() => sourceLang === 'auto' ? '' : getFlag(sourceLang), [sourceLang, getFlag]); + const targetFlag = useMemo(() => getFlag(targetLang), [targetLang, getFlag]); const filteredGlossaries = useMemo(() => { if (!filterByLang || sourceLang === 'auto') { @@ -235,7 +241,15 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary return glossaries.filter(g => g.source_language === sourceLang); }, [glossaries, filterByLang, sourceLang]); + const filteredTemplates = useMemo(() => { + if (!filterByLang || sourceLang === 'auto') { + return templates; + } + return templates.filter(t => t.source_lang === sourceLang); + }, [templates, filterByLang, sourceLang]); + const selected = glossaries.find(g => g.id === glossaryId); + return (
- - + + {t('translate.glossary.title') || 'Glossaire & Terminologie'} - + ({sourceFlag || 'AUTO'}➔{targetFlag})
@@ -282,13 +296,13 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {mode === 'classic' ? (
- + Moteur neutre sans glossaire (IA uniquement)
) : !isPro ? (
-

+

{t('translate.glossary.proOnly') || 'Passez Pro pour appliquer vos glossaires terminologiques.'}

@@ -302,44 +316,44 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary disabled={disabled} onClick={() => setIsOpen(!isOpen)} className={cn( - "w-full bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 py-2 px-3 rounded-lg flex items-center justify-between shadow-sm transition-all", + "w-full bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 py-2.5 px-3 rounded-lg flex items-center justify-between shadow-sm transition-all cursor-pointer", isOpen && "border-brand-accent dark:border-brand-accent", disabled && "opacity-50 cursor-not-allowed" )} >
- + {selected ? selected.name : (isLoading ? "Chargement..." : "Sélectionner un glossaire...")} - + {selected - ? `${SUPPORTED_LANGUAGES.find(l => l.code === selected.source_language)?.flag || '🌐'} ➜ ${targetFlag} • ${selected.terms_count} termes` - : (filteredGlossaries.length > 0 ? "Aucun glossaire sélectionné" : "Aucun glossaire disponible") + ? `${getFlag(selected.source_language)} ➜ ${targetFlag} • ${selected.terms_count} termes` + : (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? "Sélectionnez un glossaire" : "Aucun glossaire disponible") }
- + {/* Error message */} {error && ( -

{error}

+

{error}

)} {/* Selector Dropdown list */} {isOpen && !disabled && ( -
+
{/* Filter toggle header */} - {sourceLang !== 'auto' && glossaries.length > 0 && ( -
- + {sourceLang !== 'auto' && (glossaries.length > 0 || templates.length > 0) && ( +
+ Filtrer par langue ({sourceFlag})
)} - {filteredGlossaries.length > 0 ? ( - filteredGlossaries.map(g => { - const flag = SUPPORTED_LANGUAGES.find(l => l.code === g.source_language)?.flag ?? ''; - const isSelected = g.id === glossaryId; - return ( - - ); - }) - ) : ( + {/* Custom Glossaries Section */} + {filteredGlossaries.length > 0 && ( +
+
+ Mes Glossaires +
+ {filteredGlossaries.map(g => { + const flag = getFlag(g.source_language); + const isSelected = g.id === glossaryId; + return ( + + ); + })} +
+ )} + + {/* Templates Section */} + {filteredTemplates.length > 0 && ( +
+
+ Modèles disponibles +
+
+ {filteredTemplates.map(tmpl => { + const isImporting = importingId === tmpl.id; + const existingGlossary = glossaries.find( + g => g.name.toLowerCase().includes(tmpl.name.toLowerCase().split('/')[0].trim()) + ); + const isAlreadySelected = existingGlossary?.id === glossaryId; + const flag = getFlag(tmpl.source_lang); + const tFlag = getFlag(tmpl.target_lang); + + return ( + + ); + })} +
+
+ )} + + {/* Empty State */} + {filteredGlossaries.length === 0 && filteredTemplates.length === 0 && (
-

- Aucun glossaire pour la langue {sourceFlag || sourceLang.toUpperCase()}. +

+ Aucun glossaire ni modèle disponible pour la langue {sourceFlag || sourceLang.toUpperCase()}.

- {filterByLang && glossaries.length > 0 && ( + {filterByLang && (glossaries.length > 0 || templates.length > 0) && ( )} Créer un glossaire ➔ @@ -409,43 +488,43 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {selected && (
- + Aperçu des correspondances actives : - + {selectedGlossaryDetail?.terms?.length || selected.terms_count} au total
{isLoadingDetail ? ( -
- Chargement... +
+ Chargement...
) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? ( -
+
{selectedGlossaryDetail.terms.slice(0, 4).map((t: any, i: number) => ( -
- +
+ {t.source} ➔ {t.target}
))} {selectedGlossaryDetail.terms.length > 4 && ( - + + {selectedGlossaryDetail.terms.length - 4} autres termes )}
) : ( -

Aucun terme dans ce glossaire.

+

Aucun terme dans ce glossaire.

)}
)} {/* Ultra-neat Quick Term Adder */} {selected && ( -
+ setNewSource(e.target.value)} disabled={isAddingTerm || disabled} - className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2 py-1 text-[8px] font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent" + className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent" /> setNewTarget(e.target.value)} disabled={isAddingTerm || disabled} - className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2 py-1 text-[8px] font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent" + className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent" />
)} - - {/* Interactive Accordion for Templates Generator */} - {templates.length > 0 && ( -
- - - {showTemplates && ( -
- {templates.map(tmpl => { - const isImporting = importingId === tmpl.id; - const existingGlossary = glossaries.find( - g => g.name.toLowerCase().includes(tmpl.name.toLowerCase().split('/')[0].trim()) - ); - const isAlreadySelected = existingGlossary?.id === glossaryId; - - return ( - - ); - })} -
- )} -
- )}
) : (
- Moteur neutre sans glossaire appliqué + Moteur neutre sans glossaire appliqué
)}
diff --git a/frontend/src/app/dashboard/translate/LanguageSelector.tsx b/frontend/src/app/dashboard/translate/LanguageSelector.tsx index 40c4f47..283f03f 100644 --- a/frontend/src/app/dashboard/translate/LanguageSelector.tsx +++ b/frontend/src/app/dashboard/translate/LanguageSelector.tsx @@ -66,28 +66,28 @@ function Combobox({ type="button" onClick={() => setOpen(!open)} className={cn( - 'w-full py-2 px-3 bg-brand-muted/60 dark:bg-white/5 rounded-xl border text-[10px] font-bold uppercase tracking-wider text-brand-dark dark:text-white flex items-center justify-between hover:border-brand-accent/50 transition-all select-none cursor-pointer', + 'w-full py-2.5 px-3.5 bg-brand-muted/60 dark:bg-white/5 rounded-xl border text-xs font-bold uppercase tracking-wider text-brand-dark dark:text-white flex items-center justify-between hover:border-brand-accent/50 transition-all select-none cursor-pointer', open ? 'border-brand-accent/50' : 'border-brand-accent/20 dark:border-white/10' )} > {label || placeholder} - + {open && (
-
+
setQuery(e.target.value)} placeholder="Search..." - className="w-full bg-transparent px-1 py-1 text-[10px] outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white" + className="w-full bg-transparent px-1 py-1 text-xs outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white" />
{filtered.length === 0 && ( -
No results
+
No results
)} {filtered.map(lang => ( ))}
@@ -143,7 +143,7 @@ export default function LanguageSelector({
{/* Source */}
- Source + Source - Cible + Cible onProviderChange(p.id)} className={cn( - 'p-2 py-1.5 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-center min-h-[44px]', + 'p-2.5 py-2 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-center min-h-[48px]', isSelected ? 'border-brand-accent bg-brand-accent/5 dark:bg-brand-accent/10' : 'border-black/[0.05] dark:border-white/[0.05] bg-brand-muted/20 dark:bg-zinc-800/10 hover:bg-brand-muted/50 dark:hover:bg-zinc-800/20' @@ -150,14 +150,14 @@ export function ProviderSelector({ >
Standard {isSelected &&
}
- + {label} @@ -167,6 +167,7 @@ export function ProviderSelector({ const renderLlmCard = (p: AvailableProvider, locked: boolean) => { const isSelected = provider === p.id; const theme = LLM_THEMES[p.id] || DEFAULT_LLM_THEME; + const displayLabel = p.model || p.label.replace(/^Traduction\s+IA\s+/i, ''); return (