From e497f2d2184227639f74fcf15950be028e6fedab Mon Sep 17 00:00:00 2001 From: Sepehr Date: Sun, 7 Jun 2026 09:38:19 +0200 Subject: [PATCH] refactor(glossaries): single source of truth + dedicated detail page UX refonte : - Retire la section 'Glossaires professionnels' de la vue principale (les 8 cartes de templates sont maintenant dans le dialog de creation) - Cartes 'Vos glossaires' plus simples : nom, langues, termes, date - Cliquer sur la carte navigue vers /dashboard/glossaries/[id] - Plus de boutons Edit/Delete sur la carte (deplaces dans la page detail) - Recherche par nom (visible si > 3 glossaires) - Badge 'Non enregistre' si modifications non sauvegardees Nouvelle page /dashboard/glossaries/[id] : - Edition inline du nom (input), langues source/cible (select) - Tableau des termes avec recherche et edition en place - Ajout/suppression de termes (max 500) - Export / Import CSV (meme logique que l'edit dialog) - Zone danger : confirmation en 2 temps pour la suppression - Back link vers la liste - i18n : 40 nouvelles cles ajoutees aux 13 locales (FR + EN traduit, les autres utilisent le fallback EN) Design preserve : editorial-card, brand-accent, meme typographie, meme palette. Refactor structurel uniquement, pas de restyling. Le system prompt (Instructions de contexte) reste tel quel, au-dessus de la liste des glossaires, comme dans le design actuel. --- .../app/dashboard/glossaries/[id]/page.tsx | 598 ++++++++++++++++++ .../src/app/dashboard/glossaries/page.tsx | 350 +++------- frontend/src/lib/i18n.tsx | 520 +++++++++++++++ 3 files changed, 1192 insertions(+), 276 deletions(-) create mode 100644 frontend/src/app/dashboard/glossaries/[id]/page.tsx diff --git a/frontend/src/app/dashboard/glossaries/[id]/page.tsx b/frontend/src/app/dashboard/glossaries/[id]/page.tsx new file mode 100644 index 0000000..20416ac --- /dev/null +++ b/frontend/src/app/dashboard/glossaries/[id]/page.tsx @@ -0,0 +1,598 @@ +'use client'; + +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { + ArrowLeft, Library, Calendar, Hash, Save, Trash2, Loader2, + CheckCircle2, AlertCircle, Download, Upload, Plus, X, Search, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useI18n } from '@/lib/i18n'; +import { useUser } from '@/app/dashboard/useUser'; +import { + useGlossary, + useGlossaries, +} from '../useGlossaries'; +import { + exportGlossaryToCsv, + parseFileToTerms, + generateCsvContent, +} from '../csvUtils'; +import { SUPPORTED_LANGUAGES } from '../types'; +import type { GlossaryTermInput } from '../types'; +import { useToast } from '@/components/ui/toast'; +import { ProUpgradePrompt } from '../ProUpgradePrompt'; + +const MAX_TERMS = 500; + +function getDisplayTarget( + term: { target: string; translations?: Record | null }, + lang: string +): string { + if (lang === 'multi' || lang === 'en' || !lang) return term.target; + const translations = term.translations || {}; + return translations[lang] || term.target; +} + +export default function GlossaryDetailPage() { + const params = useParams(); + const router = useRouter(); + const id = (params?.id as string) || ''; + const { t } = useI18n(); + const { data: user, isLoading: isLoadingUser } = useUser(); + const { glossary, isLoading, error } = useGlossary(id); + const { updateGlossary, deleteGlossary, isUpdating, isDeleting } = useGlossaries(); + const { toast } = useToast(); + + const isPro = user?.tier === 'pro'; + const fileInputRef = useRef(null); + + // Editable state + const [name, setName] = useState(''); + const [sourceLanguage, setSourceLanguage] = useState('fr'); + const [targetLanguage, setTargetLanguage] = useState('multi'); + const [terms, setTerms] = useState([]); + const [initialized, setInitialized] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + // Initialize from glossary data + useEffect(() => { + if (glossary && !initialized) { + setName(glossary.name); + setSourceLanguage(glossary.source_language || 'fr'); + setTargetLanguage(glossary.target_language || 'multi'); + setTerms( + glossary.terms.map((t) => ({ + source: t.source, + target: t.target, + translations: t.translations || {}, + })) + ); + setInitialized(true); + } + }, [glossary, initialized]); + + // Reset on id change + useEffect(() => { + setInitialized(false); + }, [id]); + + const hasUnsavedChanges = useCallback(() => { + if (!glossary) return false; + 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; + }, [glossary, name, sourceLanguage, targetLanguage, terms]); + + const validTerms = terms.filter((t) => t.source.trim() && t.target.trim()); + const validTermsCount = validTerms.length; + const isDirty = hasUnsavedChanges(); + + const handleAddTerm = () => { + if (validTermsCount >= MAX_TERMS) { + toast({ + variant: 'destructive', + title: t('glossaries.detail.maxTermsTitle') || 'Limite atteinte', + description: t('glossaries.detail.maxTermsDesc', { max: String(MAX_TERMS) }) || + `Maximum ${MAX_TERMS} termes par glossaire.`, + }); + return; + } + setTerms([...terms, { source: '', target: '' }]); + }; + + const handleRemoveTerm = (index: number) => { + setTerms(terms.filter((_, i) => i !== index)); + }; + + const handleTermChange = (index: number, field: 'source' | 'target', value: string) => { + setTerms(terms.map((t, i) => (i === index ? { ...t, [field]: value } : t))); + }; + + const handleTargetLanguageChange = (newLang: string) => { + if (glossary) { + setTargetLanguage(newLang); + if (newLang === 'multi' || newLang === 'en') return; + // Remap each term to show translation for the new language (when available) + setTerms((prev) => + prev.map((t) => { + const translations = (t.translations || {}) as Record; + const langTarget = translations[newLang]; + if (langTarget) { + return { ...t, target: langTarget }; + } + return t; + }) + ); + } + }; + + const handleSave = async () => { + if (!glossary || !name.trim()) return; + try { + await updateGlossary(glossary.id, { + name: name.trim(), + source_language: sourceLanguage, + target_language: targetLanguage, + terms: validTerms, + }); + toast({ + title: t('glossaries.detail.savedTitle') || 'Enregistré', + description: t('glossaries.detail.savedDesc') || 'Le glossaire a été mis à jour.', + }); + } catch { + toast({ + variant: 'destructive', + title: t('glossaries.toast.error') || 'Erreur', + description: t('glossaries.toast.errorUpdate') || 'Impossible de mettre à jour le glossaire.', + }); + } + }; + + const handleDelete = async () => { + if (!glossary) return; + try { + await deleteGlossary(glossary.id); + toast({ + title: t('glossaries.toast.deleted') || 'Supprimé', + description: t('glossaries.toast.deletedDesc') || 'Le glossaire a été supprimé.', + }); + router.push('/dashboard/glossaries'); + } catch { + toast({ + variant: 'destructive', + title: t('glossaries.toast.error') || 'Erreur', + description: t('glossaries.toast.errorDelete') || 'Impossible de supprimer le glossaire.', + }); + } + }; + + const handleExport = () => { + if (!glossary) return; + const csv = generateCsvContent(validTerms); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${name.replace(/[^a-z0-9]/gi, '_') || 'glossary'}.csv`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + const handleImportClick = () => fileInputRef.current?.click(); + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + e.target.value = ''; + try { + const parsed = await parseFileToTerms(file); + if (parsed.length === 0) { + toast({ + variant: 'destructive', + title: t('glossaries.detail.importEmptyTitle') || 'Fichier vide', + description: t('glossaries.detail.importEmptyDesc') || + 'Aucun terme détecté dans ce fichier.', + }); + return; + } + if (parsed.length > MAX_TERMS) { + toast({ + variant: 'destructive', + title: t('glossaries.detail.maxTermsTitle') || 'Trop de termes', + description: t('glossaries.detail.maxTermsDesc', { max: String(MAX_TERMS) }) || + `Maximum ${MAX_TERMS} termes par glossaire.`, + }); + return; + } + setTerms(parsed); + toast({ + title: t('glossaries.detail.importedTitle') || 'Importé', + description: t('glossaries.detail.importedDesc', { count: String(parsed.length) }) || + `${parsed.length} termes importés.`, + }); + } catch { + toast({ + variant: 'destructive', + title: t('glossaries.detail.importErrorTitle') || 'Erreur', + description: t('glossaries.detail.importErrorDesc') || 'Impossible de lire le fichier.', + }); + } + }; + + // Filter terms by search + const filteredTerms = useCallback(() => { + if (!searchQuery.trim()) return terms.map((t, i) => ({ ...t, _index: i })); + const q = searchQuery.toLowerCase(); + return terms + .map((t, i) => ({ ...t, _index: i })) + .filter((t) => t.source.toLowerCase().includes(q) || t.target.toLowerCase().includes(q)); + }, [terms, searchQuery]); + + if (isLoadingUser) { + return ( +
+
+
+ ); + } + + if (!isPro) { + return ; + } + + if (isLoading) { + return ( +
+
+
+

+ {t('glossaries.loading') || 'Chargement…'} +

+
+
+ ); + } + + if (error || !glossary) { + return ( +
+ + + {t('glossaries.detail.backToList') || 'Retour aux glossaires'} + +
+ +

+ {t('glossaries.detail.notFoundTitle') || 'Glossaire introuvable'} +

+

+ {t('glossaries.detail.notFoundDesc') || + 'Ce glossaire n\'existe pas ou vous n\'y avez pas accès.'} +

+
+
+ ); + } + + const srcInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language); + const tgtInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language); + + return ( +
+ + {/* Back link */} + + + {t('glossaries.detail.backToList') || 'Retour aux glossaires'} + + + {/* Header */} +
+
+
+ +
+
+ setName(e.target.value)} + disabled={isUpdating} + className="w-full text-2xl md:text-3xl font-serif font-semibold text-brand-dark dark:text-white tracking-tight leading-tight bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-brand-accent/20 rounded px-2 py-1 -mx-2" + /> +
+ + {srcInfo?.flag ?? '🌐'} {srcInfo?.label ?? glossary.source_language} + + {tgtInfo?.flag ?? '🌐'} {tgtInfo?.label ?? glossary.target_language} + + + + {validTermsCount} {t('glossaries.defineTerms') || 'termes'} + + + + {new Date(glossary.created_at).toLocaleDateString()} + +
+
+
+ + {/* Action bar */} +
+ {isDirty && ( + + + {t('glossaries.status.unsaved') || 'Non enregistré'} + + )} + +
+
+ + {/* Settings card */} +
+

