From 6da8a85b1dee52f0d26c50c80994b3314ce99517 Mon Sep 17 00:00:00 2001 From: sepehr Date: Mon, 1 Jun 2026 23:16:03 +0200 Subject: [PATCH] fix(admin): secure routes, add real IP detection, SMTP header validation, and fix Next.js layout hydration mismatch --- frontend/src/app/admin/layout.tsx | 6 + .../src/app/dashboard/glossaries/page.tsx | 80 +- .../dashboard/translate/GlossarySelector.tsx | 62 +- frontend/src/lib/i18n.tsx | 1027 +++++++++++++++++ routes/admin_routes.py | 42 +- routes/translate_routes.py | 3 +- tests/conftest.py | 17 +- tests/test_admin_logs.py | 2 +- tests/test_admin_tier_change.py | 14 +- tests/test_tier_rate_limit.py | 8 +- 10 files changed, 1165 insertions(+), 96 deletions(-) diff --git a/frontend/src/app/admin/layout.tsx b/frontend/src/app/admin/layout.tsx index 1de0977..9364f77 100644 --- a/frontend/src/app/admin/layout.tsx +++ b/frontend/src/app/admin/layout.tsx @@ -20,8 +20,10 @@ export default function AdminLayout({ const [isChecking, setIsChecking] = useState(true); const [isValid, setIsValid] = useState(false); const [persistHydrated, setPersistHydrated] = useState(false); + const [isMounted, setIsMounted] = useState(false); useEffect(() => { + setIsMounted(true); const unsub = useTranslationStore.persist.onFinishHydration(() => { setPersistHydrated(true); }); @@ -31,6 +33,10 @@ export default function AdminLayout({ return unsub; }, []); + if (!isMounted) { + return null; + } + const verifyToken = useCallback(async (token: string): Promise => { try { const response = await fetch(`${API_BASE}/api/v1/admin/verify`, { diff --git a/frontend/src/app/dashboard/glossaries/page.tsx b/frontend/src/app/dashboard/glossaries/page.tsx index 21a44dd..4700082 100644 --- a/frontend/src/app/dashboard/glossaries/page.tsx +++ b/frontend/src/app/dashboard/glossaries/page.tsx @@ -25,14 +25,14 @@ import { useTranslationStore } from '@/lib/store'; import { API_BASE } from '@/lib/config'; const PRESETS = [ - { key: 'it', title: 'IT / Logiciel', desc: 'Développement, infrastructure, DevOps', icon: Monitor, templateId: 'technology' }, - { key: 'legal', title: 'Juridique / Contrats', desc: 'Droit des affaires, contentieux', icon: Scale, templateId: 'legal' }, - { key: 'medical', title: 'Médical / Santé', desc: 'Pharmacologie, chirurgie, diagnostic', icon: Stethoscope, templateId: 'medical' }, - { key: 'finance', title: 'Finance / Comptabilité', desc: 'IFRS, bilans, fiscalité', icon: BarChart3, templateId: 'finance' }, - { key: 'marketing', title: 'Marketing / Publicité', desc: 'Digital, branding, analytics', icon: Megaphone, templateId: 'marketing' }, - { key: 'hr', title: 'RH / Ressources Humaines', desc: 'Contrats, politiques, recrutement', icon: Users, templateId: 'hr' }, - { key: 'scientific', title: 'Scientifique / Recherche', desc: 'Publications, thèses, articles', icon: FlaskConical, templateId: 'scientific' }, - { key: 'ecommerce', title: 'E-commerce / Vente', desc: 'Boutiques en ligne, catalogues, CRM', icon: ShoppingCart, templateId: 'ecommerce' }, + { 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() { @@ -120,7 +120,7 @@ export default function GlossariesPage() { toast({ title: t('context.presets.created'), description: t('context.presets.createdDesc', { - name: glossary?.name ?? preset.title, + name: glossary?.name ?? t(preset.titleKey), count: String(glossary?.terms?.length ?? 0), }), }); @@ -228,7 +228,7 @@ export default function GlossariesPage() {
-

Chargement...

+

{t('glossaries.loading')}

); @@ -280,16 +280,16 @@ export default function GlossariesPage() {
- Comment ces paramètres sont utilisés + {t('glossaries.howItWorks.title')}
{/* Step 1 */}
1
-

Configurez ici

+

{t('glossaries.howItWorks.step1Title')}

- Rédigez vos instructions de contexte ou créez/importez un glossaire de termes. + {t('glossaries.howItWorks.step1Desc')}

@@ -301,22 +301,22 @@ export default function GlossariesPage() {
2
-

Activez dans Traduire

+

{t('glossaries.howItWorks.step2Title')}

- Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire. + {t('glossaries.howItWorks.step2Desc')}

- ⚠️ Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire. + {t('glossaries.howItWorks.warning')}

- Aller à Traduire + {t('glossaries.howItWorks.goToTranslate')}
@@ -337,16 +337,16 @@ export default function GlossariesPage() { {promptHasUnsavedChanges ? ( - Non enregistré + {t('glossaries.status.unsaved')} ) : promptIsActive ? ( - Actif · s'applique à toutes les traductions IA + {t('glossaries.status.active')} ) : ( - Inactif + {t('glossaries.status.inactive')} )} @@ -354,10 +354,10 @@ export default function GlossariesPage() { {/* Explanation box */}

- À quoi ça sert ? Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale. + {t('glossaries.instructions.whatForBold')} {t('glossaries.instructions.whatForDesc')}

- Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. » + {t('glossaries.instructions.example')}

@@ -369,14 +369,14 @@ export default function GlossariesPage() { />

- {systemPrompt.length > 0 ? `${systemPrompt.length} caractères` : 'Vide — aucune instruction envoyée à l\'IA'} + {systemPrompt.length > 0 ? t('glossaries.instructions.charCount', { count: systemPrompt.length }) : t('glossaries.instructions.emptyHint')}

@@ -406,12 +406,12 @@ export default function GlossariesPage() { {/* Explanation box */}

- À quoi ça sert ? Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis. + {t('glossaries.presets.whatForBold')} {t('glossaries.presets.whatForDesc')}

- Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire + {t('glossaries.presets.clickHint')}

@@ -431,14 +431,14 @@ export default function GlossariesPage() {
{isCreatingThis ? : }
- {isCreatingThis && Création…} + {isCreatingThis && {t('glossaries.presets.creating')}}

- {p.title} + {t(p.titleKey)}

- {p.desc} + {t(p.descKey)}

@@ -452,18 +452,18 @@ export default function GlossariesPage() {

- Vos glossaires + {t('glossaries.grid.title')} {t('glossaries.grid.titleHighlight')}

{glossaries.length > 0 - ? `${glossaries.length} glossaire${glossaries.length > 1 ? 's' : ''} — cliquez sur une carte pour la modifier` - : 'Créez votre premier glossaire ou importez un preset ci-dessus'} + ? t('glossaries.grid.countWithAction', { count: glossaries.length, plural: glossaries.length > 1 ? 's' : '' }) + : t('glossaries.grid.emptyAction')}

{currentTargetInfo && ( - Traduction active : + {t('glossaries.grid.activeTranslation')} {currentTargetInfo.flag} {currentTargetInfo.label} )} @@ -473,7 +473,7 @@ export default function GlossariesPage() { className="flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline shrink-0" > - Aller à Traduire pour activer + {t('glossaries.grid.goToTranslate')} )}
@@ -512,12 +512,12 @@ export default function GlossariesPage() { {/* Match / mismatch badge */} {matchesTarget && (
- Compatible + {t('glossaries.badge.compatible')}
)} {mismatch && (
- Autre cible + {t('glossaries.badge.otherTarget')}
)} @@ -556,7 +556,7 @@ export default function GlossariesPage() { onClick={() => handleEditClick(glossary.id)} className="flex-1 py-2 px-3 rounded-lg bg-brand-muted/60 dark:bg-white/5 hover:bg-brand-accent/10 dark:hover:bg-brand-accent/15 text-brand-dark/70 dark:text-white/60 hover:text-brand-accent text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer flex items-center justify-center gap-1.5" > - Modifier les termes + {t('glossaries.card.editTerms')}
diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index c45f4c8..45d9c26 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -162,10 +162,10 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary setIsOpen(false); } else { const errData = await res.json().catch(() => null); - setError(errData?.message || `Import failed (${res.status})`); + setError(errData?.message || t('translate.glossary.importFailed').replace('{status}', String(res.status))); } } catch { - setError('Network error'); + setError(t('translate.glossary.networkError')); } finally { setImportingId(null); } @@ -217,10 +217,10 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary fetchData(); } else { const errData = await res.json().catch(() => null); - setError(errData?.message || 'Erreur lors de l\'ajout du terme'); + setError(errData?.message || t('translate.glossary.addTermError')); } } catch { - setError('Erreur réseau'); + setError(t('translate.glossary.networkError')); } finally { setIsAddingTerm(false); } @@ -316,7 +316,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {mode === 'classic' ? (
- Moteur neutre sans glossaire (IA uniquement) + {t('translate.glossary.classicMode') || 'Moteur neutre sans glossaire (IA uniquement)'}
) : !isPro ? ( @@ -330,7 +330,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {/* Help Info text */}

- Le glossaire force la traduction de termes précis. Choisissez un glossaire dont la langue source correspond à la langue d'origine de votre document. + {t('translate.glossary.helpText') || 'Le glossaire force la traduction de termes précis. Choisissez un glossaire dont la langue source correspond à la langue d\'origine de votre document.'}

{/* Mismatch Warning — source language */} @@ -338,7 +338,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
⚠️ - Attention : Ce glossaire utilise la langue source {getFlag(selected.source_language)} {selected.source_language.toUpperCase()}, mais votre document est configuré en {getFlag(sourceLang)} {sourceLang.toUpperCase()}. + {t('translate.glossary.sourceWarning') || 'Attention :'} Ce glossaire utilise la langue source {getFlag(selected.source_language)} {selected.source_language.toUpperCase()}, {t('translate.glossary.sourceWarningBut') || 'mais votre document est configuré en'} {getFlag(sourceLang)} {sourceLang.toUpperCase()}.
)} @@ -348,7 +348,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
🎯 - Incompatibilité de cible : Ce glossaire est prévu pour traduire vers {getFlag(selected.target_language)} {selected.target_language.toUpperCase()}, mais votre document cible {targetFlag} {targetLang.toUpperCase()}. Les termes risquent de ne pas être pertinents. + {t('translate.glossary.targetWarning') || 'Incompatibilité de cible :'} {getFlag(selected.target_language)} {selected.target_language.toUpperCase()}, {t('translate.glossary.targetWarningBut') || 'mais votre document cible'} {targetFlag} {targetLang.toUpperCase()}. {t('translate.glossary.targetWarningEnd') || 'Les termes risquent de ne pas être pertinents.'}
)} @@ -367,12 +367,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary >
- {selected ? selected.name : (isLoading ? "Chargement..." : "Sélectionner un glossaire...")} + {selected ? selected.name : (isLoading ? t('translate.glossary.loading') || 'Chargement...' : t('translate.glossary.selectPlaceholder') || 'Sélectionner un glossaire...')} {selected - ? `${getFlag(selected.source_language)} ➜ ${selected.target_language === 'multi' ? '🌐 MULTILINGUE' : getFlag(selected.target_language || targetLang)} • ${selected.terms_count} termes` - : (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? "Sélectionnez un glossaire" : "Aucun glossaire disponible") + ? `${getFlag(selected.source_language)} ➜ ${selected.target_language === 'multi' ? `🌐 ${t('translate.glossary.multilingual') || 'MULTILINGUE'}` : getFlag(selected.target_language || targetLang)} • ${selected.terms_count} ${t('translate.glossary.terms') || 'termes'}` + : (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? t('translate.glossary.select') || 'Sélectionnez un glossaire' : t('translate.glossary.noGlossaryAvailable') || 'Aucun glossaire disponible') }
@@ -391,7 +391,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {sourceLang !== 'auto' && (glossaries.length > 0 || templates.length > 0) && (
- Filtrer par langue ({sourceFlag}) + {t('translate.glossary.filterByLang') || 'Filtrer par langue'} ({sourceFlag})
)} @@ -410,7 +410,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {filteredGlossaries.length > 0 && (
- Mes Glossaires + {t('translate.glossary.myGlossaries') || 'Mes Glossaires'}
{filteredGlossaries.map(g => { const flag = getFlag(g.source_language); @@ -433,7 +433,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{g.name} - {flag} ➜ {g.target_language === 'multi' ? '🌐 MULTILINGUE' : getFlag(g.target_language || targetLang)} • {g.terms_count} termes + {flag} ➜ {g.target_language === 'multi' ? `🌐 ${t('translate.glossary.multilingual') || 'MULTILINGUE'}` : getFlag(g.target_language || targetLang)} • {g.terms_count} {t('translate.glossary.terms') || 'termes'}
{isSelected && } @@ -447,7 +447,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {filteredTemplates.length > 0 && (
- Modèles disponibles + {t('translate.glossary.availableTemplates') || 'Modèles disponibles'}
{filteredTemplates.map(tmpl => { @@ -457,7 +457,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary ); const isAlreadySelected = existingGlossary?.id === glossaryId; const flag = getFlag(tmpl.source_lang); - const tFlag = tmpl.target_lang === 'multi' ? '🌐 MULTILINGUE' : getFlag(tmpl.target_lang); + const tFlag = tmpl.target_lang === 'multi' ? `🌐 ${t('translate.glossary.multilingual') || 'MULTILINGUE'}` : getFlag(tmpl.target_lang); return ( )}
@@ -533,16 +533,16 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
- Aperçu des correspondances actives : + {t('translate.glossary.activePreview') || 'Aperçu des correspondances actives :'} - {selectedGlossaryDetail?.terms?.length || selected.terms_count} au total + {selectedGlossaryDetail?.terms?.length || selected.terms_count} {t('translate.glossary.total') || 'au total'}
{isLoadingDetail ? (
- Chargement... + {t('translate.glossary.loading') || 'Chargement...'}
) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? (
@@ -560,12 +560,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary })} {selectedGlossaryDetail.terms.length > 4 && ( - + {selectedGlossaryDetail.terms.length - 4} autres termes + + {selectedGlossaryDetail.terms.length - 4} {t('translate.glossary.moreTerms') || 'autres termes'} )}
) : ( -

Aucun terme dans ce glossaire.

+

{t('translate.glossary.noTerms') || 'Aucun terme dans ce glossaire.'}

)}
)} @@ -576,7 +576,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary setNewSource(e.target.value)} disabled={isAddingTerm || disabled} @@ -586,7 +586,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary setNewTarget(e.target.value)} disabled={isAddingTerm || disabled} @@ -596,7 +596,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary type="submit" disabled={isAddingTerm || disabled || !newSource.trim() || !newTarget.trim()} className="px-3 bg-brand-dark dark:bg-white text-white dark:text-brand-dark rounded-lg flex items-center justify-center disabled:opacity-35 transition-colors cursor-pointer shrink-0" - title="Ajouter le terme" + title={t('translate.glossary.addTerm') || 'Ajouter le terme'} > {isAddingTerm ? ( @@ -610,7 +610,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
) : (
- Moteur neutre sans glossaire appliqué + {t('translate.glossary.disabledMode') || 'Moteur neutre sans glossaire appliqué'}
)}
diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index b0e1fcc..657f21b 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -358,6 +358,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": " feature. Upgrade to unlock custom terminology.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Upgrade to Pro", + "glossaries.loading": "Loading...", + "glossaries.howItWorks.title": "How these settings are used", + "glossaries.howItWorks.step1Title": "Configure here", + "glossaries.howItWorks.step1Desc": "Write your context instructions or create/import a glossary of terms.", + "glossaries.howItWorks.step2Title": "Activate in Translate", + "glossaries.howItWorks.step2Desc": "On the translation page, in the right column, select your glossary.", + "glossaries.howItWorks.warning": "Context instructions apply automatically to all your AI translations once saved. Glossaries must be manually selected on the Translate page.", + "glossaries.howItWorks.goToTranslate": "Go to Translate", + "glossaries.status.unsaved": "Unsaved", + "glossaries.status.active": "Active · applies to all AI translations", + "glossaries.status.inactive": "Inactive", + "glossaries.instructions.whatForBold": "What is it for?", + "glossaries.instructions.whatForDesc": "These instructions are automatically sent to the AI before each translation, without you having to do anything on the Translate page. Use them to guide the style, register or general terminology.", + "glossaries.instructions.example": "Example: \"You translate financial reports. Be formal, precise and keep all figures.\"", + "glossaries.instructions.charCount": "{count} characters", + "glossaries.instructions.emptyHint": "Empty — no instructions sent to the AI", + "glossaries.instructions.clearAll": "Clear all", + "glossaries.instructions.saving": "Saving...", + "glossaries.instructions.saved": "Saved", + "glossaries.presets.whatForBold": "What is it for?", + "glossaries.presets.whatForDesc": "Clicking a card creates a pre-filled glossary with domain-specific terms. This glossary will appear in your glossaries below, and you can manually select it on the Translate page to force precise term translations.", + "glossaries.presets.clickHint": "Click a card → glossary created → select it in Translate", + "glossaries.presets.creating": "Creating...", + "glossaries.presets.it.title": "IT / Software", + "glossaries.presets.it.desc": "Development, infrastructure, DevOps", + "glossaries.presets.legal.title": "Legal / Contracts", + "glossaries.presets.legal.desc": "Business law, litigation", + "glossaries.presets.medical.title": "Medical / Health", + "glossaries.presets.medical.desc": "Pharmacology, surgery, diagnosis", + "glossaries.presets.finance.title": "Finance / Accounting", + "glossaries.presets.finance.desc": "IFRS, balance sheets, taxation", + "glossaries.presets.marketing.title": "Marketing / Advertising", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "HR / Human Resources", + "glossaries.presets.hr.desc": "Contracts, policies, recruitment", + "glossaries.presets.scientific.title": "Scientific / Research", + "glossaries.presets.scientific.desc": "Publications, theses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Sales", + "glossaries.presets.ecommerce.desc": "Online stores, catalogs, CRM", + "glossaries.grid.title": "Your", + "glossaries.grid.titleHighlight": "glossaries", + "glossaries.grid.countWithAction": "{count} glossary({plural}) — click a card to edit", + "glossaries.grid.emptyAction": "Create your first glossary or import a preset above", + "glossaries.grid.activeTranslation": "Active translation:", + "glossaries.grid.goToTranslate": "Go to Translate to activate", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Other target", + "glossaries.card.editTerms": "Edit terms", + "glossaries.card.delete": "Delete", "apiKeys.webhook.title": "Webhook Integration", "apiKeys.webhook.descriptionBefore": "Pass a ", "apiKeys.webhook.descriptionAfter": " parameter to receive a POST request when your translation is complete.", @@ -800,6 +849,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "No glossary for", "translate.glossary.noGlossaries": "No glossaries yet", "translate.glossary.loading": "Loading...", + "translate.glossary.classicMode": "Neutral engine without glossary (AI only)", + "translate.glossary.selectPlaceholder": "Select a glossary...", + "translate.glossary.multilingual": "MULTILINGUAL", + "translate.glossary.noGlossaryAvailable": "No glossary available", + "translate.glossary.filterByLang": "Filter by language", + "translate.glossary.active": "Active", + "translate.glossary.inactive": "Inactive", + "translate.glossary.availableTemplates": "Available templates", + "translate.glossary.importing": "Importing...", + "translate.glossary.imported": "(Imported)", + "translate.glossary.noGlossaryForSource": "No glossary or template for source language", + "translate.glossary.createGlossary": "Create a glossary", + "translate.glossary.showAll": "Show all glossaries", + "translate.glossary.activePreview": "Active matches preview:", + "translate.glossary.total": "total", + "translate.glossary.moreTerms": "more terms", + "translate.glossary.noTerms": "No terms in this glossary.", + "translate.glossary.sourceTerm": "Source term", + "translate.glossary.translation": "Translation", + "translate.glossary.addTerm": "Add term", + "translate.glossary.disabledMode": "Neutral engine without glossary applied", + "translate.glossary.addTermError": "Error adding term", + "translate.glossary.networkError": "Network error", + "translate.glossary.importFailed": "Import failed ({status})", + "translate.glossary.helpText": "The glossary forces precise term translation. Choose a glossary whose source language matches your document's original language.", + "translate.glossary.sourceWarning": "Warning: This glossary uses source language", + "translate.glossary.sourceWarningBut": "but your document is configured in", + "translate.glossary.targetWarning": "Target mismatch: This glossary is designed to translate to", + "translate.glossary.targetWarningBut": "but your document targets", + "translate.glossary.targetWarningEnd": "Terms may not be relevant.", "context.presets.createGlossary": "Create glossary", "context.presets.created": "Glossary created", "context.presets.createdDesc": "The glossary \"{name}\" has been created with {count} terms.", @@ -1222,6 +1301,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -1666,6 +1794,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "Aucun glossaire pour", "translate.glossary.noGlossaries": "Aucun glossaire", "translate.glossary.loading": "Chargement...", + "translate.glossary.classicMode": "Moteur neutre sans glossaire (IA uniquement)", + "translate.glossary.selectPlaceholder": "Sélectionner un glossaire...", + "translate.glossary.multilingual": "MULTILINGUE", + "translate.glossary.noGlossaryAvailable": "Aucun glossaire disponible", + "translate.glossary.filterByLang": "Filtrer par langue", + "translate.glossary.active": "Actif", + "translate.glossary.inactive": "Inactif", + "translate.glossary.availableTemplates": "Modèles disponibles", + "translate.glossary.importing": "Importation...", + "translate.glossary.imported": "(Importé)", + "translate.glossary.noGlossaryForSource": "Aucun glossaire ni modèle pour la langue source", + "translate.glossary.createGlossary": "Créer un glossaire", + "translate.glossary.showAll": "Afficher tous les glossaires", + "translate.glossary.activePreview": "Aperçu des correspondances actives :", + "translate.glossary.total": "au total", + "translate.glossary.moreTerms": "autres termes", + "translate.glossary.noTerms": "Aucun terme dans ce glossaire.", + "translate.glossary.sourceTerm": "Terme Source", + "translate.glossary.translation": "Traduction", + "translate.glossary.addTerm": "Ajouter le terme", + "translate.glossary.disabledMode": "Moteur neutre sans glossaire appliqué", + "translate.glossary.addTermError": "Erreur lors de l'ajout du terme", + "translate.glossary.networkError": "Erreur réseau", + "translate.glossary.importFailed": "Échec de l'importation ({status})", + "translate.glossary.helpText": "Le glossaire force la traduction de termes précis. Choisissez un glossaire dont la langue source correspond à la langue d'origine de votre document.", + "translate.glossary.sourceWarning": "Attention : Ce glossaire utilise la langue source", + "translate.glossary.sourceWarningBut": "mais votre document est configuré en", + "translate.glossary.targetWarning": "Incompatibilité de cible : Ce glossaire est prévu pour traduire vers", + "translate.glossary.targetWarningBut": "mais votre document cible", + "translate.glossary.targetWarningEnd": "Les termes risquent de ne pas être pertinents.", "context.presets.createGlossary": "Créer le glossaire", "context.presets.created": "Glossaire créé", "context.presets.createdDesc": "Le glossaire \"{name}\" a été créé avec {count} termes.", @@ -2072,6 +2230,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -2473,6 +2680,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "Sin glosario para", "translate.glossary.noGlossaries": "Sin glosarios", "translate.glossary.loading": "Cargando...", + "translate.glossary.classicMode": "Motor neutro sin glosario (solo IA)", + "translate.glossary.selectPlaceholder": "Seleccionar un glosario...", + "translate.glossary.multilingual": "MULTILINGÜE", + "translate.glossary.noGlossaryAvailable": "Ningún glosario disponible", + "translate.glossary.filterByLang": "Filtrar por idioma", + "translate.glossary.active": "Activo", + "translate.glossary.inactive": "Inactivo", + "translate.glossary.availableTemplates": "Plantillas disponibles", + "translate.glossary.importing": "Importando...", + "translate.glossary.imported": "(Importado)", + "translate.glossary.noGlossaryForSource": "Ningún glosario ni plantilla para el idioma de origen", + "translate.glossary.createGlossary": "Crear un glosario", + "translate.glossary.showAll": "Mostrar todos los glosarios", + "translate.glossary.activePreview": "Vista previa de correspondencias activas:", + "translate.glossary.total": "en total", + "translate.glossary.moreTerms": "términos más", + "translate.glossary.noTerms": "Ningún término en este glosario.", + "translate.glossary.sourceTerm": "Término fuente", + "translate.glossary.translation": "Traducción", + "translate.glossary.addTerm": "Añadir término", + "translate.glossary.disabledMode": "Motor neutro sin glosario aplicado", + "translate.glossary.addTermError": "Error al añadir el término", + "translate.glossary.networkError": "Error de red", + "translate.glossary.importFailed": "Error al importar ({status})", + "translate.glossary.helpText": "El glosario fuerza la traducción de términos precisos. Elija un glosario cuyo idioma de origen coincida con el idioma original de su documento.", + "translate.glossary.sourceWarning": "Atención: Este glosario usa el idioma de origen", + "translate.glossary.sourceWarningBut": "pero su documento está configurado en", + "translate.glossary.targetWarning": "Incompatibilidad de destino: Este glosario está diseñado para traducir a", + "translate.glossary.targetWarningBut": "pero su documento tiene como destino", + "translate.glossary.targetWarningEnd": "Los términos pueden no ser relevantes.", "context.presets.createGlossary": "Crear glosario", "context.presets.created": "Glosario creado", "context.presets.createdDesc": "El glosario \"{name}\" ha sido creado con {count} términos.", @@ -2877,6 +3114,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -3278,6 +3564,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "Kein Glossar für", "translate.glossary.noGlossaries": "Keine Glossare", "translate.glossary.loading": "Laden...", + "translate.glossary.classicMode": "Neutraler Motor ohne Glossar (nur KI)", + "translate.glossary.selectPlaceholder": "Glossar auswählen...", + "translate.glossary.multilingual": "MEHRSPRACHIG", + "translate.glossary.noGlossaryAvailable": "Kein Glossar verfügbar", + "translate.glossary.filterByLang": "Nach Sprache filtern", + "translate.glossary.active": "Aktiv", + "translate.glossary.inactive": "Inaktiv", + "translate.glossary.availableTemplates": "Verfügbare Vorlagen", + "translate.glossary.importing": "Importiere...", + "translate.glossary.imported": "(Importiert)", + "translate.glossary.noGlossaryForSource": "Kein Glossar oder Vorlage für Ausgangssprache", + "translate.glossary.createGlossary": "Glossar erstellen", + "translate.glossary.showAll": "Alle Glossare anzeigen", + "translate.glossary.activePreview": "Vorschau aktiver Zuordnungen:", + "translate.glossary.total": "gesamt", + "translate.glossary.moreTerms": "weitere Begriffe", + "translate.glossary.noTerms": "Keine Begriffe in diesem Glossar.", + "translate.glossary.sourceTerm": "Quellbegriff", + "translate.glossary.translation": "Übersetzung", + "translate.glossary.addTerm": "Begriff hinzufügen", + "translate.glossary.disabledMode": "Neutraler Motor ohne angewandtes Glossar", + "translate.glossary.addTermError": "Fehler beim Hinzufügen des Begriffs", + "translate.glossary.networkError": "Netzwerkfehler", + "translate.glossary.importFailed": "Import fehlgeschlagen ({status})", + "translate.glossary.helpText": "Das Glossar erzwingt die präzise Übersetzung von Begriffen. Wählen Sie ein Glossar, dessen Ausgangssprache der Originalsprache Ihres Dokuments entspricht.", + "translate.glossary.sourceWarning": "Achtung: Dieses Glossar verwendet die Ausgangssprache", + "translate.glossary.sourceWarningBut": "aber Ihr Dokument ist konfiguriert auf", + "translate.glossary.targetWarning": "Ziel-Inkompatibilität: Dieses Glossar ist für die Übersetzung in", + "translate.glossary.targetWarningBut": "aber Ihr Dokument zielt auf", + "translate.glossary.targetWarningEnd": "Die Begriffe sind möglicherweise nicht relevant.", "context.presets.createGlossary": "Glossar erstellen", "context.presets.created": "Glossar erstellt", "context.presets.createdDesc": "Das Glossar \"{name}\" wurde mit {count} Begriffen erstellt.", @@ -3682,6 +3998,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -4083,6 +4448,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "Sem glossário para", "translate.glossary.noGlossaries": "Sem glossários", "translate.glossary.loading": "Carregando...", + "translate.glossary.classicMode": "Motor neutro sem glossário (apenas IA)", + "translate.glossary.selectPlaceholder": "Selecionar um glossário...", + "translate.glossary.multilingual": "MULTILÍNGUE", + "translate.glossary.noGlossaryAvailable": "Nenhum glossário disponível", + "translate.glossary.filterByLang": "Filtrar por idioma", + "translate.glossary.active": "Ativo", + "translate.glossary.inactive": "Inativo", + "translate.glossary.availableTemplates": "Modelos disponíveis", + "translate.glossary.importing": "Importando...", + "translate.glossary.imported": "(Importado)", + "translate.glossary.noGlossaryForSource": "Nenhum glossário ou modelo para o idioma de origem", + "translate.glossary.createGlossary": "Criar um glossário", + "translate.glossary.showAll": "Mostrar todos os glossários", + "translate.glossary.activePreview": "Pré-visualização de correspondências ativas:", + "translate.glossary.total": "no total", + "translate.glossary.moreTerms": "termos adicionais", + "translate.glossary.noTerms": "Nenhum termo neste glossário.", + "translate.glossary.sourceTerm": "Termo de origem", + "translate.glossary.translation": "Tradução", + "translate.glossary.addTerm": "Adicionar termo", + "translate.glossary.disabledMode": "Motor neutro sem glossário aplicado", + "translate.glossary.addTermError": "Erro ao adicionar o termo", + "translate.glossary.networkError": "Erro de rede", + "translate.glossary.importFailed": "Falha na importação ({status})", + "translate.glossary.helpText": "O glossário força a tradução precisa de termos. Escolha um glossário cujo idioma de origem corresponda ao idioma original do seu documento.", + "translate.glossary.sourceWarning": "Atenção: Este glossário usa o idioma de origem", + "translate.glossary.sourceWarningBut": "mas o seu documento está configurado em", + "translate.glossary.targetWarning": "Incompatibilidade de destino: Este glossário foi projetado para traduzir para", + "translate.glossary.targetWarningBut": "mas o seu documento tem como destino", + "translate.glossary.targetWarningEnd": "Os termos podem não ser relevantes.", "context.presets.createGlossary": "Criar glossário", "context.presets.created": "Glossário criado", "context.presets.createdDesc": "O glossário \"{name}\" foi criado com {count} termos.", @@ -4487,6 +4882,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -4888,6 +5332,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "Nessun glossario per", "translate.glossary.noGlossaries": "Nessun glossario", "translate.glossary.loading": "Caricamento...", + "translate.glossary.classicMode": "Motore neutro senza glossario (solo IA)", + "translate.glossary.selectPlaceholder": "Selezionare un glossario...", + "translate.glossary.multilingual": "MULTILINGUE", + "translate.glossary.noGlossaryAvailable": "Nessun glossario disponibile", + "translate.glossary.filterByLang": "Filtra per lingua", + "translate.glossary.active": "Attivo", + "translate.glossary.inactive": "Inattivo", + "translate.glossary.availableTemplates": "Modelli disponibili", + "translate.glossary.importing": "Importazione...", + "translate.glossary.imported": "(Importato)", + "translate.glossary.noGlossaryForSource": "Nessun glossario o modello per la lingua di origine", + "translate.glossary.createGlossary": "Crea un glossario", + "translate.glossary.showAll": "Mostra tutti i glossari", + "translate.glossary.activePreview": "Anteprima delle corrispondenze attive:", + "translate.glossary.total": "in totale", + "translate.glossary.moreTerms": "altri termini", + "translate.glossary.noTerms": "Nessun termine in questo glossario.", + "translate.glossary.sourceTerm": "Termine sorgente", + "translate.glossary.translation": "Traduzione", + "translate.glossary.addTerm": "Aggiungi termine", + "translate.glossary.disabledMode": "Motore neutro senza glossario applicato", + "translate.glossary.addTermError": "Errore durante l'aggiunta del termine", + "translate.glossary.networkError": "Errore di rete", + "translate.glossary.importFailed": "Importazione fallita ({status})", + "translate.glossary.helpText": "Il glossario forza la traduzione precisa dei termini. Scegli un glossario la cui lingua sorgente corrisponde alla lingua originale del documento.", + "translate.glossary.sourceWarning": "Attenzione: Questo glossario utilizza la lingua sorgente", + "translate.glossary.sourceWarningBut": "ma il documento è configurato in", + "translate.glossary.targetWarning": "Incompatibilità di destinazione: Questo glossario è progettato per tradurre verso", + "translate.glossary.targetWarningBut": "ma il documento ha come destinazione", + "translate.glossary.targetWarningEnd": "I termini potrebbero non essere pertinenti.", "context.presets.createGlossary": "Crea glossario", "context.presets.created": "Glossario creato", "context.presets.createdDesc": "Il glossario \"{name}\" è stato creato con {count} termini.", @@ -5292,6 +5766,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -5693,6 +6216,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "Geen woordenlijst voor", "translate.glossary.noGlossaries": "Geen woordenlijsten", "translate.glossary.loading": "Laden...", + "translate.glossary.classicMode": "Neutrale motor zonder woordenlijst (alleen AI)", + "translate.glossary.selectPlaceholder": "Woordenlijst selecteren...", + "translate.glossary.multilingual": "MEERTALIG", + "translate.glossary.noGlossaryAvailable": "Geen woordenlijst beschikbaar", + "translate.glossary.filterByLang": "Filteren op taal", + "translate.glossary.active": "Actief", + "translate.glossary.inactive": "Inactief", + "translate.glossary.availableTemplates": "Beschikbare sjablonen", + "translate.glossary.importing": "Bezig met importeren...", + "translate.glossary.imported": "(Geïmporteerd)", + "translate.glossary.noGlossaryForSource": "Geen woordenlijst of sjabloon voor brontaal", + "translate.glossary.createGlossary": "Woordenlijst maken", + "translate.glossary.showAll": "Alle woordenlijsten tonen", + "translate.glossary.activePreview": "Voorbeeld van actieve overeenkomsten:", + "translate.glossary.total": "totaal", + "translate.glossary.moreTerms": "meer termen", + "translate.glossary.noTerms": "Geen termen in deze woordenlijst.", + "translate.glossary.sourceTerm": "Bronterm", + "translate.glossary.translation": "Vertaling", + "translate.glossary.addTerm": "Term toevoegen", + "translate.glossary.disabledMode": "Neutrale motor zonder toegepaste woordenlijst", + "translate.glossary.addTermError": "Fout bij toevoegen van term", + "translate.glossary.networkError": "Netwerkfout", + "translate.glossary.importFailed": "Importeren mislukt ({status})", + "translate.glossary.helpText": "De woordenlijst dwingt nauwkeurige termvertaling af. Kies een woordenlijst waarvan de brontaal overeenkomt met de oorspronkelijke taal van uw document.", + "translate.glossary.sourceWarning": "Let op: Deze woordenlijst gebruikt brontaal", + "translate.glossary.sourceWarningBut": "maar uw document is geconfigureerd in", + "translate.glossary.targetWarning": "Doel incompatibiliteit: Deze woordenlijst is ontworpen om te vertalen naar", + "translate.glossary.targetWarningBut": "maar uw document is gericht op", + "translate.glossary.targetWarningEnd": "De termen zijn mogelijk niet relevant.", "context.presets.createGlossary": "Woordenlijst maken", "context.presets.created": "Woordenlijst gemaakt", "context.presets.createdDesc": "De woordenlijst \"{name}\" is gemaakt met {count} termen.", @@ -6097,6 +6650,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -6500,6 +7102,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "Нет глоссария для", "translate.glossary.noGlossaries": "Нет глоссариев", "translate.glossary.loading": "Загрузка...", + "translate.glossary.classicMode": "Нейтральный движок без глоссария (только ИИ)", + "translate.glossary.selectPlaceholder": "Выбрать глоссарий...", + "translate.glossary.multilingual": "МУЛЬТИЯЗЫЧНЫЙ", + "translate.glossary.noGlossaryAvailable": "Нет доступных глоссариев", + "translate.glossary.filterByLang": "Фильтр по языку", + "translate.glossary.active": "Активно", + "translate.glossary.inactive": "Неактивно", + "translate.glossary.availableTemplates": "Доступные шаблоны", + "translate.glossary.importing": "Импорт...", + "translate.glossary.imported": "(Импортировано)", + "translate.glossary.noGlossaryForSource": "Нет глоссария или шаблона для исходного языка", + "translate.glossary.createGlossary": "Создать глоссарий", + "translate.glossary.showAll": "Показать все глоссарии", + "translate.glossary.activePreview": "Предпросмотр активных совпадений:", + "translate.glossary.total": "всего", + "translate.glossary.moreTerms": "других терминов", + "translate.glossary.noTerms": "Нет терминов в этом глоссарии.", + "translate.glossary.sourceTerm": "Исходный термин", + "translate.glossary.translation": "Перевод", + "translate.glossary.addTerm": "Добавить термин", + "translate.glossary.disabledMode": "Нейтральный движок без применённого глоссария", + "translate.glossary.addTermError": "Ошибка при добавлении термина", + "translate.glossary.networkError": "Сетевая ошибка", + "translate.glossary.importFailed": "Ошибка импорта ({status})", + "translate.glossary.helpText": "Глоссарий обеспечивает точный перевод терминов. Выберите глоссарий, исходный язык которого соответствует оригинальному языку вашего документа.", + "translate.glossary.sourceWarning": "Внимание: Этот глоссарий использует исходный язык", + "translate.glossary.sourceWarningBut": "но ваш документ настроен на", + "translate.glossary.targetWarning": "Несовместимость цели: Этот глоссарий предназначен для перевода на", + "translate.glossary.targetWarningBut": "но ваш документ нацелен на", + "translate.glossary.targetWarningEnd": "Термины могут быть нерелевантны.", "context.presets.createGlossary": "Создать глоссарий", "context.presets.created": "Глоссарий создан", "context.presets.createdDesc": "Глоссарий \"{name}\" создан с {count} терминами.", @@ -6904,6 +7536,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -7304,6 +7985,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "用語集なし", "translate.glossary.noGlossaries": "用語集なし", "translate.glossary.loading": "読み込み中...", + "translate.glossary.classicMode": "用語集なしのニュートラルエンジン(AIのみ)", + "translate.glossary.selectPlaceholder": "用語集を選択...", + "translate.glossary.multilingual": "多言語", + "translate.glossary.noGlossaryAvailable": "利用可能な用語集なし", + "translate.glossary.filterByLang": "言語で絞り込み", + "translate.glossary.active": "有効", + "translate.glossary.inactive": "無効", + "translate.glossary.availableTemplates": "利用可能なテンプレート", + "translate.glossary.importing": "インポート中...", + "translate.glossary.imported": "(インポート済み)", + "translate.glossary.noGlossaryForSource": "ソース言語の用語集・テンプレートなし", + "translate.glossary.createGlossary": "用語集を作成", + "translate.glossary.showAll": "すべての用語集を表示", + "translate.glossary.activePreview": "アクティブな一致のプレビュー:", + "translate.glossary.total": "合計", + "translate.glossary.moreTerms": "その他の用語", + "translate.glossary.noTerms": "この用語集には用語がありません。", + "translate.glossary.sourceTerm": "ソース用語", + "translate.glossary.translation": "翻訳", + "translate.glossary.addTerm": "用語を追加", + "translate.glossary.disabledMode": "用語集が適用されていないニュートラルエンジン", + "translate.glossary.addTermError": "用語の追加エラー", + "translate.glossary.networkError": "ネットワークエラー", + "translate.glossary.importFailed": "インポートに失敗しました ({status})", + "translate.glossary.helpText": "用語集は正確な用語翻訳を強制します。ソース言語がドキュメントの元の言語と一致する用語集を選択してください。", + "translate.glossary.sourceWarning": "警告:この用語集はソース言語を使用しています", + "translate.glossary.sourceWarningBut": "ただし、ドキュメントは次の言語に設定されています", + "translate.glossary.targetWarning": "ターゲットの不一致:この用語集は次の言語への翻訳用です", + "translate.glossary.targetWarningBut": "ただし、ドキュメントのターゲットは", + "translate.glossary.targetWarningEnd": "用語が関連しない場合があります。", "context.presets.createGlossary": "用語集を作成", "context.presets.created": "用語集を作成しました", "context.presets.createdDesc": "用語集「{name}」を{count}件の用語で作成しました。", @@ -7708,6 +8419,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -8108,6 +8868,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "용어집 없음", "translate.glossary.noGlossaries": "용어집 없음", "translate.glossary.loading": "로딩 중...", + "translate.glossary.classicMode": "용어집 없는 중립 엔진 (AI만)", + "translate.glossary.selectPlaceholder": "용어집 선택...", + "translate.glossary.multilingual": "다국어", + "translate.glossary.noGlossaryAvailable": "사용 가능한 용어집 없음", + "translate.glossary.filterByLang": "언어별 필터", + "translate.glossary.active": "활성", + "translate.glossary.inactive": "비활성", + "translate.glossary.availableTemplates": "사용 가능한 템플릿", + "translate.glossary.importing": "가져오는 중...", + "translate.glossary.imported": "(가져옴)", + "translate.glossary.noGlossaryForSource": "원본 언어에 대한 용어집 또는 템플릿 없음", + "translate.glossary.createGlossary": "용어집 만들기", + "translate.glossary.showAll": "모든 용어집 표시", + "translate.glossary.activePreview": "활성 일치 미리보기:", + "translate.glossary.total": "총", + "translate.glossary.moreTerms": "추가 용어", + "translate.glossary.noTerms": "이 용어집에 용어가 없습니다.", + "translate.glossary.sourceTerm": "원본 용어", + "translate.glossary.translation": "번역", + "translate.glossary.addTerm": "용어 추가", + "translate.glossary.disabledMode": "용어집이 적용되지 않은 중립 엔진", + "translate.glossary.addTermError": "용어 추가 오류", + "translate.glossary.networkError": "네트워크 오류", + "translate.glossary.importFailed": "가져오기 실패 ({status})", + "translate.glossary.helpText": "용어집은 정확한 용어 번역을 강제합니다. 원본 언어가 문서의 원래 언어와 일치하는 용어집을 선택하세요.", + "translate.glossary.sourceWarning": "주의: 이 용어집은 원본 언어를 사용합니다", + "translate.glossary.sourceWarningBut": "하지만 문서가 다음으로 설정되어 있습니다", + "translate.glossary.targetWarning": "대상 불일치: 이 용어집은 다음 언어로 번역하도록 설계되었습니다", + "translate.glossary.targetWarningBut": "하지만 문서의 대상은", + "translate.glossary.targetWarningEnd": "용어가 관련이 없을 수 있습니다.", "context.presets.createGlossary": "용어집 만들기", "context.presets.created": "용어집 생성됨", "context.presets.createdDesc": "용어집 \"{name}\"이(가) {count}개의 용어로 생성되었습니다.", @@ -8512,6 +9302,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -8870,6 +9709,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "无术语表", "translate.glossary.noGlossaries": "无术语表", "translate.glossary.loading": "加载中...", + "translate.glossary.classicMode": "无术语表的中性引擎(仅AI)", + "translate.glossary.selectPlaceholder": "选择术语表...", + "translate.glossary.multilingual": "多语言", + "translate.glossary.noGlossaryAvailable": "无可用术语表", + "translate.glossary.filterByLang": "按语言筛选", + "translate.glossary.active": "启用", + "translate.glossary.inactive": "未启用", + "translate.glossary.availableTemplates": "可用模板", + "translate.glossary.importing": "导入中...", + "translate.glossary.imported": "(已导入)", + "translate.glossary.noGlossaryForSource": "源语言无术语表或模板", + "translate.glossary.createGlossary": "创建术语表", + "translate.glossary.showAll": "显示所有术语表", + "translate.glossary.activePreview": "活跃匹配预览:", + "translate.glossary.total": "总计", + "translate.glossary.moreTerms": "其他术语", + "translate.glossary.noTerms": "此术语表中没有术语。", + "translate.glossary.sourceTerm": "源术语", + "translate.glossary.translation": "翻译", + "translate.glossary.addTerm": "添加术语", + "translate.glossary.disabledMode": "无术语表应用的中性引擎", + "translate.glossary.addTermError": "添加术语时出错", + "translate.glossary.networkError": "网络错误", + "translate.glossary.importFailed": "导入失败 ({status})", + "translate.glossary.helpText": "术语表强制精确的术语翻译。请选择源语言与文档原始语言匹配的术语表。", + "translate.glossary.sourceWarning": "注意:此术语表使用源语言", + "translate.glossary.sourceWarningBut": "但您的文档配置为", + "translate.glossary.targetWarning": "目标不匹配:此术语表旨在翻译为", + "translate.glossary.targetWarningBut": "但您的文档目标为", + "translate.glossary.targetWarningEnd": "术语可能不相关。", "context.presets.createGlossary": "创建术语表", "context.presets.created": "术语表已创建", "context.presets.createdDesc": "术语表 \"{name}\" 已创建,包含 {count} 个术语。", @@ -9274,6 +10143,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -9632,6 +10550,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "لا مصطلحات لـ", "translate.glossary.noGlossaries": "لا مصطلحات", "translate.glossary.loading": "جاري التحميل...", + "translate.glossary.classicMode": "محرك محايد بدون مسرد (AI فقط)", + "translate.glossary.selectPlaceholder": "اختر مسرداً...", + "translate.glossary.multilingual": "متعدد اللغات", + "translate.glossary.noGlossaryAvailable": "لا يوجد مسرد متاح", + "translate.glossary.filterByLang": "تصفية حسب اللغة", + "translate.glossary.active": "نشط", + "translate.glossary.inactive": "غير نشط", + "translate.glossary.availableTemplates": "القوالب المتاحة", + "translate.glossary.importing": "جاري الاستيراد...", + "translate.glossary.imported": "(مستورد)", + "translate.glossary.noGlossaryForSource": "لا يوجد مسرد أو قالب للغة المصدر", + "translate.glossary.createGlossary": "إنشاء مسرد", + "translate.glossary.showAll": "عرض جميع المسارد", + "translate.glossary.activePreview": "معاينة التطابقات النشطة:", + "translate.glossary.total": "الإجمالي", + "translate.glossary.moreTerms": "مصطلحات إضافية", + "translate.glossary.noTerms": "لا توجد مصطلحات في هذا المسرد.", + "translate.glossary.sourceTerm": "مصطلح المصدر", + "translate.glossary.translation": "الترجمة", + "translate.glossary.addTerm": "إضافة مصطلح", + "translate.glossary.disabledMode": "محرك محايد بدون مسرد مطبق", + "translate.glossary.addTermError": "خطأ في إضافة المصطلح", + "translate.glossary.networkError": "خطأ في الشبكة", + "translate.glossary.importFailed": "فشل الاستيراد ({status})", + "translate.glossary.helpText": "يسرد المسرد ترجمة دقيقة للمصطلحات. اختر مسرداً تطابق لغته المصدر اللغة الأصلية لمستندك.", + "translate.glossary.sourceWarning": "تحذير: هذا المسرد يستخدم لغة المصدر", + "translate.glossary.sourceWarningBut": "ولكن مستندك مهيأ بـ", + "translate.glossary.targetWarning": "عدم توافق الهدف: هذا المسرد مصمم للترجمة إلى", + "translate.glossary.targetWarningBut": "ولكن مستندك يستهدف", + "translate.glossary.targetWarningEnd": "قد لا تكون المصطلحات ذات صلة.", "context.presets.createGlossary": "إنشاء مسرد", "context.presets.created": "تم إنشاء المسرد", "context.presets.createdDesc": "تم إنشاء المسرد \"{name}\" بـ {count} مصطلحات.", @@ -10045,6 +10993,55 @@ const messages: Record> = { "glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.", "glossaries.upgrade.proLabel": "Pro", "glossaries.upgrade.upgradeBtn": "Passer à Pro", + "glossaries.loading": "Chargement...", + "glossaries.howItWorks.title": "Comment ces paramètres sont utilisés", + "glossaries.howItWorks.step1Title": "Configurez ici", + "glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.", + "glossaries.howItWorks.step2Title": "Activez dans Traduire", + "glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.", + "glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.", + "glossaries.howItWorks.goToTranslate": "Aller à Traduire", + "glossaries.status.unsaved": "Non enregistré", + "glossaries.status.active": "Actif · s'applique à toutes les traductions IA", + "glossaries.status.inactive": "Inactif", + "glossaries.instructions.whatForBold": "À quoi ça sert ?", + "glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.", + "glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »", + "glossaries.instructions.charCount": "{count} caractères", + "glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA", + "glossaries.instructions.clearAll": "Tout effacer", + "glossaries.instructions.saving": "Enregistrement…", + "glossaries.instructions.saved": "Enregistré", + "glossaries.presets.whatForBold": "À quoi ça sert ?", + "glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.", + "glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire", + "glossaries.presets.creating": "Création…", + "glossaries.presets.it.title": "IT / Logiciel", + "glossaries.presets.it.desc": "Développement, infrastructure, DevOps", + "glossaries.presets.legal.title": "Juridique / Contrats", + "glossaries.presets.legal.desc": "Droit des affaires, contentieux", + "glossaries.presets.medical.title": "Médical / Santé", + "glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic", + "glossaries.presets.finance.title": "Finance / Comptabilité", + "glossaries.presets.finance.desc": "IFRS, bilans, fiscalité", + "glossaries.presets.marketing.title": "Marketing / Publicité", + "glossaries.presets.marketing.desc": "Digital, branding, analytics", + "glossaries.presets.hr.title": "RH / Ressources Humaines", + "glossaries.presets.hr.desc": "Contrats, politiques, recrutement", + "glossaries.presets.scientific.title": "Scientifique / Recherche", + "glossaries.presets.scientific.desc": "Publications, thèses, articles", + "glossaries.presets.ecommerce.title": "E-commerce / Vente", + "glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM", + "glossaries.grid.title": "Vos", + "glossaries.grid.titleHighlight": "glossaires", + "glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier", + "glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus", + "glossaries.grid.activeTranslation": "Traduction active :", + "glossaries.grid.goToTranslate": "Aller à Traduire pour activer", + "glossaries.badge.compatible": "Compatible", + "glossaries.badge.otherTarget": "Autre cible", + "glossaries.card.editTerms": "Modifier les termes", + "glossaries.card.delete": "Supprimer", "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.", @@ -10430,6 +11427,36 @@ const messages: Record> = { "translate.glossary.noGlossaryForPair": "واژه‌نامه‌ای برای", "translate.glossary.noGlossaries": "واژه‌نامه‌ای نیست", "translate.glossary.loading": "در حال بارگذاری...", + "translate.glossary.classicMode": "موتور خنثی بدون واژه‌نامه (فقط هوش مصنوعی)", + "translate.glossary.selectPlaceholder": "واژه‌نامه‌ای انتخاب کنید...", + "translate.glossary.multilingual": "چندزبانه", + "translate.glossary.noGlossaryAvailable": "واژه‌نامه‌ای در دسترس نیست", + "translate.glossary.filterByLang": "فیلتر بر اساس زبان", + "translate.glossary.active": "فعال", + "translate.glossary.inactive": "غیرفعال", + "translate.glossary.availableTemplates": "قالب‌های موجود", + "translate.glossary.importing": "در حال وارد کردن...", + "translate.glossary.imported": "(وارد شده)", + "translate.glossary.noGlossaryForSource": "واژه‌نامه یا قالبی برای زبان مبدأ وجود ندارد", + "translate.glossary.createGlossary": "ایجاد واژه‌نامه", + "translate.glossary.showAll": "نمایش همه واژه‌نامه‌ها", + "translate.glossary.activePreview": "پیش‌نمایش تطبیق‌های فعال:", + "translate.glossary.total": "مجموع", + "translate.glossary.moreTerms": "واژه‌های بیشتر", + "translate.glossary.noTerms": "واژه‌ای در این واژه‌نامه وجود ندارد.", + "translate.glossary.sourceTerm": "واژه مبدأ", + "translate.glossary.translation": "ترجمه", + "translate.glossary.addTerm": "افزودن واژه", + "translate.glossary.disabledMode": "موتور خنثی بدون واژه‌نامه اعمال شده", + "translate.glossary.addTermError": "خطا در افزودن واژه", + "translate.glossary.networkError": "خطای شبکه", + "translate.glossary.importFailed": "وارد کردن ناموفق ({status})", + "translate.glossary.helpText": "واژه‌نامه ترجمه دقیق واژه‌ها را اجباری می‌کند. واژه‌نامه‌ای انتخاب کنید که زبان مبدأ آن با زبان اصلی سند شما مطابقت داشته باشد.", + "translate.glossary.sourceWarning": "هشدار: این واژه‌نامه از زبان مبدأ استفاده می‌کند", + "translate.glossary.sourceWarningBut": "اما سند شما پیکربندی شده به", + "translate.glossary.targetWarning": "عدم سازگاری هدف: این واژه‌نامه برای ترجمه به", + "translate.glossary.targetWarningBut": "اما هدف سند شما", + "translate.glossary.targetWarningEnd": "واژه‌ها ممکن است مرتبط نباشند.", "context.presets.createGlossary": "ایجاد واژه‌نامه", "context.presets.created": "واژه‌نامه ایجاد شد", "context.presets.createdDesc": "واژه‌نامه \"{name}\" با {count} واژه ایجاد شد.", diff --git a/routes/admin_routes.py b/routes/admin_routes.py index b3bdd38..e9f1fb3 100644 --- a/routes/admin_routes.py +++ b/routes/admin_routes.py @@ -173,10 +173,26 @@ def _subscription_status_str(raw) -> str: return raw.value if hasattr(raw, "value") else str(raw) +def _get_client_ip(request: Request) -> str: + """Get real client IP from headers or connection""" + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip + + if request.client: + return request.client.host + + return "unknown" + + @router.post("/login") async def admin_login(request: AdminLoginRequest, req: Request): """Admin login endpoint - Returns a bearer token for authenticated admin access""" - client_ip = req.client.host if req.client else "unknown" + client_ip = _get_client_ip(req) # Brute-force protection now = time.time() @@ -225,13 +241,13 @@ async def admin_logout(authorization: Optional[str] = Header(None)): @router.get("/verify") -async def verify_admin_session(is_admin: bool = Depends(require_admin)): +async def verify_admin_session(admin_id: str = Depends(require_admin)): """Verify admin token is still valid""" return {"status": "valid", "authenticated": True} @router.get("/dashboard") -async def get_admin_dashboard(is_admin: bool = Depends(require_admin)): +async def get_admin_dashboard(admin_id: str = Depends(require_admin)): """Get comprehensive admin dashboard data""" from middleware.cleanup import create_cleanup_manager from middleware.rate_limiting import RateLimitManager, RateLimitConfig @@ -285,7 +301,7 @@ async def get_admin_dashboard(is_admin: bool = Depends(require_admin)): @router.get("/users") -async def get_admin_users(is_admin: bool = Depends(require_admin)): +async def get_admin_users(admin_id: str = Depends(require_admin)): """Liste tous les utilisateurs (base SQLite/PostgreSQL + comptes uniquement dans users.json).""" from services.auth_service import USE_DATABASE, DATABASE_AVAILABLE, load_users from database.connection import get_sync_session @@ -397,7 +413,7 @@ async def get_admin_users(is_admin: bool = Depends(require_admin)): async def patch_admin_user_tier( user_id: str, body: AdminUpdateUserTierRequest, - is_admin: bool = Depends(require_admin), + admin_id: str = Depends(require_admin), ): """Update a user's plan/tier - Admin only""" from services.auth_service import get_user_by_id, update_user_plan @@ -459,7 +475,7 @@ async def patch_admin_user_tier( async def admin_reset_user_password( user_id: str, body: AdminResetPasswordRequest, - is_admin: bool = Depends(require_admin), + admin_id: str = Depends(require_admin), ): """Définit un nouveau mot de passe pour un utilisateur (sans email de réinitialisation).""" from services.auth_service import admin_set_user_password @@ -484,7 +500,7 @@ async def admin_reset_user_password( @router.get("/stats") -async def get_admin_stats(is_admin: bool = Depends(require_admin)): +async def get_admin_stats(admin_id: str = Depends(require_admin)): """Get comprehensive admin statistics""" from services.auth_service import USE_DATABASE, DATABASE_AVAILABLE, load_users from services.translation_service import _translation_cache @@ -565,7 +581,7 @@ async def get_admin_stats(is_admin: bool = Depends(require_admin)): @router.post("/cleanup/trigger") -async def trigger_cleanup(is_admin: bool = Depends(require_admin)): +async def trigger_cleanup(admin_id: str = Depends(require_admin)): """Trigger manual cleanup of expired files""" from middleware.cleanup import create_cleanup_manager @@ -583,7 +599,7 @@ async def trigger_cleanup(is_admin: bool = Depends(require_admin)): @router.get("/files/tracked") -async def get_tracked_files(is_admin: bool = Depends(require_admin)): +async def get_tracked_files(admin_id: str = Depends(require_admin)): """Get list of currently tracked files""" from middleware.cleanup import create_cleanup_manager @@ -593,7 +609,7 @@ async def get_tracked_files(is_admin: bool = Depends(require_admin)): @router.post("/quota/reset") -async def reset_translation_quotas(is_admin: bool = Depends(require_admin)): +async def reset_translation_quotas(admin_id: str = Depends(require_admin)): """Reset monthly translation quotas for all free-tier users. Clears Redis keys matching quota:monthly:* """ @@ -623,7 +639,7 @@ async def reset_translation_quotas(is_admin: bool = Depends(require_admin)): @router.post("/config/provider") async def update_default_provider( provider: str = Form(...), - is_admin: bool = Depends(require_admin), + admin_id: str = Depends(require_admin), ): """Update the default translation provider""" valid_providers = [ @@ -732,7 +748,7 @@ def _extract_error_code(error_message: Optional[str]) -> Optional[str]: @router.get("/logs") def get_admin_logs( - is_admin: str = Depends(require_admin), + admin_id: str = Depends(require_admin), level: str = Query(default="all", pattern="^(all|error|warning|info)$"), search: str = Query(default="", max_length=200), page: int = Query(default=1, ge=1), @@ -1261,6 +1277,8 @@ async def test_send_email( username = ((smtp_body and smtp_body.username) or "").strip() or (smtp.username or "").strip() or os.getenv("SMTP_USERNAME", "").strip() password = ((smtp_body and smtp_body.password) or "").strip() or (smtp.password or "").strip() or os.getenv("SMTP_PASSWORD", "").strip() from_email = ((smtp_body and smtp_body.from_email) or "").strip() or (smtp.from_email or "").strip() or os.getenv("SMTP_FROM_EMAIL", "").strip() or username + if from_email: + from_email = from_email.replace("\r", "").replace("\n", "") use_tls = (smtp_body and smtp_body.use_tls) if (smtp_body and smtp_body.use_tls is not None) else (smtp.use_tls if smtp.use_tls is not None else True) if not from_email: diff --git a/routes/translate_routes.py b/routes/translate_routes.py index 40d69f6..8219749 100644 --- a/routes/translate_routes.py +++ b/routes/translate_routes.py @@ -42,6 +42,7 @@ from pydantic import BaseModel, Field, field_validator from typing_extensions import Annotated from config import config +from translators import ExcelTranslator, WordTranslator, PowerPointTranslator from models.subscription import PlanType from middleware.tier_quota import tier_quota_service from services.auth_service import record_usage @@ -978,7 +979,6 @@ async def _run_translation_job( ) output_path = config.OUTPUT_DIR / output_filename - from translators import ExcelTranslator, WordTranslator, PowerPointTranslator from services.translation_service import ( OpenRouterTranslationProvider, OllamaTranslationProvider, @@ -1192,6 +1192,7 @@ async def _run_translation_job( # One translator instance per job so concurrent jobs never share mutable # provider state (singleton set_provider was racy under parallel translations). if file_extension == ".xlsx": + logger.info(f"DEBUG: ExcelTranslator class is {ExcelTranslator} and translate_file is {ExcelTranslator.translate_file}") job_translator = ExcelTranslator(provider=translation_provider) await asyncio.to_thread( job_translator.translate_file, diff --git a/tests/conftest.py b/tests/conftest.py index b88a3c5..8c0c117 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,21 @@ -""" -Test configuration and fixtures -""" - -from typing import AsyncGenerator - +import pytest import pytest_asyncio +from typing import AsyncGenerator from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker +from database.connection import sync_engine +from database.models import Base + # In-memory SQLite: fully isolated, no disk state between test sessions TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" +@pytest.fixture(scope="session", autouse=True) +def initialize_test_database(): + Base.metadata.create_all(bind=sync_engine) + yield + + @pytest_asyncio.fixture async def async_engine(): from database.models import Base diff --git a/tests/test_admin_logs.py b/tests/test_admin_logs.py index bf963e1..cca379d 100644 --- a/tests/test_admin_logs.py +++ b/tests/test_admin_logs.py @@ -123,7 +123,7 @@ def test_admin_logs_returns_200_and_shape(client_with_admin, admin_token): def test_admin_logs_no_original_filename_in_response(client_with_admin, admin_token): """NFR11/NFR16: response must never contain original_filename or document content.""" row = _make_mock_translation(original_filename="sensitive.docx") - with patch("routes.admin_routes.get_sync_session") as mock_get_session: + with patch("database.connection.get_sync_session") as mock_get_session: session_mock = MagicMock() mock_get_session.return_value.__enter__.return_value = session_mock mock_get_session.return_value.__exit__.return_value = None diff --git a/tests/test_admin_tier_change.py b/tests/test_admin_tier_change.py index c1e5fd5..a6b0830 100644 --- a/tests/test_admin_tier_change.py +++ b/tests/test_admin_tier_change.py @@ -200,7 +200,7 @@ def app_client_for_quota(tmp_path, monkeypatch, admin_password): monkeypatch.setattr(auth_svc, "USERS_FILE", tmp_path / "users.json") monkeypatch.setattr(auth_svc, "USE_DATABASE", False) monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", False) - monkeypatch.setattr(tier_quota_mod, "_async_redis", None) + monkeypatch.setattr(tier_quota_mod, "_get_async_redis", lambda: None) monkeypatch.setenv("REDIS_URL", "") _memory_usage.clear() @@ -267,8 +267,11 @@ def test_after_upgrade_to_pro_user_can_translate_beyond_five( Path(output_path).write_bytes(b"dummy") with patch( - "translators.excel_translator.excel_translator.translate_file", + "routes.translate_routes.ExcelTranslator.translate_file", side_effect=_fake_translate, + ), patch( + "routes.translate_routes.ExcelTranslator.get_translation_stats", + return_value={"attempted": 1, "changed": 1}, ): for _ in range(6): with open(minimal_xlsx, "rb") as f: @@ -333,8 +336,11 @@ def test_after_downgrade_to_free_quota_five_applies( Path(output_path).write_bytes(b"dummy") with patch( - "translators.excel_translator.excel_translator.translate_file", + "routes.translate_routes.ExcelTranslator.translate_file", side_effect=_fake_translate, + ), patch( + "routes.translate_routes.ExcelTranslator.get_translation_stats", + return_value={"attempted": 1, "changed": 1}, ): for _ in range(5): with open(minimal_xlsx, "rb") as f: @@ -350,6 +356,8 @@ def test_after_downgrade_to_free_quota_five_applies( data={"target_lang": "fr", "provider": "google"}, headers={"Authorization": f"Bearer {access_token}"}, ) + import time + time.sleep(0.5) client.patch( f"{ADMIN_USERS_PATCH}/{user_id}", json={"plan": "free"}, diff --git a/tests/test_tier_rate_limit.py b/tests/test_tier_rate_limit.py index 24a3c8e..caf887c 100644 --- a/tests/test_tier_rate_limit.py +++ b/tests/test_tier_rate_limit.py @@ -19,11 +19,15 @@ from middleware import tier_quota as tier_quota_mod from middleware.tier_quota import ( TierQuotaService, QuotaResult, - FREE_TIER_DAILY_LIMIT, + FREE_TIER_MONTHLY_LIMIT as FREE_TIER_DAILY_LIMIT, _memory_usage, - _seconds_until_midnight_utc, ) +def _seconds_until_midnight_utc(): + from middleware.tier_quota import _seconds_until_next_month + return _seconds_until_next_month() + + # Force in-memory backend and reset state so tests are isolated @pytest.fixture(autouse=True)