fix: remove DeepSeek/MiniMax branding, fix glossary visibility, improve translate page UX
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m7s

- US1: Replace exposed provider names (DeepSeek V3 → IA Express, MiniMax → IA Avancée)
  in legacy_routes.py, remove 52 DeepSeek references from i18n pricing/landing keys,
  add generic modelName key across all 13 languages
- US2: Fix glossary selector filtering (fallback to all glossaries when source lang
  filter returns empty), show templates by default, fix 3 broken presets
  (hvac/construction/automotive → hr/scientific/ecommerce)
- US3: Replace 15 hardcoded French strings with i18n t() calls, increase font sizes
  from 8-9px to 11px, improve text contrast (opacity /20→/40, /25→/45, /30→/50)
- US4: Add missing provider labels in admin ProviderStatus (deepseek, minimax, zai,
  google_cloud, openrouter, openrouter_premium)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 08:24:33 +02:00
parent b472dcd4b7
commit 9532fef2cd
7 changed files with 417 additions and 136 deletions

View File

@@ -15,10 +15,15 @@ interface ProviderStatusProps {
const PROVIDER_LABELS: Record<string, string> = {
google: "Google Translate",
google_cloud: "Google Cloud",
deepl: "DeepL",
ollama: "Ollama (Local)",
openai: "OpenAI",
openrouter: "OpenRouter",
openrouter: "IA Essentielle",
openrouter_premium: "IA Premium",
deepseek: "IA Express",
minimax: "IA Avancée",
zai: "Grok (xAI)",
};
const STATUS_CONFIG = {

View File

@@ -5,7 +5,7 @@ import {
BookText, Plus, Library, Calendar, Hash,
Zap, Save, Trash2, MessageSquare, Loader2,
Wrench, HardHat, Monitor, Scale, Stethoscope, BarChart3,
Megaphone, Car,
Megaphone, Car, ShoppingCart, FlaskConical, Users,
} from 'lucide-react';
import { useUser } from '@/app/dashboard/useUser';
import { useI18n } from '@/lib/i18n';
@@ -21,14 +21,14 @@ import { useTranslationStore } from '@/lib/store';
import { API_BASE } from '@/lib/config';
const PRESETS = [
{ key: 'hvac', title: 'HVAC / Génie climatique', desc: 'Thermique, ventilation, climatisation', icon: Wrench, templateId: 'hvac' },
{ key: 'construction', title: 'BTP / Construction', desc: 'Gros œuvre, second œuvre, normes', icon: HardHat, templateId: 'construction' },
{ 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: 'automotive', title: 'Automobile', desc: 'Motorisation, ADAS, homologation', icon: Car, templateId: 'automotive' },
{ 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' },
];
export default function GlossariesPage() {

View File

@@ -40,7 +40,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on
const [importingId, setImportingId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [showTemplates, setShowTemplates] = useState(false);
const [showTemplates, setShowTemplates] = useState(true);
const containerRef = useRef<HTMLDivElement>(null);
const fetchData = useCallback(async () => {
@@ -125,17 +125,20 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on
const sourceFlag = SUPPORTED_LANGUAGES.find(l => l.code === sourceLang)?.flag ?? '';
const targetFlag = SUPPORTED_LANGUAGES.find(l => l.code === targetLang)?.flag ?? '';
const filteredGlossaries = sourceLang === 'auto'
const langFiltered = sourceLang === 'auto'
? glossaries
: glossaries.filter(g => g.source_language === sourceLang);
// If filtering by source language yields nothing, show all glossaries
const filteredGlossaries = langFiltered.length > 0 ? langFiltered : glossaries;
const selected = glossaries.find(g => g.id === glossaryId);
return (
<div className="space-y-2 relative" ref={containerRef}>
<label className="text-[9px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-[0.2em] block">
<BookText size={10} className="inline mr-1.5 text-brand-accent" />
{t('translate.glossary.title')} <span className="normal-case tracking-normal font-normal text-brand-dark/25 dark:text-white/25">({sourceFlag}{targetFlag})</span>
<label className="text-[11px] font-black text-brand-dark/50 dark:text-white/50 uppercase tracking-[0.15em] block">
<BookText size={12} className="inline mr-1.5 text-brand-accent" />
{t('translate.glossary.title')} <span className="normal-case tracking-normal font-normal text-brand-dark/40 dark:text-white/40">({sourceFlag}{targetFlag})</span>
</label>
{/* Dropdown trigger */}
@@ -158,12 +161,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on
</div>
<div className="min-w-0 flex-1">
<div className="text-[11px] font-bold truncate text-brand-accent">{selected.name}</div>
<div className="text-[9px] text-brand-dark/30 dark:text-white/30">{selected.terms_count} {t('translate.glossary.terms')}</div>
<div className="text-[11px] text-brand-dark/50 dark:text-white/50">{selected.terms_count} {t('translate.glossary.terms')}</div>
</div>
<button
type="button"
onClick={(e) => { e.stopPropagation(); onChange(null); }}
className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-brand-dark/20 hover:text-brand-dark hover:bg-brand-muted dark:text-white/20 dark:hover:text-white dark:hover:bg-white/10 transition-colors"
className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-brand-dark/40 hover:text-brand-dark hover:bg-brand-muted dark:text-white/40 dark:hover:text-white dark:hover:bg-white/10 transition-colors"
>
<X size={11} />
</button>
@@ -182,12 +185,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on
</span>
</>
)}
<ChevronDown size={14} className={cn("shrink-0 text-brand-dark/20 dark:text-white/20 transition-transform", isOpen && "rotate-180")} />
<ChevronDown size={14} className={cn("shrink-0 text-brand-dark/40 dark:text-white/40 transition-transform", isOpen && "rotate-180")} />
</button>
{/* Error */}
{error && (
<p className="text-[10px] text-red-500 pl-1">{error}</p>
<p className="text-[11px] text-red-500 pl-1">{error}</p>
)}
{/* Dropdown panel */}
@@ -221,7 +224,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on
{g.name}
</div>
</div>
<span className="text-[9px] text-brand-dark/25 dark:text-white/25">{g.terms_count} {t('translate.glossary.terms')}</span>
<span className="text-[10px] text-brand-dark/45 dark:text-white/45">{g.terms_count} {t('translate.glossary.terms')}</span>
</button>
);
})}

View File

@@ -83,32 +83,32 @@ export default function TranslatePage() {
/* ── Human-friendly error messages ──────────────────────────── */
const humanFriendlyError = (raw: string | null): string => {
if (!raw) return 'Une erreur inattendue est survenue. Réessayez.';
if (!raw) return t('dashboard.translate.error.unexpected');
const r = raw.toLowerCase();
if (r.includes('0 out of') || r.includes('0 textes') || r.includes('0 texts'))
return "La traduction n'a produit aucun résultat. Vérifiez que le document contient du texte, puis réessayez ou choisissez un autre moteur.";
return t('dashboard.translate.error.noResult');
if (r.includes('api key') || r.includes('api_key') || r.includes('unauthorized') || r.includes('401'))
return "Clé API invalide ou manquante. Contactez l'administrateur pour configurer les clés.";
return t('dashboard.translate.error.apiKey');
if (r.includes('quota') || r.includes('rate limit') || r.includes('429'))
return "Limite d'utilisation atteinte. Réessayez dans quelques minutes ou choisissez un autre moteur.";
return t('dashboard.translate.error.quota');
if (r.includes('timeout') || r.includes('timed out') || r.includes('connexion'))
return "La connexion au service de traduction a expiré. Vérifiez votre réseau et réessayez.";
return t('dashboard.translate.error.timeout');
if (r.includes('not found') || r.includes('404'))
return "La session a expiré. Cliquez sur Réessayer pour relancer la traduction.";
return t('dashboard.translate.error.sessionExpired');
if (r.includes('empty') || r.includes('vide') || r.includes('no translatable'))
return "Le document semble vide ou ne contient pas de texte traduisible (PDF image ?).";
return t('dashboard.translate.error.empty');
if (r.includes('unsupported') || r.includes('format'))
return "Format de fichier non supporté ou fichier corrompu.";
return t('dashboard.translate.error.unsupported');
if (r.includes('lost connection') || r.includes('internet'))
return "Connexion perdue. Vérifiez votre réseau et réessayez.";
return t('dashboard.translate.error.connection');
// Generic fallback — show raw but readable
return `Traduction échouée : ${raw.charAt(0).toUpperCase() + raw.slice(1)}`;
return t('dashboard.translate.error.generic', { detail: raw.charAt(0).toUpperCase() + raw.slice(1) });
};
useEffect(() => {
if (submit.error && submit.error !== lastErrorRef.current) {
lastErrorRef.current = submit.error;
showError({ title: 'Traduction échouée', description: humanFriendlyError(submit.error) });
showError({ title: t('dashboard.translate.error.title'), description: humanFriendlyError(submit.error) });
}
}, [submit.error, showError]);
@@ -275,7 +275,7 @@ export default function TranslatePage() {
{/* ── CONFIGURING STATE: File strip ─────────────────── */}
{showConfiguring && (
<div className="editorial-card p-10 bg-white border-none shadow-editorial dark:bg-[#141414]">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] mb-10 text-brand-dark/30 border-b border-black/5 pb-6 dark:text-white/30 dark:border-white/5">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] mb-10 text-brand-dark/50 border-b border-black/5 pb-6 dark:text-white/50 dark:border-white/5">
{t('landing.translate.sourceDocument')}
</h4>
<FileStrip file={upload.file!} onRemove={upload.removeFile} onReplace={() => replaceInputRef.current?.click()} t={t} />
@@ -295,7 +295,7 @@ export default function TranslatePage() {
<h3 className="text-2xl font-black uppercase tracking-tight mb-2 text-brand-dark dark:text-white">
{t('dashboard.translate.translating')}
</h3>
<p className="text-[10px] text-brand-dark/30 font-black uppercase tracking-widest dark:text-white/30">
<p className="text-[10px] text-brand-dark/50 font-black uppercase tracking-widest dark:text-white/50">
{submit.fileName || upload.file?.name}
</p>
</div>
@@ -311,7 +311,7 @@ export default function TranslatePage() {
'w-12 h-12 rounded-2xl border-4 border-white dark:border-[#141414] shadow-xl flex items-center justify-center z-10 transition-all duration-500',
submit.progress > (i * 25)
? 'bg-brand-dark text-white scale-110 dark:bg-brand-accent'
: 'bg-brand-muted text-brand-dark/20 dark:bg-white/10 dark:text-white/20'
: 'bg-brand-muted text-brand-dark/40 dark:bg-white/10 dark:text-white/40'
)}
>
<Icon size={18} />
@@ -325,7 +325,7 @@ export default function TranslatePage() {
</div>
<div className="flex justify-between items-end mb-8">
<span className="text-[10px] font-black text-brand-dark/30 uppercase tracking-[0.3em] dark:text-white/30">
<span className="text-[10px] font-black text-brand-dark/50 uppercase tracking-[0.3em] dark:text-white/50">
{activeStepIdx < 2 ? t('dashboard.translate.steps.uploading') : t('dashboard.translate.steps.starting')}
</span>
<span className="text-7xl font-black text-brand-dark dark:text-white">
@@ -360,7 +360,7 @@ export default function TranslatePage() {
</p>
</div>
</div>
<span className="px-5 py-2 bg-white dark:bg-[#1a1a1a] rounded-full text-[9px] font-black uppercase tracking-widest text-brand-accent border border-brand-accent/20 shadow-sm">
<span className="px-5 py-2 bg-white dark:bg-[#1a1a1a] rounded-full text-[11px] font-black uppercase tracking-widest text-brand-accent border border-brand-accent/20 shadow-sm">
{qualityLabel}
</span>
</div>
@@ -375,7 +375,7 @@ export default function TranslatePage() {
</button>
<button
onClick={handleNewTranslation}
className="text-[10px] font-black uppercase tracking-[0.3em] text-brand-dark/20 hover:text-brand-dark transition-colors dark:text-white/20 dark:hover:text-white"
className="text-[10px] font-black uppercase tracking-[0.3em] text-brand-dark/40 hover:text-brand-dark transition-colors dark:text-white/40 dark:hover:text-white"
>
+ {t('dashboard.translate.complete.newTranslation')}
</button>
@@ -394,7 +394,7 @@ export default function TranslatePage() {
<AlertTriangle className="size-5 text-red-500" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-black uppercase tracking-tight text-red-600 dark:text-red-400 mb-2">Traduction échouée</p>
<p className="text-sm font-black uppercase tracking-tight text-red-600 dark:text-red-400 mb-2">{t('dashboard.translate.error.title')}</p>
<p className="text-sm text-red-600/80 dark:text-red-300/80 leading-relaxed">{humanFriendlyError(submit.error)}</p>
</div>
</div>
@@ -416,7 +416,7 @@ export default function TranslatePage() {
className="premium-button w-full py-5 text-[12px] uppercase tracking-[0.25em] flex items-center justify-center gap-3 !rounded-2xl"
>
<RotateCcw size={18} />
Réessayer la traduction
{t('dashboard.translate.retry')}
</button>
)}
<button
@@ -424,7 +424,7 @@ export default function TranslatePage() {
className="w-full py-4 border-2 border-black/10 dark:border-white/10 rounded-2xl text-[11px] font-black uppercase tracking-[0.25em] text-brand-dark/50 dark:text-white/50 hover:text-brand-dark dark:hover:text-white hover:border-brand-dark/20 dark:hover:border-white/20 transition-all flex items-center justify-center gap-3"
>
<Upload size={16} />
Nouveau fichier
{t('dashboard.translate.newFile')}
</button>
</div>
</div>
@@ -441,7 +441,7 @@ export default function TranslatePage() {
<div className="editorial-card bg-white border-none shadow-editorial dark:bg-[#141414] overflow-hidden flex flex-col lg:sticky lg:top-8 lg:max-h-[calc(100vh-6rem)]">
{/* Scrollable config content */}
<div className="flex-1 overflow-y-auto p-10 pb-6">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] mb-10 text-brand-dark/30 border-b border-black/5 pb-6 dark:text-white/30 dark:border-white/5">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] mb-10 text-brand-dark/50 border-b border-black/5 pb-6 dark:text-white/50 dark:border-white/5">
{t('landing.translate.configuration')}
</h4>
@@ -463,20 +463,20 @@ export default function TranslatePage() {
{config.provider && (
<div className="flex items-center gap-2">
<span className={cn(
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[9px] font-black uppercase tracking-[0.15em]",
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.15em]",
config.mode === 'llm'
? "bg-brand-accent/10 text-brand-accent border border-brand-accent/20"
: "bg-brand-muted/50 text-brand-dark/40 dark:bg-white/5 dark:text-white/40 border border-transparent"
)}>
{config.mode === 'llm' ? (
<><Zap size={10} /> Mode IA</>
<><Zap size={10} /> {t('dashboard.translate.modeAI')}</>
) : (
<><Languages size={10} /> Mode Classique</>
<><Languages size={10} /> {t('dashboard.translate.modeClassic')}</>
)}
</span>
{config.mode === 'classic' && config.isPro && (
<span className="text-[9px] text-brand-dark/20 dark:text-white/20 italic">
Glossaires disponibles en mode IA
<span className="text-[11px] text-brand-dark/40 dark:text-white/40 italic">
{t('dashboard.translate.glossaryLLMHint')}
</span>
)}
</div>
@@ -497,7 +497,7 @@ export default function TranslatePage() {
{/* PDF mode selector */}
{isPdf && (
<div className="space-y-2">
<label className="text-[9px] font-black text-brand-dark/40 uppercase tracking-[0.2em] block mb-3 dark:text-white/40">
<label className="text-[11px] font-black text-brand-dark/50 uppercase tracking-[0.15em] block mb-3 dark:text-white/50">
{t('dashboard.translate.pdfMode.title')}
</label>
<div className="grid grid-cols-2 gap-2">
@@ -515,7 +515,7 @@ export default function TranslatePage() {
<FileText className="size-4 text-brand-accent" />
{t('dashboard.translate.pdfMode.preserveLayout')}
</div>
<p className="mt-1 text-[9px] text-brand-dark/50 font-bold uppercase tracking-widest leading-relaxed dark:text-white/40">
<p className="mt-1 text-[11px] text-brand-dark/55 font-bold uppercase tracking-widest leading-relaxed dark:text-white/50">
{t('dashboard.translate.pdfMode.preserveLayoutDesc')}
</p>
</button>
@@ -533,7 +533,7 @@ export default function TranslatePage() {
<Languages className="size-4 text-brand-accent" />
{t('dashboard.translate.pdfMode.textOnly')}
</div>
<p className="mt-1 text-[9px] text-brand-dark/50 font-bold uppercase tracking-widest leading-relaxed dark:text-white/40">
<p className="mt-1 text-[11px] text-brand-dark/55 font-bold uppercase tracking-widest leading-relaxed dark:text-white/50">
{t('dashboard.translate.pdfMode.textOnlyDesc')}
</p>
</button>
@@ -552,24 +552,24 @@ export default function TranslatePage() {
'w-full py-5 rounded-2xl text-[13px] font-black uppercase tracking-[0.2em] flex items-center justify-center gap-3 transition-all duration-200',
config.isConfigValid && upload.file && !submit.isSubmitting
? 'bg-brand-dark text-white hover:bg-brand-dark/90 shadow-lg shadow-brand-dark/20 dark:bg-brand-accent dark:text-brand-dark dark:hover:bg-brand-accent/90'
: 'bg-black/5 dark:bg-white/5 text-brand-dark/25 dark:text-white/25 cursor-not-allowed'
: 'bg-black/5 dark:bg-white/5 text-brand-dark/45 dark:text-white/45 cursor-not-allowed'
)}
>
{submit.isSubmitting ? (
<><Loader2 className="size-5 animate-spin" />Envoi en cours...</>
<><Loader2 className="size-5 animate-spin" />{t('dashboard.translate.submitting')}</>
) : (
<><ArrowRight size={20} />Lancer la traduction</>
<><ArrowRight size={20} />{t('dashboard.translate.submit')}</>
)}
</button>
{!upload.file && (
<p className="text-center text-[10px] text-brand-dark/30 dark:text-white/30 mt-2 font-bold uppercase tracking-widest"> Déposez d'abord un fichier</p>
<p className="text-center text-[11px] text-brand-dark/40 dark:text-white/40 mt-2 font-bold uppercase tracking-widest"> {t('dashboard.translate.noFile')}</p>
)}
{upload.file && !config.targetLang && (
<p className="text-center text-[10px] text-brand-dark/30 dark:text-white/30 mt-2 font-bold uppercase tracking-widest">↑ Sélectionnez une langue cible</p>
<p className="text-center text-[11px] text-brand-dark/40 dark:text-white/40 mt-2 font-bold uppercase tracking-widest"> {t('dashboard.translate.noTargetLang')}</p>
)}
</div>
<div className="shrink-0 flex justify-between px-6 pb-5 pt-1 text-[9px] font-black uppercase tracking-[0.2em] text-brand-dark/20 dark:text-white/20">
<div className="shrink-0 flex justify-between px-6 pb-5 pt-1 text-[10px] font-black uppercase tracking-[0.15em] text-brand-dark/40 dark:text-white/40">
<span className="flex items-center gap-2">
<ShieldCheck size={12} /> {t('landing.translate.zeroRetention')}
</span>
@@ -583,7 +583,7 @@ export default function TranslatePage() {
{/* ── MONITOR (processing) ────────────────────────────── */}
{showProcessing && (
<div className="editorial-card p-10 bg-white border-none shadow-editorial h-full dark:bg-[#141414]">
<h4 className="text-[10px] font-black uppercase tracking-[0.4em] mb-12 flex items-center gap-3 text-brand-dark/30 dark:text-white/30">
<h4 className="text-[11px] font-black uppercase tracking-[0.3em] mb-12 flex items-center gap-3 text-brand-dark/45 dark:text-white/45">
<div className="w-2 h-2 bg-brand-accent rounded-full animate-ping" />
{t('dashboard.translate.liveMonitor')}
</h4>
@@ -603,7 +603,7 @@ export default function TranslatePage() {
<p className="text-[11px] font-black uppercase tracking-tight truncate text-brand-dark dark:text-white">
{submit.fileName || upload.file?.name}
</p>
<p className="text-[9px] text-brand-dark/40 font-bold uppercase tracking-widest mt-1 dark:text-white/40">
<p className="text-[11px] text-brand-dark/45 font-bold uppercase tracking-widest mt-1 dark:text-white/45">
{upload.file ? `${fmt(upload.file.size)} ` : ''}{(submit.fileName || upload.file?.name || '').split('.').pop()?.toUpperCase()}
</p>
</div>
@@ -612,16 +612,16 @@ export default function TranslatePage() {
{/* Config summary */}
<div className="space-y-8 mb-16 px-2">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/50 dark:text-white/50">
<span>{t('dashboard.translate.language.source')}</span>
<span className="text-brand-dark dark:text-white">{srcLangName.toUpperCase()}</span>
</div>
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/50 dark:text-white/50">
<span>{t('dashboard.translate.language.target')}</span>
<span className="text-brand-accent">{tgtLangName.toUpperCase()}</span>
</div>
{currentProvider && (
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/50 dark:text-white/50">
<span>{t('dashboard.translate.engine')}</span>
<span className="text-brand-dark dark:text-white">{currentProvider.label.toUpperCase()}</span>
</div>
@@ -631,7 +631,7 @@ export default function TranslatePage() {
{/* Quality progress */}
<div className="pt-10 border-t border-black/10 dark:border-white/10">
<div className="flex justify-between text-[10px] font-black uppercase tracking-[0.4em] mb-4">
<span className="text-brand-dark/30 dark:text-white/30">{t('dashboard.translate.quality')}</span>
<span className="text-brand-dark/50 dark:text-white/50">{t('dashboard.translate.quality')}</span>
<span className="text-brand-accent">{qualityLabel}</span>
</div>
<div className="h-2 bg-brand-muted rounded-full overflow-hidden p-0.5 dark:bg-white/10">
@@ -654,22 +654,22 @@ export default function TranslatePage() {
{/* ── SUMMARY (complete) ──────────────────────────────── */}
{showComplete && (
<div className="editorial-card p-10 bg-white border-none shadow-editorial h-full dark:bg-[#141414]">
<h4 className="text-[10px] font-black uppercase tracking-[0.4em] mb-12 flex items-center gap-3 text-brand-dark/30 dark:text-white/30">
<h4 className="text-[11px] font-black uppercase tracking-[0.3em] mb-12 flex items-center gap-3 text-brand-dark/45 dark:text-white/45">
<CheckCircle2 size={14} className="text-emerald-500" />
{t('dashboard.translate.summary')}
</h4>
<div className="space-y-8 mb-16 px-2">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/50 dark:text-white/50">
<span>{t('dashboard.translate.language.source')}</span>
<span className="text-brand-dark dark:text-white">{srcLangName.toUpperCase()}</span>
</div>
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/50 dark:text-white/50">
<span>{t('dashboard.translate.language.target')}</span>
<span className="text-brand-accent">{tgtLangName.toUpperCase()}</span>
</div>
{currentProvider && (
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/30 dark:text-white/30">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.4em] text-brand-dark/50 dark:text-white/50">
<span>{t('dashboard.translate.engine')}</span>
<span className="text-brand-dark dark:text-white">{currentProvider.label.toUpperCase()}</span>
</div>
@@ -678,7 +678,7 @@ export default function TranslatePage() {
<div className="pt-10 border-t border-black/10 dark:border-white/10">
<div className="flex justify-between text-[10px] font-black uppercase tracking-[0.4em] mb-4">
<span className="text-brand-dark/30 dark:text-white/30">{t('dashboard.translate.quality')}</span>
<span className="text-brand-dark/50 dark:text-white/50">{t('dashboard.translate.quality')}</span>
<span className="text-brand-accent">{qualityLabel}</span>
</div>
<div className="h-2 bg-brand-muted rounded-full overflow-hidden p-0.5 dark:bg-white/10">
@@ -701,7 +701,7 @@ export default function TranslatePage() {
<div className="flex-1">
<div className="flex items-center gap-4 mb-4">
<span className="accent-pill !px-3 !py-1 text-[9px] italic">{t('memento.title')}</span>
<span className="accent-pill !px-3 !py-1 text-[11px] italic">{t('memento.title')}</span>
<h3 className="text-2xl font-black uppercase tracking-tight text-brand-dark dark:text-white">{t('memento.title')}</h3>
</div>
<p className="text-sm text-brand-dark/40 font-medium leading-relaxed max-w-2xl dark:text-white/40">
@@ -736,12 +736,12 @@ function FileStrip({ file, onRemove, onReplace, t }: { file: File; onRemove: ()
<FileIcon className={`size-5 shrink-0 ${color}`} />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-[11px] font-black uppercase tracking-tight text-brand-dark dark:text-white">{file.name}</span>
<span className="text-[9px] text-brand-dark/40 font-bold uppercase tracking-widest dark:text-white/40">{fmt(file.size)} .{ext.toUpperCase()}</span>
<span className="text-[10px] text-brand-dark/45 font-bold uppercase tracking-widest dark:text-white/45">{fmt(file.size)} .{ext.toUpperCase()}</span>
</div>
<button type="button" onClick={onReplace} className="flex shrink-0 items-center gap-1 rounded-xl px-2 py-1 text-[10px] font-black uppercase tracking-widest text-brand-dark/30 transition hover:bg-brand-muted hover:text-brand-dark dark:text-white/30 dark:hover:bg-white/10 dark:hover:text-white">
<button type="button" onClick={onReplace} className="flex shrink-0 items-center gap-1 rounded-xl px-2 py-1 text-[10px] font-black uppercase tracking-widest text-brand-dark/50 transition hover:bg-brand-muted hover:text-brand-dark dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white">
<Upload className="size-3.5" />{t('dashboard.translate.replace')}
</button>
<button type="button" aria-label="Remove" onClick={onRemove} className="flex size-7 shrink-0 items-center justify-center rounded-xl text-brand-dark/20 transition hover:bg-brand-muted hover:text-brand-dark dark:text-white/20 dark:hover:bg-white/10 dark:hover:text-white">
<button type="button" aria-label="Remove" onClick={onRemove} className="flex size-7 shrink-0 items-center justify-center rounded-xl text-brand-dark/40 transition hover:bg-brand-muted hover:text-brand-dark dark:text-white/40 dark:hover:bg-white/10 dark:hover:text-white">
<X className="size-4" />
</button>
</div>
@@ -789,7 +789,7 @@ function StatBox({ icon, value, label }: { icon: React.ReactNode; value: string;
<div className="p-6 bg-brand-muted/30 rounded-3xl text-center border border-transparent hover:border-brand-accent/10 transition-all dark:bg-white/5 dark:border-white/5">
<div className="text-brand-accent flex justify-center mb-4">{icon}</div>
<p className="text-[12px] font-black text-brand-dark mb-1 uppercase tracking-tight dark:text-white">{value}</p>
<p className="text-[8px] font-black text-brand-dark/30 uppercase tracking-[0.2em] dark:text-white/30">{label}</p>
<p className="text-[11px] font-black text-brand-dark/50 uppercase tracking-[0.15em] dark:text-white/50">{label}</p>
</div>
);
}

View File

@@ -814,7 +814,7 @@ export default function PricingPage() {
<Badge className="ml-auto text-xs bg-blue-500/10 text-blue-600 border-blue-500/30 dark:text-blue-300">{t('pricing.aiModels.essential.plan')}</Badge>
</div>
<div className="text-sm text-muted-foreground mb-3">
{t('pricing.aiModels.essential.descPrefix')} <strong className="text-foreground">DeepSeek V3.2</strong> {t('pricing.aiModels.essential.descSuffix')}
{t('pricing.aiModels.essential.descPrefix')} <strong className="text-foreground">{t('pricing.aiModels.essential.modelName')}</strong> {t('pricing.aiModels.essential.descSuffix')}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="px-2 py-1 bg-muted rounded">{t('pricing.aiModels.essential.context')}</span>