+ {t('glossaries.detail.settingsTitle') || 'Paramètres'} +

+
+
+ + +
+
+ + +
+
+
+ + {/* Terms table card */} +
+
+
+

+ {t('glossaries.detail.termsTitle') || 'Termes'} +

+

+ {validTermsCount} / {MAX_TERMS} {t('glossaries.detail.terms') || 'termes'} +

+
+
+ {terms.length > 5 && ( +
+ + setSearchQuery(e.target.value)} + placeholder={t('glossaries.detail.searchTerms') || 'Filtrer…'} + className="pl-7 pr-2 py-1.5 text-[11px] w-32 sm:w-40 rounded-md border border-black/5 dark:border-white/10 bg-white dark:bg-[#141414] focus:outline-none focus:ring-2 focus:ring-brand-accent/20" + /> +
+ )} +
+
+ + {terms.length === 0 ? ( +
+

+ {t('glossaries.detail.noTerms') || 'Aucun terme pour l\'instant.'} +

+ +
+ ) : ( +
+ + + + + + + + + {filteredTerms().map((term) => ( + + + + + + ))} + +
+ {t('glossaries.detail.source') || 'Source'} + + {t('glossaries.detail.target') || 'Cible'} + +
+ handleTermChange(term._index, 'source', e.target.value)} + disabled={isUpdating} + placeholder={t('glossaries.detail.sourcePlaceholder') || 'terme source'} + className="w-full bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-brand-accent/20 rounded px-2 py-1.5 -mx-2 text-brand-dark dark:text-white" + /> + + handleTermChange(term._index, 'target', e.target.value)} + disabled={isUpdating} + placeholder={t('glossaries.detail.targetPlaceholder') || 'terme cible'} + className="w-full bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-brand-accent/20 rounded px-2 py-1.5 -mx-2 text-brand-dark dark:text-white" + /> + + +
+
+ )} + + {/* Add term footer */} + {terms.length > 0 && ( +
+ + {validTermsCount >= MAX_TERMS && ( + + {t('glossaries.detail.maxReached') || 'Limite maximale atteinte'} + + )} +
+ )} +
+ + {/* CSV + Danger zone */} +
+
+

+ {t('glossaries.detail.csvTitle') || 'CSV'} +

+

+ {t('glossaries.detail.csvDesc') || + 'Exportez vos termes en CSV ou importez-en de nouveaux (remplace la liste actuelle).'} +

+
+ + + +
+
+ +
+

+ {t('glossaries.detail.dangerTitle') || 'Zone danger'} +

+

+ {t('glossaries.detail.dangerDesc') || + 'La suppression est définitive. Tous les termes associés seront perdus.'} +

+ {!confirmDelete ? ( + + ) : ( +
+

+ {t('glossaries.detail.confirmDelete') || 'Confirmer la suppression ?'} +

+
+ + +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/app/dashboard/glossaries/page.tsx b/frontend/src/app/dashboard/glossaries/page.tsx index b74b109..53831a4 100644 --- a/frontend/src/app/dashboard/glossaries/page.tsx +++ b/frontend/src/app/dashboard/glossaries/page.tsx @@ -1,81 +1,52 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { BookText, Plus, Library, Calendar, Hash, - Zap, Save, Trash2, MessageSquare, Loader2, - Monitor, Scale, Stethoscope, BarChart3, - Megaphone, ShoppingCart, FlaskConical, Users, - CheckCircle2, AlertCircle, ArrowRight, MousePointerClick, - Info, ExternalLink, + MessageSquare, Save, Trash2, Loader2, + CheckCircle2, AlertCircle, ArrowRight, Info, ExternalLink, Search, } from 'lucide-react'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { cn } from '@/lib/utils'; import { useUser } from '@/app/dashboard/useUser'; import { useI18n } from '@/lib/i18n'; -import { useGlossaries, useGlossary } from './useGlossaries'; -import type { Glossary, GlossaryTermInput, GlossaryListItem } from './types'; +import { useGlossaries } from './useGlossaries'; +import type { GlossaryListItem } from './types'; import { ProUpgradePrompt } from './ProUpgradePrompt'; import { CreateGlossaryDialog } from './CreateGlossaryDialog'; -import { EditGlossaryDialog } from './EditGlossaryDialog'; -import { DeleteGlossaryDialog } from './DeleteGlossaryDialog'; import { useToast } from '@/components/ui/toast'; import { SUPPORTED_LANGUAGES } from './types'; import { useTranslationStore } from '@/lib/store'; -import { API_BASE } from '@/lib/config'; - -const PRESETS = [ - { key: 'it', titleKey: 'glossaries.presets.it.title', descKey: 'glossaries.presets.it.desc', icon: Monitor, templateId: 'technology' }, - { key: 'legal', titleKey: 'glossaries.presets.legal.title', descKey: 'glossaries.presets.legal.desc', icon: Scale, templateId: 'legal' }, - { key: 'medical', titleKey: 'glossaries.presets.medical.title', descKey: 'glossaries.presets.medical.desc', icon: Stethoscope, templateId: 'medical' }, - { key: 'finance', titleKey: 'glossaries.presets.finance.title', descKey: 'glossaries.presets.finance.desc', icon: BarChart3, templateId: 'finance' }, - { key: 'marketing', titleKey: 'glossaries.presets.marketing.title', descKey: 'glossaries.presets.marketing.desc', icon: Megaphone, templateId: 'marketing' }, - { key: 'hr', titleKey: 'glossaries.presets.hr.title', descKey: 'glossaries.presets.hr.desc', icon: Users, templateId: 'hr' }, - { key: 'scientific', titleKey: 'glossaries.presets.scientific.title', descKey: 'glossaries.presets.scientific.desc', icon: FlaskConical, templateId: 'scientific' }, - { key: 'ecommerce', titleKey: 'glossaries.presets.ecommerce.title', descKey: 'glossaries.presets.ecommerce.desc', icon: ShoppingCart, templateId: 'ecommerce' }, -]; export default function GlossariesPage() { const { t } = useI18n(); + const router = useRouter(); const { data: user, isLoading: isLoadingUser } = useUser(); const { glossaries, total, isLoading: isLoadingGlossaries, isCreating, - isUpdating, - isDeleting, isImportingTemplate, createGlossary, - updateGlossary, - deleteGlossary, importTemplate, } = useGlossaries(); const { toast } = useToast(); const { settings, updateSettings } = useTranslationStore(); const [createDialogOpen, setCreateDialogOpen] = useState(false); - const [editDialogOpen, setEditDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [selectedGlossary, setSelectedGlossary] = useState(null); - const [glossaryToDelete, setGlossaryToDelete] = useState<{ id: string; name: string } | null>(null); const [systemPrompt, setSystemPrompt] = useState(settings.systemPrompt); const [isSavingPrompt, setIsSavingPrompt] = useState(false); const [promptSaved, setPromptSaved] = useState(false); - const [creatingPreset, setCreatingPreset] = useState(null); - - const { glossary: fullGlossary, isLoading: isLoadingGlossaryDetail } = useGlossary( - selectedGlossary?.id || null - ); + const [searchQuery, setSearchQuery] = useState(''); const isPro = user?.tier === 'pro'; const isLoading = isLoadingUser || isLoadingGlossaries; - // Current translation target from store const currentTargetLang = settings.defaultTargetLanguage; const currentTargetInfo = SUPPORTED_LANGUAGES.find(l => l.code === currentTargetLang); - // Track whether prompt has unsaved changes const promptHasUnsavedChanges = systemPrompt !== settings.systemPrompt; const promptIsActive = !!settings.systemPrompt?.trim(); @@ -92,7 +63,9 @@ export default function GlossariesPage() { setPromptSaved(true); toast({ title: t('context.saved'), description: t('context.savedDesc') }); setTimeout(() => setPromptSaved(false), 3000); - } finally { setIsSavingPrompt(false); } + } finally { + setIsSavingPrompt(false); + } }; const handleClearPrompt = () => { @@ -100,56 +73,7 @@ export default function GlossariesPage() { setSystemPrompt(''); }; - const handleCreatePresetGlossary = async (preset: typeof PRESETS[0]) => { - setCreatingPreset(preset.key); - try { - const token = localStorage.getItem('token'); - if (!token) return; - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }; - const params = new URLSearchParams({ template_id: preset.templateId }); - const res = await fetch(`${API_BASE}/api/v1/glossaries/import?${params.toString()}`, { - method: 'POST', - headers, - }); - if (res.ok) { - const result = await res.json(); - const glossary = result.data; - toast({ - title: t('context.presets.created'), - description: t('context.presets.createdDesc', { - name: glossary?.name ?? t(preset.titleKey), - count: String(glossary?.terms?.length ?? 0), - }), - }); - } else if (res.status === 409) { - toast({ title: t('glossaries.presets.alreadyImported') }); - } else { - toast({ variant: 'destructive', title: t('glossaries.toast.error'), description: t('glossaries.toast.errorCreate') }); - } - } catch { - toast({ variant: 'destructive', title: t('glossaries.toast.error'), description: t('glossaries.toast.errorImport') }); - } finally { - setCreatingPreset(null); - } - }; - - const handleEditClick = (id: string) => { - const glossary = glossaries.find((g: GlossaryListItem) => g.id === id); - if (glossary) { - setSelectedGlossary(glossary); - setEditDialogOpen(true); - } - }; - - const handleDeleteClick = (id: string, name: string) => { - setGlossaryToDelete({ id, name }); - setDeleteDialogOpen(true); - }; - - const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => { + const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: { source: string; target: string; translations?: Record }[] }) => { try { await createGlossary(data); setCreateDialogOpen(false); @@ -187,43 +111,12 @@ export default function GlossariesPage() { } }; - const handleSaveGlossary = async (id: string, data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => { - try { - await updateGlossary(id, data); - setEditDialogOpen(false); - setSelectedGlossary(null); - toast({ - title: t('glossaries.toast.updated'), - description: t('glossaries.toast.updatedDesc', { name: data.name }), - }); - } catch (error) { - toast({ - variant: 'destructive', - title: t('glossaries.toast.error'), - description: t('glossaries.toast.errorUpdate'), - }); - throw error; - } - }; - - const handleDeleteConfirm = async () => { - if (!glossaryToDelete) return; - try { - await deleteGlossary(glossaryToDelete.id); - setDeleteDialogOpen(false); - setGlossaryToDelete(null); - toast({ - title: t('glossaries.toast.deleted'), - description: t('glossaries.toast.deletedDesc'), - }); - } catch (error) { - toast({ - variant: 'destructive', - title: t('glossaries.toast.error'), - description: t('glossaries.toast.errorDelete'), - }); - } - }; + // Filtered list (search) + const filteredGlossaries = useMemo(() => { + if (!searchQuery.trim()) return glossaries; + const q = searchQuery.toLowerCase(); + return glossaries.filter((g) => g.name.toLowerCase().includes(q)); + }, [glossaries, searchQuery]); if (isLoading) { return ( @@ -240,18 +133,6 @@ export default function GlossariesPage() { return ; } - const renderTitle = (title: string) => { - const lastSpaceIndex = title.lastIndexOf(' '); - if (lastSpaceIndex === -1) return title; - const firstPart = title.substring(0, lastSpaceIndex); - const lastWord = title.substring(lastSpaceIndex + 1); - return ( - <> - {firstPart} {lastWord} - - ); - }; - return (
@@ -270,15 +151,15 @@ export default function GlossariesPage() {
- {/* ── Comment ça marche ─────────────────────────────────── */} + {/* ── How it works ───────────────────────────────────────── */}
@@ -325,9 +206,8 @@ export default function GlossariesPage() {
- {/* ── System Prompt ────────────────────────────────────── */} + {/* ── System Prompt (Context) ─────────────────────────────── */}
- {/* Header with status badge */}
@@ -335,7 +215,6 @@ export default function GlossariesPage() { {t('context.instructions.title')}
- {/* Status badge */} {promptHasUnsavedChanges ? ( @@ -396,75 +275,9 @@ export default function GlossariesPage() {
- {/* ── Professional Presets ─────────────────────────────── */} -
-
- -

- {t('context.presets.title')} -

-
- - {/* Explanation box */} -
-

- {t('glossaries.presets.whatForBold')} {t('glossaries.presets.whatForDesc')} -

-
- -

- {t('glossaries.presets.clickHint')} -

-
-
- -
- {(() => { - const importedTemplateIds = new Set( - glossaries - .map((g: GlossaryListItem) => g.template_id) - .filter(Boolean) as string[] - ); - return PRESETS.map((p) => { - const Icon = p.icon; - const isCreatingThis = creatingPreset === p.key; - const alreadyImported = importedTemplateIds.has(p.templateId); - return ( - - ); - }); - })()} -
-
- - {/* ── Glossary Grid ──────────────────────────────────────── */} -
-
+ {/* ── Your Glossaries ────────────────────────────────────── */} +
+

{t('glossaries.grid.title')} {t('glossaries.grid.titleHighlight')} @@ -475,7 +288,7 @@ export default function GlossariesPage() { : t('glossaries.grid.emptyAction')}

-
+
{currentTargetInfo && ( {t('glossaries.grid.activeTranslation')} @@ -494,29 +307,57 @@ export default function GlossariesPage() {
+ {/* Search bar (only if more than 3 glossaries) */} + {glossaries.length > 3 && ( +
+ + setSearchQuery(e.target.value)} + placeholder={t('glossaries.grid.searchPlaceholder') || "Rechercher un glossaire…"} + className="w-full pl-9 pr-3 py-2.5 text-xs rounded-lg border border-black/5 dark:border-white/10 bg-white dark:bg-[#141414] focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30" + /> +
+ )} + {glossaries.length === 0 ? (

{t('glossaries.empty')}

-

{t('glossaries.emptyDesc')}

+

{t('glossaries.emptyDesc')}

+ +
+ ) : filteredGlossaries.length === 0 ? ( +
+

+ {t('glossaries.grid.noResults') || "Aucun résultat pour cette recherche."} +

) : ( -
- {glossaries.map((glossary: GlossaryListItem) => { +
+ {filteredGlossaries.map((glossary: GlossaryListItem) => { const termCount = glossary.terms_count ?? 0; const srcInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language); const tgtInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language); const isMultilingual = glossary.target_language === 'multi'; - // Does this glossary match the current translation target? const matchesTarget = currentTargetLang && (isMultilingual || glossary.target_language === currentTargetLang); const mismatch = currentTargetLang && !isMultilingual && glossary.target_language && glossary.target_language !== currentTargetLang; return ( -
router.push(`/dashboard/glossaries/${glossary.id}`)} className={cn( - 'editorial-card p-6 bg-white dark:bg-[#141414] border rounded-2xl shadow-sm group transition-all relative', + 'editorial-card p-6 bg-white dark:bg-[#141414] border rounded-2xl shadow-sm group transition-all relative text-left w-full', + 'hover:shadow-md hover:-translate-y-0.5 cursor-pointer', matchesTarget ? 'border-brand-accent/40 ring-1 ring-brand-accent/20 hover:border-brand-accent/60' : mismatch @@ -524,7 +365,6 @@ export default function GlossariesPage() { : 'border-black/5 dark:border-white/5 hover:border-brand-accent/30' )} > - {/* Match / mismatch badge */} {matchesTarget && (
{t('glossaries.badge.compatible')} @@ -564,45 +404,12 @@ export default function GlossariesPage() { {new Date(glossary.created_at).toLocaleDateString()}
- - {/* Action buttons — always visible, unambiguous */} -
- - -
-
+ ); })}
)} -
- - {/* ── About section ──────────────────────────────────────── */} -
-
- - - {t('glossaries.aboutTitle')} - -
-

{t('glossaries.aboutDesc')}

-

- {t('glossaries.aboutFormat')} -

-
+
{/* Dialogs */} @@ -614,27 +421,18 @@ export default function GlossariesPage() { isCreating={isCreating} isImportingTemplate={isImportingTemplate} /> - - {editDialogOpen && (fullGlossary || !isLoadingGlossaryDetail) && ( - { - setEditDialogOpen(open); - if (!open) setSelectedGlossary(null); - }} - glossary={fullGlossary} - onSave={handleSaveGlossary} - isSaving={isUpdating} - /> - )} - -
); } + +function renderTitle(title: string) { + const lastSpaceIndex = title.lastIndexOf(' '); + if (lastSpaceIndex === -1) return title; + const firstPart = title.substring(0, lastSpaceIndex); + const lastWord = title.substring(lastSpaceIndex + 1); + return ( + <> + {firstPart} {lastWord} + + ); +} diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 240aac1..4a47e58 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -408,6 +408,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Other target", "glossaries.card.editTerms": "Edit terms", "glossaries.card.delete": "Delete", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Webhook Integration", "apiKeys.webhook.descriptionBefore": "Pass a ", "apiKeys.webhook.descriptionAfter": " parameter to receive a POST request when your translation is complete.", @@ -1352,6 +1392,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Rechercher un glossaire…", + "glossaries.grid.noResults": "Aucun résultat pour cette recherche.", + "glossaries.detail.backToList": "Retour aux glossaires", + "glossaries.detail.save": "Enregistrer", + "glossaries.detail.savedTitle": "Enregistré", + "glossaries.detail.savedDesc": "Le glossaire a été mis à jour.", + "glossaries.detail.settingsTitle": "Paramètres", + "glossaries.detail.sourceLang": "Langue source", + "glossaries.detail.targetLang": "Langue cible", + "glossaries.detail.termsTitle": "Termes", + "glossaries.detail.terms": "termes", + "glossaries.detail.searchTerms": "Filtrer…", + "glossaries.detail.noTerms": "Aucun terme pour l'instant.", + "glossaries.detail.addFirstTerm": "Ajouter le premier terme", + "glossaries.detail.addTerm": "Ajouter un terme", + "glossaries.detail.maxReached": "Limite maximale atteinte", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Cible", + "glossaries.detail.sourcePlaceholder": "terme source", + "glossaries.detail.targetPlaceholder": "terme cible", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Exportez vos termes en CSV ou importez-en de nouveaux (remplace la liste actuelle).", + "glossaries.detail.export": "Exporter", + "glossaries.detail.import": "Importer", + "glossaries.detail.dangerTitle": "Zone danger", + "glossaries.detail.dangerDesc": "La suppression est définitive. Tous les termes associés seront perdus.", + "glossaries.detail.deleteGlossary": "Supprimer ce glossaire", + "glossaries.detail.confirmDelete": "Confirmer la suppression ?", + "glossaries.detail.confirm": "Confirmer", + "glossaries.detail.cancel": "Annuler", + "glossaries.detail.notFoundTitle": "Glossaire introuvable", + "glossaries.detail.notFoundDesc": "Ce glossaire n'existe pas ou vous n'y avez pas accès.", + "glossaries.detail.maxTermsTitle": "Limite atteinte", + "glossaries.detail.maxTermsDesc": "Maximum {max} termes par glossaire.", + "glossaries.detail.importEmptyTitle": "Fichier vide", + "glossaries.detail.importEmptyDesc": "Aucun terme détecté dans ce fichier.", + "glossaries.detail.importedTitle": "Importé", + "glossaries.detail.importedDesc": "{count} termes importés.", + "glossaries.detail.importErrorTitle": "Erreur de lecture", + "glossaries.detail.importErrorDesc": "Impossible de lire le fichier.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -2282,6 +2362,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -3167,6 +3287,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -4052,6 +4212,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -4937,6 +5137,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -5822,6 +6062,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -6707,6 +6987,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -7594,6 +7914,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -8478,6 +8838,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -9362,6 +9762,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -10204,6 +10644,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.", @@ -11055,6 +11535,46 @@ const messages: Record> = { "glossaries.badge.otherTarget": "Autre cible", "glossaries.card.editTerms": "Modifier les termes", "glossaries.card.delete": "Supprimer", + "glossaries.grid.searchPlaceholder": "Search a glossary…", + "glossaries.grid.noResults": "No results for this search.", + "glossaries.detail.backToList": "Back to glossaries", + "glossaries.detail.save": "Save", + "glossaries.detail.savedTitle": "Saved", + "glossaries.detail.savedDesc": "The glossary has been updated.", + "glossaries.detail.settingsTitle": "Settings", + "glossaries.detail.sourceLang": "Source language", + "glossaries.detail.targetLang": "Target language", + "glossaries.detail.termsTitle": "Terms", + "glossaries.detail.terms": "terms", + "glossaries.detail.searchTerms": "Filter…", + "glossaries.detail.noTerms": "No terms yet.", + "glossaries.detail.addFirstTerm": "Add the first term", + "glossaries.detail.addTerm": "Add a term", + "glossaries.detail.maxReached": "Maximum limit reached", + "glossaries.detail.source": "Source", + "glossaries.detail.target": "Target", + "glossaries.detail.sourcePlaceholder": "source term", + "glossaries.detail.targetPlaceholder": "target term", + "glossaries.detail.csvTitle": "CSV", + "glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).", + "glossaries.detail.export": "Export", + "glossaries.detail.import": "Import", + "glossaries.detail.dangerTitle": "Danger zone", + "glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.", + "glossaries.detail.deleteGlossary": "Delete this glossary", + "glossaries.detail.confirmDelete": "Confirm deletion?", + "glossaries.detail.confirm": "Confirm", + "glossaries.detail.cancel": "Cancel", + "glossaries.detail.notFoundTitle": "Glossary not found", + "glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.", + "glossaries.detail.maxTermsTitle": "Limit reached", + "glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.", + "glossaries.detail.importEmptyTitle": "Empty file", + "glossaries.detail.importEmptyDesc": "No terms detected in this file.", + "glossaries.detail.importedTitle": "Imported", + "glossaries.detail.importedDesc": "{count} terms imported.", + "glossaries.detail.importErrorTitle": "Read error", + "glossaries.detail.importErrorDesc": "Unable to read the file.", "apiKeys.webhook.title": "Intégration Webhook", "apiKeys.webhook.descriptionBefore": "Passez un paramètre ", "apiKeys.webhook.descriptionAfter": " pour recevoir une requête POST lorsque votre traduction est terminée.",