i18n: fix missing keys and translate all non-admin frontend strings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m43s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m43s
- Add 12 missing i18n keys (t() was returning the literal key string) to
all 13 locales: dashboard.topbar.premiumAccess,
dashboard.translate.complete.toastOkDesc,
dashboard.translate.progress.{connectionLost,processingFallback},
glossaries.card.{term,created}, glossaries.termEditor.{addTerm,maxReached},
login.google.{connecting,errorFailed,errorGeneric}, login.orContinueWith
- Add 6 FR-drift keys (landing.pricing.{free,enterprise}.{name,desc,cta})
- Add ~120 new i18n keys covering site header/footer, file-uploader,
checkout success, dashboard pages, translate page, provider selector
themes, language selector, translation complete, api-keys, services,
settings, pricing (~1800 new key/locale pairs)
- Wrap hardcoded French/English in components with t() calls
- Convert LLM_THEMES/CLASSIC_THEMES/FALLBACK_PROVIDERS maps from
hardcoded constants to t()-driven factories
- Admin pages intentionally left untouched per request
Files: 15 components/pages + src/lib/i18n.tsx
Typecheck: passes (tsc --noEmit exit 0)
This commit is contained in:
@@ -5,6 +5,7 @@ import { useSearchParams, useRouter } from 'next/navigation';
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { API_BASE } from '@/lib/config';
|
import { API_BASE } from '@/lib/config';
|
||||||
import { CheckCircle2, XCircle, RefreshCw } from 'lucide-react';
|
import { CheckCircle2, XCircle, RefreshCw } from 'lucide-react';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /checkout/success
|
* /checkout/success
|
||||||
@@ -17,6 +18,7 @@ export default function CheckoutSuccessPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const sessionId = searchParams.get('session_id');
|
const sessionId = searchParams.get('session_id');
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const [status, setStatus] = useState<'syncing' | 'ok' | 'error'>('syncing');
|
const [status, setStatus] = useState<'syncing' | 'ok' | 'error'>('syncing');
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
@@ -43,19 +45,23 @@ export default function CheckoutSuccessPage() {
|
|||||||
try { data = await res.json(); } catch { /* ignore */ }
|
try { data = await res.json(); } catch { /* ignore */ }
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setStatus('ok');
|
setStatus('ok');
|
||||||
setMessage(data.data?.plan ? `Forfait ${data.data.plan} activé !` : 'Abonnement activé !');
|
setMessage(
|
||||||
|
data.data?.plan
|
||||||
|
? t('checkout.planActivated', { plan: data.data.plan })
|
||||||
|
: t('checkout.subscriptionActivated')
|
||||||
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ['user', 'me'] });
|
queryClient.invalidateQueries({ queryKey: ['user', 'me'] });
|
||||||
// Redirect after 2s
|
// Redirect after 2s
|
||||||
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 2000);
|
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 2000);
|
||||||
} else {
|
} else {
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
setMessage(data.message ?? data.error ?? 'Erreur de synchronisation');
|
setMessage(data.message ?? data.error ?? t('checkout.syncError'));
|
||||||
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 3000);
|
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 3000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
setMessage('Erreur réseau. Votre paiement est confirmé — rechargez votre profil.');
|
setMessage(t('checkout.networkError'));
|
||||||
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 3000);
|
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 3000);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -67,24 +73,24 @@ export default function CheckoutSuccessPage() {
|
|||||||
{status === 'syncing' && (
|
{status === 'syncing' && (
|
||||||
<>
|
<>
|
||||||
<RefreshCw className="w-12 h-12 text-brand-accent animate-spin" />
|
<RefreshCw className="w-12 h-12 text-brand-accent animate-spin" />
|
||||||
<h1 className="text-2xl font-black uppercase tracking-tight">Activation en cours…</h1>
|
<h1 className="text-2xl font-black uppercase tracking-tight">{t('checkout.activating')}</h1>
|
||||||
<p className="text-sm text-muted-foreground">Nous mettons à jour votre abonnement, veuillez patienter.</p>
|
<p className="text-sm text-muted-foreground">{t('checkout.activatingDesc')}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{status === 'ok' && (
|
{status === 'ok' && (
|
||||||
<>
|
<>
|
||||||
<CheckCircle2 className="w-12 h-12 text-emerald-500" />
|
<CheckCircle2 className="w-12 h-12 text-emerald-500" />
|
||||||
<h1 className="text-2xl font-black uppercase tracking-tight text-emerald-600">Paiement confirmé !</h1>
|
<h1 className="text-2xl font-black uppercase tracking-tight text-emerald-600">{t('checkout.paymentConfirmed')}</h1>
|
||||||
<p className="text-sm text-muted-foreground">{message}</p>
|
<p className="text-sm text-muted-foreground">{message}</p>
|
||||||
<p className="text-xs text-muted-foreground">Redirection vers votre profil…</p>
|
<p className="text-xs text-muted-foreground">{t('checkout.redirectingToProfile')}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{status === 'error' && (
|
{status === 'error' && (
|
||||||
<>
|
<>
|
||||||
<XCircle className="w-12 h-12 text-amber-500" />
|
<XCircle className="w-12 h-12 text-amber-500" />
|
||||||
<h1 className="text-2xl font-black uppercase tracking-tight">Paiement reçu</h1>
|
<h1 className="text-2xl font-black uppercase tracking-tight">{t('checkout.paymentReceived')}</h1>
|
||||||
<p className="text-sm text-muted-foreground">{message}</p>
|
<p className="text-sm text-muted-foreground">{message}</p>
|
||||||
<p className="text-xs text-muted-foreground">Redirection…</p>
|
<p className="text-xs text-muted-foreground">{t('checkout.redirecting')}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function ApiKeyTable({ keys, onRevoke, isRevoking }: ApiKeyTableProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{copiedId === key.id ? 'Copied!' : t('apiKeys.table.copyPrefix')}
|
{copiedId === key.id ? t('apiKeys.copied') : t('apiKeys.table.copyPrefix')}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export default function ApiKeysPage() {
|
|||||||
{t('apiKeys.sectionTitle')}
|
{t('apiKeys.sectionTitle')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-mono text-brand-dark dark:text-white">
|
<p className="text-sm font-mono text-brand-dark dark:text-white">
|
||||||
{keys.length > 0 ? `${keys[0].key_prefix}************************************` : 'No keys generated'}
|
{keys.length > 0 ? `${keys[0].key_prefix}************************************` : t('apiKeys.noKeysGenerated')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { useUser } from './useUser';
|
import { useUser } from './useUser';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { API_BASE } from '@/lib/config';
|
import { API_BASE } from '@/lib/config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +19,7 @@ export default function DashboardPage() {
|
|||||||
const checkoutSessionId = searchParams.get('session_id');
|
const checkoutSessionId = searchParams.get('session_id');
|
||||||
const [syncError, setSyncError] = useState<string | null>(null);
|
const [syncError, setSyncError] = useState<string | null>(null);
|
||||||
const { refetch } = useUser();
|
const { refetch } = useUser();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!checkoutSessionId) {
|
if (!checkoutSessionId) {
|
||||||
@@ -42,20 +44,20 @@ export default function DashboardPage() {
|
|||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errData = await res.json().catch(() => ({}));
|
const errData = await res.json().catch(() => ({}));
|
||||||
setSyncError(errData.message || 'Erreur lors de la synchronisation du paiement.');
|
setSyncError(errData.message || t('dashboard.checkoutSyncError'));
|
||||||
} else {
|
} else {
|
||||||
await refetch();
|
await refetch();
|
||||||
router.replace('/dashboard/translate');
|
router.replace('/dashboard/translate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setSyncError('Erreur réseau. Veuillez rafraîchir la page.');
|
if (!cancelled) setSyncError(t('dashboard.networkRefresh'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
runSync();
|
runSync();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [checkoutSessionId, refetch, router]);
|
}, [checkoutSessionId, refetch, router, t]);
|
||||||
|
|
||||||
if (syncError) {
|
if (syncError) {
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +67,7 @@ export default function DashboardPage() {
|
|||||||
onClick={() => router.replace('/dashboard/translate')}
|
onClick={() => router.replace('/dashboard/translate')}
|
||||||
className="text-xs text-muted-foreground underline"
|
className="text-xs text-muted-foreground underline"
|
||||||
>
|
>
|
||||||
Continuer vers la traduction
|
{t('dashboard.continueToTranslate')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export default function ProfilePage() {
|
|||||||
)}>
|
)}>
|
||||||
{statusMsg.type === 'ok' ? <CheckCircle2 className="w-4 h-4 shrink-0 mt-0.5" /> : <XCircle className="w-4 h-4 shrink-0 mt-0.5" />}
|
{statusMsg.type === 'ok' ? <CheckCircle2 className="w-4 h-4 shrink-0 mt-0.5" /> : <XCircle className="w-4 h-4 shrink-0 mt-0.5" />}
|
||||||
<span className="flex-1">{statusMsg.text}</span>
|
<span className="flex-1">{statusMsg.text}</span>
|
||||||
<button onClick={() => setStatusMsg(null)} className="text-muted-foreground hover:text-foreground">✕</button>
|
<button onClick={() => setStatusMsg(null)} className="text-muted-foreground hover:text-foreground" aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { Zap, CheckCircle2, Lock, Loader2, Globe, Brain } from 'lucide-react';
|
|||||||
import { API_BASE } from '@/lib/config';
|
import { API_BASE } from '@/lib/config';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
|
||||||
const FALLBACK_PROVIDERS = [
|
const FALLBACK_PROVIDERS_FACTORY = (t: (k: string) => string) => [
|
||||||
{ id: "google", label: "Google Traduction", description: "Traduction rapide, 130+ langues", mode: "classic" as const },
|
{ id: "google", label: t('services.fallback.google.label'), description: t('services.fallback.google.desc'), mode: "classic" as const },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface AvailableProvider {
|
interface AvailableProvider {
|
||||||
@@ -19,6 +19,7 @@ interface AvailableProvider {
|
|||||||
|
|
||||||
export default function TranslationServicesPage() {
|
export default function TranslationServicesPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const FALLBACK_PROVIDERS = FALLBACK_PROVIDERS_FACTORY(t);
|
||||||
const [providers, setProviders] = useState<AvailableProvider[]>([]);
|
const [providers, setProviders] = useState<AvailableProvider[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export default function TranslationServicesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchProviders();
|
fetchProviders();
|
||||||
}, []);
|
}, [FALLBACK_PROVIDERS]);
|
||||||
|
|
||||||
const classicProviders = providers.filter((p) => p.mode === "classic");
|
const classicProviders = providers.filter((p) => p.mode === "classic");
|
||||||
const llmProviders = providers.filter((p) => p.mode === "llm");
|
const llmProviders = providers.filter((p) => p.mode === "llm");
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ export default function GeneralSettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formats = [
|
const formats = [
|
||||||
{ icon: FileSpreadsheet, color: 'text-green-500', name: 'Excel', ext: '.xlsx, .xls', features: [t('settings.formats.formulas'), t('settings.formats.styles'), t('settings.formats.images')] },
|
{ icon: FileSpreadsheet, color: 'text-green-500', name: t('settings.formats.excel.name'), ext: '.xlsx, .xls', features: [t('settings.formats.formulas'), t('settings.formats.styles'), t('settings.formats.images')] },
|
||||||
{ icon: FileText, color: 'text-blue-500', name: 'Word', ext: '.docx, .doc', features: [t('settings.formats.headers'), t('settings.formats.tables'), t('settings.formats.images')] },
|
{ icon: FileText, color: 'text-blue-500', name: t('settings.formats.word.name'), ext: '.docx, .doc', features: [t('settings.formats.headers'), t('settings.formats.tables'), t('settings.formats.images')] },
|
||||||
{ icon: Presentation, color: 'text-orange-500', name: 'PowerPoint', ext: '.pptx, .ppt', features: [t('settings.formats.slides'), t('settings.formats.notes'), t('settings.formats.images')] },
|
{ icon: Presentation, color: 'text-orange-500', name: t('settings.formats.powerpoint.name'), ext: '.pptx, .ppt', features: [t('settings.formats.slides'), t('settings.formats.notes'), t('settings.formats.images')] },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ function Combobox({
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
onChange: (code: string) => void;
|
onChange: (code: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -81,13 +82,13 @@ function Combobox({
|
|||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
placeholder="Search..."
|
placeholder={t('langSelector.search')}
|
||||||
className="w-full bg-transparent px-1 py-1 text-xs outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white"
|
className="w-full bg-transparent px-1 py-1 text-xs outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[160px] overflow-y-auto p-1 mt-1 space-y-0.5">
|
<div className="max-h-[160px] overflow-y-auto p-1 mt-1 space-y-0.5">
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<div className="px-3 py-3 text-center text-xs text-brand-dark/40 dark:text-white/40">No results</div>
|
<div className="px-3 py-3 text-center text-xs text-brand-dark/40 dark:text-white/40">{t('langSelector.noResults')}</div>
|
||||||
)}
|
)}
|
||||||
{filtered.map(lang => (
|
{filtered.map(lang => (
|
||||||
<button
|
<button
|
||||||
@@ -143,7 +144,7 @@ export default function LanguageSelector({
|
|||||||
<div className="grid grid-cols-11 gap-2 items-center">
|
<div className="grid grid-cols-11 gap-2 items-center">
|
||||||
{/* Source */}
|
{/* Source */}
|
||||||
<div className="col-span-5 relative text-left">
|
<div className="col-span-5 relative text-left">
|
||||||
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">Source</span>
|
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">{t('langSelector.source')}</span>
|
||||||
<Combobox
|
<Combobox
|
||||||
value={sourceLang}
|
value={sourceLang}
|
||||||
options={languages}
|
options={languages}
|
||||||
@@ -166,7 +167,7 @@ export default function LanguageSelector({
|
|||||||
? 'text-brand-dark/50 dark:text-white/50 hover:text-brand-accent hover:bg-brand-muted/50 dark:hover:bg-white/5'
|
? 'text-brand-dark/50 dark:text-white/50 hover:text-brand-accent hover:bg-brand-muted/50 dark:hover:bg-white/5'
|
||||||
: 'cursor-not-allowed opacity-30'
|
: 'cursor-not-allowed opacity-30'
|
||||||
)}
|
)}
|
||||||
title="Inverser"
|
title={t('langSelector.swap')}
|
||||||
>
|
>
|
||||||
<ArrowRightLeft size={12} />
|
<ArrowRightLeft size={12} />
|
||||||
</button>
|
</button>
|
||||||
@@ -174,7 +175,7 @@ export default function LanguageSelector({
|
|||||||
|
|
||||||
{/* Target */}
|
{/* Target */}
|
||||||
<div className="col-span-5 relative text-left">
|
<div className="col-span-5 relative text-left">
|
||||||
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">Cible</span>
|
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">{t('langSelector.target')}</span>
|
||||||
<Combobox
|
<Combobox
|
||||||
value={targetLang}
|
value={targetLang}
|
||||||
options={languages}
|
options={languages}
|
||||||
|
|||||||
@@ -22,73 +22,73 @@ interface CardTheme {
|
|||||||
descriptionOverride: string;
|
descriptionOverride: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LLM_THEMES: Record<string, CardTheme> = {
|
const LLM_THEMES_FACTORY = (t: (k: string) => string): Record<string, CardTheme> => ({
|
||||||
deepseek: {
|
deepseek: {
|
||||||
badge: 'Essentielle',
|
badge: t('providerTheme.deepseek.badge'),
|
||||||
subBadge: 'Technique & Éco',
|
subBadge: t('providerTheme.deepseek.subBadge'),
|
||||||
accentClass: 'border-cyan-500/30 text-cyan-600 dark:text-cyan-400 bg-cyan-500/5',
|
accentClass: 'border-cyan-500/30 text-cyan-600 dark:text-cyan-400 bg-cyan-500/5',
|
||||||
glowClass: 'from-cyan-500/10 dark:from-cyan-500/5 to-transparent',
|
glowClass: 'from-cyan-500/10 dark:from-cyan-500/5 to-transparent',
|
||||||
descriptionOverride: 'Traduction ultra-précise et économique, idéale pour les documents techniques et le code.'
|
descriptionOverride: t('providerTheme.deepseek.desc')
|
||||||
},
|
},
|
||||||
openai: {
|
openai: {
|
||||||
badge: 'Premium',
|
badge: t('providerTheme.openai.badge'),
|
||||||
subBadge: 'Haute Fidélité',
|
subBadge: t('providerTheme.openai.subBadge'),
|
||||||
accentClass: 'border-emerald-500/30 text-emerald-600 dark:text-emerald-400 bg-emerald-500/5',
|
accentClass: 'border-emerald-500/30 text-emerald-600 dark:text-emerald-400 bg-emerald-500/5',
|
||||||
glowClass: 'from-emerald-500/10 dark:from-emerald-500/5 to-transparent',
|
glowClass: 'from-emerald-500/10 dark:from-emerald-500/5 to-transparent',
|
||||||
descriptionOverride: 'Le standard mondial de l\'IA. Cohérence textuelle maximale et respect strict du style.'
|
descriptionOverride: t('providerTheme.openai.desc')
|
||||||
},
|
},
|
||||||
minimax: {
|
minimax: {
|
||||||
badge: 'Avancée',
|
badge: t('providerTheme.minimax.badge'),
|
||||||
subBadge: 'Performance',
|
subBadge: t('providerTheme.minimax.subBadge'),
|
||||||
accentClass: 'border-indigo-500/30 text-indigo-600 dark:text-indigo-400 bg-indigo-500/5',
|
accentClass: 'border-indigo-500/30 text-indigo-600 dark:text-indigo-400 bg-indigo-500/5',
|
||||||
glowClass: 'from-indigo-500/10 dark:from-indigo-500/5 to-transparent',
|
glowClass: 'from-indigo-500/10 dark:from-indigo-500/5 to-transparent',
|
||||||
descriptionOverride: 'Vitesse d\'exécution incroyable et excellente compréhension des structures complexes.'
|
descriptionOverride: t('providerTheme.minimax.desc')
|
||||||
},
|
},
|
||||||
openrouter: {
|
openrouter: {
|
||||||
badge: 'Express',
|
badge: t('providerTheme.openrouter.badge'),
|
||||||
subBadge: 'Multi-Modèles',
|
subBadge: t('providerTheme.openrouter.subBadge'),
|
||||||
accentClass: 'border-purple-500/30 text-purple-600 dark:text-purple-400 bg-purple-500/5',
|
accentClass: 'border-purple-500/30 text-purple-600 dark:text-purple-400 bg-purple-500/5',
|
||||||
glowClass: 'from-purple-500/10 dark:from-purple-500/5 to-transparent',
|
glowClass: 'from-purple-500/10 dark:from-purple-500/5 to-transparent',
|
||||||
descriptionOverride: 'Accès unifié aux meilleurs modèles open-source optimisés pour la traduction.'
|
descriptionOverride: t('providerTheme.openrouter.desc')
|
||||||
},
|
},
|
||||||
openrouter_premium: {
|
openrouter_premium: {
|
||||||
badge: 'Ultra',
|
badge: t('providerTheme.openrouter_premium.badge'),
|
||||||
subBadge: 'Maximum Context',
|
subBadge: t('providerTheme.openrouter_premium.subBadge'),
|
||||||
accentClass: 'border-rose-500/30 text-rose-600 dark:text-rose-400 bg-rose-500/5',
|
accentClass: 'border-rose-500/30 text-rose-600 dark:text-rose-400 bg-rose-500/5',
|
||||||
glowClass: 'from-rose-500/10 dark:from-rose-500/5 to-transparent',
|
glowClass: 'from-rose-500/10 dark:from-rose-500/5 to-transparent',
|
||||||
descriptionOverride: 'Traduction assistée par les modèles de pointe (GPT-4o, Claude Sonnet 4.6) pour documents longs.'
|
descriptionOverride: t('providerTheme.openrouter_premium.desc')
|
||||||
},
|
},
|
||||||
zai: {
|
zai: {
|
||||||
badge: 'Spécialisée',
|
badge: t('providerTheme.zai.badge'),
|
||||||
subBadge: 'Finance & Droit',
|
subBadge: t('providerTheme.zai.subBadge'),
|
||||||
accentClass: 'border-amber-500/30 text-amber-600 dark:text-amber-400 bg-amber-500/5',
|
accentClass: 'border-amber-500/30 text-amber-600 dark:text-amber-400 bg-amber-500/5',
|
||||||
glowClass: 'from-amber-500/10 dark:from-amber-500/5 to-transparent',
|
glowClass: 'from-amber-500/10 dark:from-amber-500/5 to-transparent',
|
||||||
descriptionOverride: 'Modèle affiné pour les terminologies métiers exigeantes (juridique, finance).'
|
descriptionOverride: t('providerTheme.zai.desc')
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const DEFAULT_LLM_THEME: CardTheme = {
|
const makeDefaultLlmTheme = (t: (k: string) => string): CardTheme => ({
|
||||||
badge: 'Moderne',
|
badge: t('providerTheme.default.badge'),
|
||||||
subBadge: 'Raisonnement IA',
|
subBadge: t('providerTheme.default.subBadge'),
|
||||||
accentClass: 'border-brand-accent/30 text-brand-accent bg-brand-accent/5',
|
accentClass: 'border-brand-accent/30 text-brand-accent bg-brand-accent/5',
|
||||||
glowClass: 'from-brand-accent/10 to-transparent',
|
glowClass: 'from-brand-accent/10 to-transparent',
|
||||||
descriptionOverride: 'Traduction par grand modèle linguistique (LLM) avec analyse sémantique avancée.'
|
descriptionOverride: t('providerTheme.default.desc')
|
||||||
};
|
});
|
||||||
|
|
||||||
const CLASSIC_THEMES: Record<string, { labelOverride?: string; descriptionOverride?: string }> = {
|
const CLASSIC_THEMES_FACTORY = (t: (k: string) => string): Record<string, { labelOverride?: string; descriptionOverride?: string }> => ({
|
||||||
google: {
|
google: {
|
||||||
labelOverride: 'Google Traduction',
|
labelOverride: t('providerTheme.classic.google.label'),
|
||||||
descriptionOverride: 'Traduction ultra-rapide couvrant plus de 130 langues. Recommandé pour les flux généraux.'
|
descriptionOverride: t('providerTheme.classic.google.desc')
|
||||||
},
|
},
|
||||||
deepl: {
|
deepl: {
|
||||||
labelOverride: 'DeepL Pro',
|
labelOverride: t('providerTheme.classic.deepl.label'),
|
||||||
descriptionOverride: 'Traduction haute précision réputée pour sa fluidité et ses formulations naturelles.'
|
descriptionOverride: t('providerTheme.classic.deepl.desc')
|
||||||
},
|
},
|
||||||
google_cloud: {
|
google_cloud: {
|
||||||
labelOverride: 'Google Cloud API',
|
labelOverride: t('providerTheme.classic.google_cloud.label'),
|
||||||
descriptionOverride: 'Moteur cloud professionnel optimisé pour le traitement de gros volumes de documents.'
|
descriptionOverride: t('providerTheme.classic.google_cloud.desc')
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
export function ProviderSelector({
|
export function ProviderSelector({
|
||||||
provider,
|
provider,
|
||||||
@@ -100,6 +100,11 @@ export function ProviderSelector({
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [activeTab, setActiveTab] = useState<'classic' | 'llm'>('classic');
|
const [activeTab, setActiveTab] = useState<'classic' | 'llm'>('classic');
|
||||||
|
|
||||||
|
// Theme maps built from i18n (so they follow the active locale)
|
||||||
|
const LLM_THEMES = LLM_THEMES_FACTORY(t);
|
||||||
|
const DEFAULT_LLM_THEME = makeDefaultLlmTheme(t);
|
||||||
|
const CLASSIC_THEMES = CLASSIC_THEMES_FACTORY(t);
|
||||||
|
|
||||||
// Filter providers
|
// Filter providers
|
||||||
const classicProviders = availableProviders.filter((p) => p.mode === 'classic');
|
const classicProviders = availableProviders.filter((p) => p.mode === 'classic');
|
||||||
const llmProviders = availableProviders.filter((p) => p.mode === 'llm');
|
const llmProviders = availableProviders.filter((p) => p.mode === 'llm');
|
||||||
@@ -118,7 +123,7 @@ export function ProviderSelector({
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-sm text-brand-dark/50 dark:text-white/40 py-4">
|
<div className="flex items-center gap-2 text-sm text-brand-dark/50 dark:text-white/40 py-4">
|
||||||
<Loader2 className="size-4 animate-spin text-brand-accent" />
|
<Loader2 className="size-4 animate-spin text-brand-accent" />
|
||||||
<span>{t('dashboard.translate.provider.loading') || 'Chargement des moteurs...'}</span>
|
<span>{t('dashboard.translate.provider.loading')}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -126,7 +131,7 @@ export function ProviderSelector({
|
|||||||
if (availableProviders.length === 0) {
|
if (availableProviders.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="rounded-xl border border-amber-200 bg-amber-50/50 px-4 py-3 text-xs text-amber-700 dark:border-amber-900/30 dark:bg-amber-950/20 dark:text-amber-400 font-light">
|
<p className="rounded-xl border border-amber-200 bg-amber-50/50 px-4 py-3 text-xs text-amber-700 dark:border-amber-900/30 dark:bg-amber-950/20 dark:text-amber-400 font-light">
|
||||||
{t('dashboard.translate.provider.noneConfigured') || 'Aucun fournisseur configuré'}
|
{t('dashboard.translate.provider.noneConfigured')}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -209,7 +214,7 @@ export function ProviderSelector({
|
|||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-[0.2em] text-brand-dark/40 dark:text-white/45">
|
<label className="text-[10px] font-bold uppercase tracking-[0.2em] text-brand-dark/40 dark:text-white/45">
|
||||||
{t('dashboard.translate.provider.sectionTitle') || 'Moteur de Traduction'}
|
{t('dashboard.translate.provider.sectionTitle')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -225,7 +230,7 @@ export function ProviderSelector({
|
|||||||
: 'text-brand-dark/50 dark:text-white/40 hover:text-brand-dark dark:hover:text-white'
|
: 'text-brand-dark/50 dark:text-white/40 hover:text-brand-dark dark:hover:text-white'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t('dashboard.translate.provider.tabStandard') || 'Standard'}
|
{t('dashboard.translate.provider.tabStandard')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -238,7 +243,7 @@ export function ProviderSelector({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Sparkles className={cn("size-3", activeTab === 'llm' ? 'text-brand-accent' : 'text-brand-dark/35 dark:text-white/30')} />
|
<Sparkles className={cn("size-3", activeTab === 'llm' ? 'text-brand-accent' : 'text-brand-dark/35 dark:text-white/30')} />
|
||||||
{t('dashboard.translate.provider.tabLLM') || 'Multi-Modèles IA'}
|
{t('dashboard.translate.provider.tabLLM')}
|
||||||
{!isPro && <Lock className="size-2.5 opacity-60 ml-0.5" />}
|
{!isPro && <Lock className="size-2.5 opacity-60 ml-0.5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,7 +257,7 @@ export function ProviderSelector({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-4 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-xl font-light">
|
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-4 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-xl font-light">
|
||||||
Aucun traducteur standard disponible.
|
{t('providerSelector.noClassic')}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -262,7 +267,7 @@ export function ProviderSelector({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-4 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-xl font-light">
|
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-4 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-xl font-light">
|
||||||
Aucun modèle IA configuré.
|
{t('providerSelector.noLlm')}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -278,7 +283,7 @@ export function ProviderSelector({
|
|||||||
<p className="text-[10px] text-brand-dark/60 dark:text-white/60 leading-normal font-light">
|
<p className="text-[10px] text-brand-dark/60 dark:text-white/60 leading-normal font-light">
|
||||||
<span className="font-bold text-brand-dark dark:text-white">{meta?.labelOverride || activeP.label} : </span>
|
<span className="font-bold text-brand-dark dark:text-white">{meta?.labelOverride || activeP.label} : </span>
|
||||||
{meta?.descriptionOverride || activeP.description}
|
{meta?.descriptionOverride || activeP.description}
|
||||||
<span className="block mt-1 font-bold text-brand-accent">Coût : 1 crédit par page</span>
|
<span className="block mt-1 font-bold text-brand-accent">{t('providerSelector.costOne')}</span>
|
||||||
</p>
|
</p>
|
||||||
) : null;
|
) : null;
|
||||||
})()
|
})()
|
||||||
@@ -291,7 +296,7 @@ export function ProviderSelector({
|
|||||||
<span className="font-bold text-brand-dark dark:text-white">{activeP.label} : </span>
|
<span className="font-bold text-brand-dark dark:text-white">{activeP.label} : </span>
|
||||||
{theme.descriptionOverride} <span className="opacity-50 text-[9px] font-mono">({activeP.model || 'default'})</span>
|
{theme.descriptionOverride} <span className="opacity-50 text-[9px] font-mono">({activeP.model || 'default'})</span>
|
||||||
<span className="block mt-1 font-bold text-brand-accent">
|
<span className="block mt-1 font-bold text-brand-accent">
|
||||||
Coût : {activeP.id === 'openrouter_premium' ? '5 crédits par page (Facteur Premium)' : '1 crédit par page'}
|
{activeP.id === 'openrouter_premium' ? t('providerSelector.costFive') : t('providerSelector.costOne')}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -306,16 +311,16 @@ export function ProviderSelector({
|
|||||||
<div className="p-3.5 rounded-xl border border-brand-accent/20 bg-brand-accent/[0.02] dark:bg-brand-accent/[0.01] text-center flex flex-col items-center gap-1.5">
|
<div className="p-3.5 rounded-xl border border-brand-accent/20 bg-brand-accent/[0.02] dark:bg-brand-accent/[0.01] text-center flex flex-col items-center gap-1.5">
|
||||||
<Sparkles className="size-3.5 text-brand-accent animate-pulse" />
|
<Sparkles className="size-3.5 text-brand-accent animate-pulse" />
|
||||||
<span className="text-[11px] font-bold text-brand-dark dark:text-white leading-none">
|
<span className="text-[11px] font-bold text-brand-dark dark:text-white leading-none">
|
||||||
{t('dashboard.translate.provider.llmDivider') || 'Intelligence Artificielle Active'}
|
{t('dashboard.translate.provider.llmDivider')}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-[9.5px] text-brand-dark/50 dark:text-white/50 leading-relaxed font-light">
|
<p className="text-[9.5px] text-brand-dark/50 dark:text-white/50 leading-relaxed font-light">
|
||||||
Débloquez la traduction contextuelle haut de gamme pour vos documents entiers.
|
{t('providerSelector.unlockContextual')}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="/pricing"
|
href="/pricing"
|
||||||
className="w-full inline-flex items-center justify-center py-1.5 rounded-lg bg-brand-dark dark:bg-white text-white dark:text-brand-dark text-[10px] font-semibold hover:opacity-95 active:scale-[0.98] transition-all shadow-sm"
|
className="w-full inline-flex items-center justify-center py-1.5 rounded-lg bg-brand-dark dark:bg-white text-white dark:text-brand-dark text-[10px] font-semibold hover:opacity-95 active:scale-[0.98] transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
{t('dashboard.translate.provider.upgrade') || 'Passer Pro'}
|
{t('dashboard.translate.provider.upgrade')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function TranslationComplete({
|
|||||||
: t('dashboard.translate.complete.descGeneric')}
|
: t('dashboard.translate.complete.descGeneric')}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3 inline-flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700 dark:border-emerald-800/50 dark:bg-emerald-950/30 dark:text-emerald-400">
|
<div className="mt-3 inline-flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700 dark:border-emerald-800/50 dark:bg-emerald-950/30 dark:text-emerald-400">
|
||||||
<TrendingUp className="size-3" /> Haute qualité
|
<TrendingUp className="size-3" /> {t('translateComplete.highQuality')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -122,17 +122,17 @@ export function TranslationComplete({
|
|||||||
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
|
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
|
||||||
<FileText className="size-4 text-emerald-600" />
|
<FileText className="size-4 text-emerald-600" />
|
||||||
<p className="text-sm font-bold text-foreground">142</p>
|
<p className="text-sm font-bold text-foreground">142</p>
|
||||||
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">Segments</p>
|
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">{t('translateComplete.segments')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
|
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
|
||||||
<Activity className="size-4 text-emerald-600" />
|
<Activity className="size-4 text-emerald-600" />
|
||||||
<p className="text-sm font-bold text-foreground">12.8k</p>
|
<p className="text-sm font-bold text-foreground">12.8k</p>
|
||||||
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">Caractères</p>
|
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">{t('translateComplete.characters')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
|
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
|
||||||
<Timer className="size-4 text-emerald-600" />
|
<Timer className="size-4 text-emerald-600" />
|
||||||
<p className="text-sm font-bold text-emerald-600">96%</p>
|
<p className="text-sm font-bold text-emerald-600">96%</p>
|
||||||
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">Confiance</p>
|
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">{t('translateComplete.confidence')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ export default function TranslatePage() {
|
|||||||
const tgtLangName = config.languages.find(l => l.code === config.targetLang)?.name || config.targetLang;
|
const tgtLangName = config.languages.find(l => l.code === config.targetLang)?.name || config.targetLang;
|
||||||
const activeStepIdx = getActiveStepIdx(submit.progress);
|
const activeStepIdx = getActiveStepIdx(submit.progress);
|
||||||
const qualityLabel = useMemo(() => getQualityLabel(t, config.provider), [t, config.provider]);
|
const qualityLabel = useMemo(() => getQualityLabel(t, config.provider), [t, config.provider]);
|
||||||
|
const fileTypeButtons = [
|
||||||
|
{ label: t('translate.fileType.word'), type: 'word' as const, icon: <FileText size={11} className="text-blue-500" /> },
|
||||||
|
{ label: t('translate.fileType.excel'), type: 'excel' as const, icon: <FileSpreadsheet size={11} className="text-green-500" /> },
|
||||||
|
{ label: t('translate.fileType.slides'), type: 'slides' as const, icon: <Presentation size={11} className="text-orange-500" /> },
|
||||||
|
{ label: t('translate.fileType.pdf'), type: 'pdf' as const, icon: <FileType size={11} className="text-red-500" /> },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full p-6 pb-24 lg:pb-8 lg:p-8 dark:bg-[#0a0a0a] selection:bg-brand-accent/10">
|
<div className="min-h-full p-6 pb-24 lg:pb-8 lg:p-8 dark:bg-[#0a0a0a] selection:bg-brand-accent/10">
|
||||||
@@ -191,19 +197,27 @@ export default function TranslatePage() {
|
|||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
{showProcessing ? (
|
{showProcessing ? (
|
||||||
<>
|
<>
|
||||||
<span className="accent-pill mb-4 block w-fit italic">Traitement en cours</span>
|
<span className="accent-pill mb-4 block w-fit italic">{t('translate.header.processing')}</span>
|
||||||
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||||
Analyse IA <span className="italic">Active</span>
|
{(() => {
|
||||||
|
const full = t('translate.header.aiActive');
|
||||||
|
const i = full.lastIndexOf(' ');
|
||||||
|
return <>{i === -1 ? full : <>{full.slice(0, i)} <span className="italic">{full.slice(i + 1)}</span></>}</>;
|
||||||
|
})()}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
|
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
|
||||||
Votre mise en page est en cours de préservation par notre moteur contextuel.
|
{t('translate.header.aiActiveDesc')}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : showComplete ? (
|
) : showComplete ? (
|
||||||
<>
|
<>
|
||||||
<span className="accent-pill mb-4 block w-fit italic">Complété</span>
|
<span className="accent-pill mb-4 block w-fit italic">{t('translate.header.completed')}</span>
|
||||||
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||||
Traduction <span className="italic">terminée</span>
|
{(() => {
|
||||||
|
const full = t('translate.header.completedTitle');
|
||||||
|
const i = full.lastIndexOf(' ');
|
||||||
|
return <>{i === -1 ? full : <>{full.slice(0, i)} <span className="italic">{full.slice(i + 1)}</span></>}</>;
|
||||||
|
})()}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed truncate max-w-xl">
|
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed truncate max-w-xl">
|
||||||
{submit.fileName}
|
{submit.fileName}
|
||||||
@@ -211,12 +225,16 @@ export default function TranslatePage() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="accent-pill mb-4 block w-fit">Espace Pro</span>
|
<span className="accent-pill mb-4 block w-fit">{t('translate.header.proSpace')}</span>
|
||||||
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
|
||||||
Traduire un <span className="italic">document</span>
|
{(() => {
|
||||||
|
const full = t('translate.header.translateDoc');
|
||||||
|
const i = full.lastIndexOf(' ');
|
||||||
|
return <>{i === -1 ? full : <>{full.slice(0, i)} <span className="italic">{full.slice(i + 1)}</span></>}</>;
|
||||||
|
})()}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
|
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
|
||||||
Conservez la mise en page d'origine grâce au moteur de traduction ultra-haute fidélité.
|
{t('translate.header.translateDocDesc')}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -240,7 +258,7 @@ export default function TranslatePage() {
|
|||||||
onClick={() => dropzoneInputRef.current?.click()}
|
onClick={() => dropzoneInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
<div className="absolute top-4 right-4 text-[8px] font-bold uppercase tracking-widest text-brand-dark/30 dark:text-white/30 bg-brand-muted dark:bg-white/5 px-3 py-1 rounded-full border border-black/[0.03] dark:border-white/[0.03]">
|
<div className="absolute top-4 right-4 text-[8px] font-bold uppercase tracking-widest text-brand-dark/30 dark:text-white/30 bg-brand-muted dark:bg-white/5 px-3 py-1 rounded-full border border-black/[0.03] dark:border-white/[0.03]">
|
||||||
Format natif
|
{t('translate.upload.nativeFormat')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-16 h-16 bg-brand-muted dark:bg-white/5 rounded-2xl flex items-center justify-center text-brand-accent group-hover:scale-105 group-hover:bg-brand-dark dark:group-hover:bg-brand-accent group-hover:text-white dark:group-hover:text-brand-dark transition-all duration-300 mb-6 shadow-sm">
|
<div className="w-16 h-16 bg-brand-muted dark:bg-white/5 rounded-2xl flex items-center justify-center text-brand-accent group-hover:scale-105 group-hover:bg-brand-dark dark:group-hover:bg-brand-accent group-hover:text-white dark:group-hover:text-brand-dark transition-all duration-300 mb-6 shadow-sm">
|
||||||
@@ -256,12 +274,7 @@ export default function TranslatePage() {
|
|||||||
|
|
||||||
{/* Simulated file triggers */}
|
{/* Simulated file triggers */}
|
||||||
<div className="flex flex-wrap justify-center gap-2.5" onClick={(e) => e.stopPropagation()}>
|
<div className="flex flex-wrap justify-center gap-2.5" onClick={(e) => e.stopPropagation()}>
|
||||||
{[
|
{fileTypeButtons.map(f => (
|
||||||
{ label: 'Word (.docx)', type: 'word' as const, icon: <FileText size={11} className="text-blue-500" /> },
|
|
||||||
{ label: 'Excel (.xlsx)', type: 'excel' as const, icon: <FileSpreadsheet size={11} className="text-green-500" /> },
|
|
||||||
{ label: 'Slides (.pptx)', type: 'slides' as const, icon: <Presentation size={11} className="text-orange-500" /> },
|
|
||||||
{ label: 'PDF (.pdf)', type: 'pdf' as const, icon: <FileType size={11} className="text-red-500" /> },
|
|
||||||
].map(f => (
|
|
||||||
<button
|
<button
|
||||||
key={f.type}
|
key={f.type}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -310,17 +323,17 @@ export default function TranslatePage() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{submit.isSubmitting ? (
|
{submit.isSubmitting ? (
|
||||||
<><Loader2 className="size-4 animate-spin" /> {t('dashboard.translate.actions.uploading') || 'Soumission...'}</>
|
<><Loader2 className="size-4 animate-spin" /> {t('translate.submit')}</>
|
||||||
) : (
|
) : (
|
||||||
<>Lancer la traduction <ArrowRight size={13} className={upload.file ? 'text-brand-accent' : 'opacity-20'} /></>
|
<>{t('translate.startTranslation')} <ArrowRight size={13} className={upload.file ? 'text-brand-accent' : 'opacity-20'} /></>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{!upload.file && (
|
{!upload.file && (
|
||||||
<p className="text-center text-[8px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest">{t('dashboard.translate.noFile') || 'Veuillez charger un fichier'}</p>
|
<p className="text-center text-[8px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest">{t('translate.pleaseLoadFile')}</p>
|
||||||
)}
|
)}
|
||||||
{upload.file && !config.targetLang && (
|
{upload.file && !config.targetLang && (
|
||||||
<p className="text-center text-[8px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest">{t('dashboard.translate.noTargetLang') || 'Veuillez choisir une langue cible'}</p>
|
<p className="text-center text-[8px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest">{t('translate.chooseTargetLang')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between text-[7.5px] font-bold uppercase tracking-[0.15em] text-brand-dark/30 dark:text-white/30 border-t border-black/5 dark:border-white/5 pt-4">
|
<div className="flex justify-between text-[7.5px] font-bold uppercase tracking-[0.15em] text-brand-dark/30 dark:text-white/30 border-t border-black/5 dark:border-white/5 pt-4">
|
||||||
@@ -343,7 +356,7 @@ export default function TranslatePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-2xl font-serif font-medium text-brand-dark dark:text-white tracking-tight">
|
<h3 className="text-2xl font-serif font-medium text-brand-dark dark:text-white tracking-tight">
|
||||||
Moteur contextuel actif
|
{t('translate.contextEngineActive')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[10px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest mt-1">
|
<p className="text-[10px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest mt-1">
|
||||||
{submit.fileName || upload.file?.name}
|
{submit.fileName || upload.file?.name}
|
||||||
@@ -376,7 +389,7 @@ export default function TranslatePage() {
|
|||||||
|
|
||||||
<div className="flex justify-between items-end mt-12 pt-6">
|
<div className="flex justify-between items-end mt-12 pt-6">
|
||||||
<span className="text-[10px] font-bold text-brand-dark/30 dark:text-white/30 uppercase tracking-[0.3em]">
|
<span className="text-[10px] font-bold text-brand-dark/30 dark:text-white/30 uppercase tracking-[0.3em]">
|
||||||
{activeStepIdx < 2 ? 'Phase 1: Initialisation' : 'Phase 2: Reconstruction Contextuelle'}
|
{activeStepIdx < 2 ? t('translate.phase1') : t('translate.phase2')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-7xl font-serif font-medium text-brand-dark dark:text-white leading-none">
|
<span className="text-7xl font-serif font-medium text-brand-dark dark:text-white leading-none">
|
||||||
{Math.round(submit.progress)}%
|
{Math.round(submit.progress)}%
|
||||||
@@ -384,10 +397,10 @@ export default function TranslatePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-4 pt-12 border-t border-black/5 dark:border-white/5">
|
<div className="grid grid-cols-4 gap-4 pt-12 border-t border-black/5 dark:border-white/5">
|
||||||
<StatBox icon={<FileText size={18} />} value={`${Math.round(submit.progress)}%`} label="segments" />
|
<StatBox icon={<FileText size={18} />} value={`${Math.round(submit.progress)}%`} label={t('translate.stat.segments')} />
|
||||||
<StatBox icon={<Zap size={18} />} value="99.9%" label="précision" />
|
<StatBox icon={<Zap size={18} />} value="99.9%" label={t('translate.stat.precision')} />
|
||||||
<StatBox icon={<Clock size={18} />} value="Turbo" label="vitesse" />
|
<StatBox icon={<Clock size={18} />} value="Turbo" label={t('translate.stat.speedLabel')} />
|
||||||
<StatBox icon={<Activity size={18} />} value={formatElapsed(elapsed)} label="temps" />
|
<StatBox icon={<Activity size={18} />} value={formatElapsed(elapsed)} label={t('translate.stat.time')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -402,7 +415,7 @@ export default function TranslatePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<p className="text-[13px] font-bold uppercase tracking-[0.1em] text-brand-dark dark:text-white">
|
<p className="text-[13px] font-bold uppercase tracking-[0.1em] text-brand-dark dark:text-white">
|
||||||
Traduction terminée
|
{t('translate.header.completedTitle')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-brand-dark/40 dark:text-white/40 font-bold uppercase mt-1 tracking-widest max-w-[300px] truncate">
|
<p className="text-[10px] text-brand-dark/40 dark:text-white/40 font-bold uppercase mt-1 tracking-widest max-w-[300px] truncate">
|
||||||
{submit.fileName}
|
{submit.fileName}
|
||||||
@@ -410,7 +423,7 @@ export default function TranslatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="px-5 py-2 bg-white dark:bg-[#1a1a1a] rounded-full text-[9px] font-bold uppercase tracking-widest text-brand-accent border border-brand-accent/20 shadow-sm shrink-0">
|
<span className="px-5 py-2 bg-white dark:bg-[#1a1a1a] rounded-full text-[9px] font-bold uppercase tracking-widest text-brand-accent border border-brand-accent/20 shadow-sm shrink-0">
|
||||||
✓ Qualité Maître
|
{t('translate.complete.masterQuality')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -420,13 +433,13 @@ export default function TranslatePage() {
|
|||||||
className="premium-button px-24 py-6 text-xl !rounded-full flex items-center gap-6 mb-8 group cursor-pointer hover:scale-[1.02] active:scale-95"
|
className="premium-button px-24 py-6 text-xl !rounded-full flex items-center gap-6 mb-8 group cursor-pointer hover:scale-[1.02] active:scale-95"
|
||||||
>
|
>
|
||||||
<Download size={28} className="group-hover:translate-y-1 transition-transform" />
|
<Download size={28} className="group-hover:translate-y-1 transition-transform" />
|
||||||
Télécharger
|
{t('translate.download')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleNewTranslation}
|
onClick={handleNewTranslation}
|
||||||
className="text-[10px] font-bold uppercase tracking-[0.3em] text-brand-dark/30 hover:text-brand-dark dark:text-white/30 dark:hover:text-white transition-colors"
|
className="text-[10px] font-bold uppercase tracking-[0.3em] text-brand-dark/30 hover:text-brand-dark dark:text-white/30 dark:hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
+ Nouvelle traduction
|
{t('translate.newTranslation')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -441,7 +454,7 @@ export default function TranslatePage() {
|
|||||||
<AlertTriangle className="size-5 text-red-500" />
|
<AlertTriangle className="size-5 text-red-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 text-left">
|
<div className="flex-1 min-w-0 text-left">
|
||||||
<p className="text-sm font-bold uppercase tracking-tight text-red-600 dark:text-red-400 mb-2">Erreur lors de la traduction</p>
|
<p className="text-sm font-bold uppercase tracking-tight text-red-600 dark:text-red-400 mb-2">{t('translate.failedTitle')}</p>
|
||||||
<p className="text-xs text-red-600/80 dark:text-red-300/80 leading-relaxed font-medium">{humanFriendlyError(submit.error)}</p>
|
<p className="text-xs text-red-600/80 dark:text-red-300/80 leading-relaxed font-medium">{humanFriendlyError(submit.error)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -459,7 +472,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 cursor-pointer hover:scale-[1.01] active:scale-98"
|
className="premium-button w-full py-5 text-[12px] uppercase tracking-[0.25em] flex items-center justify-center gap-3 !rounded-2xl cursor-pointer hover:scale-[1.01] active:scale-98"
|
||||||
>
|
>
|
||||||
<RotateCcw size={18} />
|
<RotateCcw size={18} />
|
||||||
Réessayer
|
{t('translate.retry')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -467,7 +480,7 @@ export default function TranslatePage() {
|
|||||||
className="w-full py-4 border border-black/10 dark:border-white/10 rounded-2xl text-[10px] font-bold uppercase tracking-[0.25em] text-brand-dark/50 dark:text-white/50 hover:text-brand-dark dark:hover:text-white transition-all flex items-center justify-center gap-3 cursor-pointer hover:bg-brand-muted/30 dark:hover:bg-white/5"
|
className="w-full py-4 border border-black/10 dark:border-white/10 rounded-2xl text-[10px] font-bold uppercase tracking-[0.25em] text-brand-dark/50 dark:text-white/50 hover:text-brand-dark dark:hover:text-white transition-all flex items-center justify-center gap-3 cursor-pointer hover:bg-brand-muted/30 dark:hover:bg-white/5"
|
||||||
>
|
>
|
||||||
<Upload size={16} />
|
<Upload size={16} />
|
||||||
Téléverser un autre fichier
|
{t('translate.uploadAnother')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -556,7 +569,7 @@ export default function TranslatePage() {
|
|||||||
{config.mode === 'classic' ? (
|
{config.mode === 'classic' ? (
|
||||||
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center">
|
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center">
|
||||||
<span className="text-[7.5px] font-black uppercase opacity-45 block">
|
<span className="text-[7.5px] font-black uppercase opacity-45 block">
|
||||||
Indisponible en mode Standard (IA uniquement)
|
{t('translate.unavailableStandard')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -590,7 +603,7 @@ export default function TranslatePage() {
|
|||||||
{t('dashboard.translate.pdfMode.preserveLayout') || 'Mise en page'}
|
{t('dashboard.translate.pdfMode.preserveLayout') || 'Mise en page'}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-[8px] text-brand-dark/40 dark:text-white/40 font-bold uppercase tracking-widest leading-relaxed">
|
<p className="mt-1.5 text-[8px] text-brand-dark/40 dark:text-white/40 font-bold uppercase tracking-widest leading-relaxed">
|
||||||
Conserver la mise en page
|
{t('translate.preserveLayoutDesc')}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -605,10 +618,10 @@ export default function TranslatePage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-tight text-brand-dark dark:text-white">
|
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-tight text-brand-dark dark:text-white">
|
||||||
<Languages className="size-3.5 text-brand-accent" />
|
<Languages className="size-3.5 text-brand-accent" />
|
||||||
Texte brut
|
{t('translate.textOnly')}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-[8px] text-brand-dark/40 dark:text-white/40 font-bold uppercase tracking-widest leading-relaxed">
|
<p className="mt-1.5 text-[8px] text-brand-dark/40 dark:text-white/40 font-bold uppercase tracking-widest leading-relaxed">
|
||||||
Traduction rapide du texte uniquement
|
{t('translate.textOnlyDesc')}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -624,7 +637,7 @@ export default function TranslatePage() {
|
|||||||
<div className="editorial-card p-6 bg-white dark:bg-[#141414] border-none shadow-editorial h-full">
|
<div className="editorial-card p-6 bg-white dark:bg-[#141414] border-none shadow-editorial h-full">
|
||||||
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] mb-8 flex items-center gap-3 text-brand-dark/45 dark:text-white/45 pb-3 border-b border-black/[0.03] dark:border-white/[0.03]">
|
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] mb-8 flex items-center gap-3 text-brand-dark/45 dark:text-white/45 pb-3 border-b border-black/[0.03] dark:border-white/[0.03]">
|
||||||
<div className="w-2 h-2 bg-brand-accent rounded-full animate-ping" />
|
<div className="w-2 h-2 bg-brand-accent rounded-full animate-ping" />
|
||||||
Moniteur IA
|
{t('translate.monitor')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{/* File summary */}
|
{/* File summary */}
|
||||||
@@ -670,8 +683,8 @@ export default function TranslatePage() {
|
|||||||
{/* Quality progress */}
|
{/* Quality progress */}
|
||||||
<div className="pt-6 border-t border-black/5 dark:border-white/5 text-left">
|
<div className="pt-6 border-t border-black/5 dark:border-white/5 text-left">
|
||||||
<div className="flex justify-between text-[9px] font-bold uppercase tracking-[0.2em] mb-3">
|
<div className="flex justify-between text-[9px] font-bold uppercase tracking-[0.2em] mb-3">
|
||||||
<span className="text-brand-dark/40 dark:text-white/40">Intégrité Layout</span>
|
<span className="text-brand-dark/40 dark:text-white/40">{t('translate.layoutIntegrity')}</span>
|
||||||
<span className="text-brand-accent">100% SECURE</span>
|
<span className="text-brand-accent">{t('translate.secureHundred')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-brand-muted dark:bg-white/5 rounded-full overflow-hidden p-0.5">
|
<div className="h-2 bg-brand-muted dark:bg-white/5 rounded-full overflow-hidden p-0.5">
|
||||||
<div
|
<div
|
||||||
@@ -685,7 +698,7 @@ export default function TranslatePage() {
|
|||||||
onClick={handleNewTranslation}
|
onClick={handleNewTranslation}
|
||||||
className="w-full mt-12 py-4 border border-red-100 text-red-500 rounded-2xl text-[9px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:bg-red-50 dark:border-red-950/20 dark:hover:bg-red-950/30 transition-all cursor-pointer"
|
className="w-full mt-12 py-4 border border-red-100 text-red-500 rounded-2xl text-[9px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:bg-red-50 dark:border-red-950/20 dark:hover:bg-red-950/30 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
⟳ Annuler le processus
|
{t('translate.cancelProcess')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -695,7 +708,7 @@ export default function TranslatePage() {
|
|||||||
<div className="editorial-card p-6 bg-white dark:bg-[#141414] border-none shadow-editorial h-full">
|
<div className="editorial-card p-6 bg-white dark:bg-[#141414] border-none shadow-editorial h-full">
|
||||||
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] mb-8 flex items-center gap-3 text-brand-dark/45 dark:text-white/45 pb-3 border-b border-black/[0.03] dark:border-white/[0.03]">
|
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] mb-8 flex items-center gap-3 text-brand-dark/45 dark:text-white/45 pb-3 border-b border-black/[0.03] dark:border-white/[0.03]">
|
||||||
<CheckCircle2 size={14} className="text-emerald-500" />
|
<CheckCircle2 size={14} className="text-emerald-500" />
|
||||||
Récapitulatif
|
{t('translate.summary')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="space-y-6 mb-8 px-2 text-left">
|
<div className="space-y-6 mb-8 px-2 text-left">
|
||||||
@@ -717,8 +730,8 @@ export default function TranslatePage() {
|
|||||||
|
|
||||||
<div className="pt-6 border-t border-black/5 dark:border-white/5 text-left">
|
<div className="pt-6 border-t border-black/5 dark:border-white/5 text-left">
|
||||||
<div className="flex justify-between text-[9px] font-bold uppercase tracking-[0.2em] mb-3">
|
<div className="flex justify-between text-[9px] font-bold uppercase tracking-[0.2em] mb-3">
|
||||||
<span className="text-brand-dark/40 dark:text-white/40">Intégrité Layout</span>
|
<span className="text-brand-dark/40 dark:text-white/40">{t('translate.layoutIntegrity')}</span>
|
||||||
<span className="text-brand-accent">100% OK</span>
|
<span className="text-brand-accent">{t('translate.okHundred')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-brand-muted dark:bg-white/5 rounded-full overflow-hidden p-0.5">
|
<div className="h-2 bg-brand-muted dark:bg-white/5 rounded-full overflow-hidden p-0.5">
|
||||||
<div className="h-full bg-brand-accent rounded-full" style={{ width: '100%' }} />
|
<div className="h-full bg-brand-accent rounded-full" style={{ width: '100%' }} />
|
||||||
@@ -773,9 +786,9 @@ export default function TranslatePage() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{submit.isSubmitting ? (
|
{submit.isSubmitting ? (
|
||||||
<><Loader2 className="size-4 animate-spin" /> {t('dashboard.translate.actions.uploading') || 'Soumission...'}</>
|
<><Loader2 className="size-4 animate-spin" /> {t('translate.submit')}</>
|
||||||
) : (
|
) : (
|
||||||
<>Lancer la traduction <ArrowRight size={13} className={upload.file ? 'text-brand-accent' : 'opacity-20'} /></>
|
<>{t('translate.startTranslation')} <ArrowRight size={13} className={upload.file ? 'text-brand-accent' : 'opacity-20'} /></>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ export default function PricingPage() {
|
|||||||
href={isLoggedIn ? "/dashboard" : "/"}
|
href={isLoggedIn ? "/dashboard" : "/"}
|
||||||
className="px-6 py-2 rounded-full text-[9px] font-black uppercase tracking-widest text-foreground/40 hover:text-foreground transition-all"
|
className="px-6 py-2 rounded-full text-[9px] font-black uppercase tracking-widest text-foreground/40 hover:text-foreground transition-all"
|
||||||
>
|
>
|
||||||
Dashboard
|
{t('pricing.dashboard')}
|
||||||
</Link>
|
</Link>
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<Link
|
<Link
|
||||||
@@ -486,7 +486,7 @@ export default function PricingPage() {
|
|||||||
? "bg-emerald-50 border-emerald-200 text-emerald-700 dark:bg-emerald-950/50 dark:border-emerald-800 dark:text-emerald-300"
|
? "bg-emerald-50 border-emerald-200 text-emerald-700 dark:bg-emerald-950/50 dark:border-emerald-800 dark:text-emerald-300"
|
||||||
: "bg-red-50 border-red-200 text-red-700 dark:bg-red-950/50 dark:border-red-800 dark:text-red-300"
|
: "bg-red-50 border-red-200 text-red-700 dark:bg-red-950/50 dark:border-red-800 dark:text-red-300"
|
||||||
)}>
|
)}>
|
||||||
<span className="text-lg">{toastMsg.type === 'ok' ? '✓' : '✕'}</span>
|
<span className="text-lg">{toastMsg.type === 'ok' ? t('pricing.okSymbol') : t('pricing.errSymbol')}</span>
|
||||||
<p className="text-sm flex-1">{toastMsg.text}</p>
|
<p className="text-sm flex-1">{toastMsg.text}</p>
|
||||||
<button onClick={() => setToastMsg(null)} className="text-muted-foreground hover:text-foreground text-lg leading-none">×</button>
|
<button onClick={() => setToastMsg(null)} className="text-muted-foreground hover:text-foreground text-lg leading-none">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import {
|
import {
|
||||||
Upload,
|
Upload,
|
||||||
FileText,
|
FileText,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
Presentation,
|
Presentation,
|
||||||
X,
|
X,
|
||||||
Download,
|
Download,
|
||||||
Loader2,
|
Loader2,
|
||||||
Cpu,
|
Cpu,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Brain,
|
Brain,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
File,
|
File,
|
||||||
Zap,
|
Zap,
|
||||||
@@ -35,6 +35,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { useTranslationStore, openaiModels, openrouterModels } from "@/lib/store";
|
import { useTranslationStore, openaiModels, openrouterModels } from "@/lib/store";
|
||||||
import { translateDocument, languages, providers, extractTextsFromDocument, reconstructDocument, TranslatedText } from "@/lib/api";
|
import { translateDocument, languages, providers, extractTextsFromDocument, reconstructDocument, TranslatedText } from "@/lib/api";
|
||||||
import { useWebLLM } from "@/lib/webllm";
|
import { useWebLLM } from "@/lib/webllm";
|
||||||
|
import { useI18n } from "@/lib/i18n";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const fileIcons: Record<string, React.ElementType> = {
|
const fileIcons: Record<string, React.ElementType> = {
|
||||||
@@ -54,13 +55,14 @@ interface FilePreviewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
|
const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
|
||||||
|
const { t } = useI18n();
|
||||||
const [preview, setPreview] = useState<string | null>(null);
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generatePreview = async () => {
|
const generatePreview = async () => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (file.type.startsWith('image/')) {
|
if (file.type.startsWith('image/')) {
|
||||||
@@ -120,7 +122,7 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" size="sm">
|
<Badge variant="outline" size="sm">
|
||||||
{getFileExtension(file.name).toUpperCase()}
|
{getFileExtension(file.name).toUpperCase()}
|
||||||
@@ -145,9 +147,9 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
|
|||||||
) : preview ? (
|
) : preview ? (
|
||||||
<div className="p-4 h-full overflow-hidden">
|
<div className="p-4 h-full overflow-hidden">
|
||||||
{file.type.startsWith('image/') ? (
|
{file.type.startsWith('image/') ? (
|
||||||
<img
|
<img
|
||||||
src={preview}
|
src={preview}
|
||||||
alt="Preview"
|
alt="Preview"
|
||||||
className="w-full h-full object-contain rounded"
|
className="w-full h-full object-contain rounded"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -167,7 +169,7 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
|
|||||||
<div className="flex items-center justify-between p-4 border-t border-border-subtle">
|
<div className="flex items-center justify-between p-4 border-t border-border-subtle">
|
||||||
<div className="flex items-center gap-2 text-sm text-text-tertiary">
|
<div className="flex items-center gap-2 text-sm text-text-tertiary">
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
Preview
|
{t('fileUploader.preview')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="ghost" size="icon-sm">
|
<Button variant="ghost" size="icon-sm">
|
||||||
@@ -184,6 +186,7 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function FileUploader() {
|
export function FileUploader() {
|
||||||
|
const { t } = useI18n();
|
||||||
const { settings } = useTranslationStore();
|
const { settings } = useTranslationStore();
|
||||||
const webllm = useWebLLM();
|
const webllm = useWebLLM();
|
||||||
|
|
||||||
@@ -198,7 +201,7 @@ export function FileUploader() {
|
|||||||
const [isTranslating, setTranslating] = useState(false);
|
const [isTranslating, setTranslating] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Sync with store settings when they change
|
// Sync with store settings when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTargetLanguage(settings.defaultTargetLanguage);
|
setTargetLanguage(settings.defaultTargetLanguage);
|
||||||
@@ -233,11 +236,11 @@ export function FileUploader() {
|
|||||||
// WebLLM specific validation
|
// WebLLM specific validation
|
||||||
if (provider === "webllm") {
|
if (provider === "webllm") {
|
||||||
if (!webllm.isWebGPUSupported()) {
|
if (!webllm.isWebGPUSupported()) {
|
||||||
setError("WebGPU is not supported in this browser. Please use Chrome 113+ or Edge 113+.");
|
setError(t('fileUploader.webgpuUnsupported'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!webllm.isLoaded) {
|
if (!webllm.isLoaded) {
|
||||||
setError("WebLLM model not loaded. Go to Settings > Translation Services to load a model first.");
|
setError(t('fileUploader.webllmNotLoaded'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +259,7 @@ export function FileUploader() {
|
|||||||
await handleServerTranslation();
|
await handleServerTranslation();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Translation failed");
|
setError(err instanceof Error ? err.message : t('fileUploader.translationError'));
|
||||||
} finally {
|
} finally {
|
||||||
setTranslating(false);
|
setTranslating(false);
|
||||||
setTranslationStatus("");
|
setTranslationStatus("");
|
||||||
@@ -275,15 +278,15 @@ export function FileUploader() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Extract texts from document
|
// Step 1: Extract texts from document
|
||||||
setTranslationStatus("Extracting texts from document...");
|
setTranslationStatus(t('fileUploader.extracting'));
|
||||||
setProgress(5);
|
setProgress(5);
|
||||||
const extractResult = await extractTextsFromDocument(file);
|
const extractResult = await extractTextsFromDocument(file);
|
||||||
|
|
||||||
if (extractResult.texts.length === 0) {
|
if (extractResult.texts.length === 0) {
|
||||||
throw new Error("No translatable text found in document");
|
throw new Error(t('fileUploader.noTranslatable'));
|
||||||
}
|
}
|
||||||
|
|
||||||
setTranslationStatus(`Found ${extractResult.texts.length} texts to translate`);
|
setTranslationStatus(t('fileUploader.foundTexts', { count: extractResult.texts.length }));
|
||||||
setProgress(10);
|
setProgress(10);
|
||||||
|
|
||||||
// Step 2: Translate each text using WebLLM
|
// Step 2: Translate each text using WebLLM
|
||||||
@@ -293,15 +296,19 @@ export function FileUploader() {
|
|||||||
|
|
||||||
for (let i = 0; i < totalTexts; i++) {
|
for (let i = 0; i < totalTexts; i++) {
|
||||||
const item = extractResult.texts[i];
|
const item = extractResult.texts[i];
|
||||||
setTranslationStatus(`Translating ${i + 1}/${totalTexts}: "${item.text.substring(0, 30)}..."`);
|
setTranslationStatus(t('fileUploader.translatingItem', {
|
||||||
|
current: String(i + 1),
|
||||||
|
total: String(totalTexts),
|
||||||
|
preview: item.text.substring(0, 30),
|
||||||
|
}));
|
||||||
|
|
||||||
const translatedText = await webllm.translate(
|
const translatedText = await webllm.translate(
|
||||||
item.text,
|
item.text,
|
||||||
langName,
|
langName,
|
||||||
settings.systemPrompt || undefined,
|
settings.systemPrompt || undefined,
|
||||||
settings.glossary || undefined
|
settings.glossary || undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
translations.push({
|
translations.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
translated_text: translatedText,
|
translated_text: translatedText,
|
||||||
@@ -313,7 +320,7 @@ export function FileUploader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Reconstruct document with translations
|
// Step 3: Reconstruct document with translations
|
||||||
setTranslationStatus("Reconstructing document...");
|
setTranslationStatus(t('fileUploader.reconstructing'));
|
||||||
setProgress(92);
|
setProgress(92);
|
||||||
const blob = await reconstructDocument(
|
const blob = await reconstructDocument(
|
||||||
extractResult.session_id,
|
extractResult.session_id,
|
||||||
@@ -322,7 +329,7 @@ export function FileUploader() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
setTranslationStatus("Translation complete!");
|
setTranslationStatus(t('fileUploader.translationComplete'));
|
||||||
|
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
setDownloadUrl(url);
|
setDownloadUrl(url);
|
||||||
@@ -406,10 +413,10 @@ export function FileUploader() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-3">
|
<CardTitle className="flex items-center gap-3">
|
||||||
<Upload className="h-5 w-5 text-primary" />
|
<Upload className="h-5 w-5 text-primary" />
|
||||||
Upload Document
|
{t('fileUploader.uploadDocument')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Drag and drop or click to select a file (Excel, Word, PowerPoint)
|
{t('fileUploader.uploadDesc')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
@@ -424,7 +431,7 @@ export function FileUploader() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} ref={fileInputRef} className="hidden" />
|
<input {...getInputProps()} ref={fileInputRef} className="hidden" />
|
||||||
|
|
||||||
{/* Upload Icon with animation */}
|
{/* Upload Icon with animation */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"w-16 h-16 mx-auto mb-4 rounded-2xl bg-primary/10 flex items-center justify-center transition-all duration-300",
|
"w-16 h-16 mx-auto mb-4 rounded-2xl bg-primary/10 flex items-center justify-center transition-all duration-300",
|
||||||
@@ -435,16 +442,16 @@ export function FileUploader() {
|
|||||||
isDragActive ? "scale-110" : ""
|
isDragActive ? "scale-110" : ""
|
||||||
)} />
|
)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-lg font-medium text-foreground mb-2">
|
<p className="text-lg font-medium text-foreground mb-2">
|
||||||
{isDragActive
|
{isDragActive
|
||||||
? "Drop your file here..."
|
? t('fileUploader.dropHere')
|
||||||
: "Drag & drop your document here"}
|
: t('fileUploader.dragAndDrop')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-text-tertiary mb-6">
|
<p className="text-sm text-text-tertiary mb-6">
|
||||||
or click to browse
|
{t('fileUploader.orClickBrowse')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Supported formats */}
|
{/* Supported formats */}
|
||||||
<div className="flex flex-wrap justify-center gap-3">
|
<div className="flex flex-wrap justify-center gap-3">
|
||||||
{[
|
{[
|
||||||
@@ -472,19 +479,19 @@ export function FileUploader() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-3">
|
<CardTitle className="flex items-center gap-3">
|
||||||
<Brain className="h-5 w-5 text-primary" />
|
<Brain className="h-5 w-5 text-primary" />
|
||||||
Translation Options
|
{t('fileUploader.translationOptions')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure your translation settings
|
{t('fileUploader.configureSettings')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Target Language */}
|
{/* Target Language */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label htmlFor="language" className="text-text-secondary font-medium">Target Language</Label>
|
<Label htmlFor="language" className="text-text-secondary font-medium">{t('fileUploader.targetLanguage')}</Label>
|
||||||
<Select value={targetLanguage} onValueChange={setTargetLanguage}>
|
<Select value={targetLanguage} onValueChange={setTargetLanguage}>
|
||||||
<SelectTrigger id="language" className="bg-surface border-border-subtle">
|
<SelectTrigger id="language" className="bg-surface border-border-subtle">
|
||||||
<SelectValue placeholder="Select language" />
|
<SelectValue placeholder={t('fileUploader.selectLanguage')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-surface-elevated border-border max-h-80">
|
<SelectContent className="bg-surface-elevated border-border max-h-80">
|
||||||
{languages.map((lang) => (
|
{languages.map((lang) => (
|
||||||
@@ -505,10 +512,10 @@ export function FileUploader() {
|
|||||||
|
|
||||||
{/* Provider Selection */}
|
{/* Provider Selection */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-text-secondary font-medium">Translation Provider</Label>
|
<Label className="text-text-secondary font-medium">{t('fileUploader.translationProvider')}</Label>
|
||||||
<Select value={provider} onValueChange={(value: ProviderType) => setProvider(value)}>
|
<Select value={provider} onValueChange={(value: ProviderType) => setProvider(value)}>
|
||||||
<SelectTrigger className="bg-surface border-border-subtle">
|
<SelectTrigger className="bg-surface border-border-subtle">
|
||||||
<SelectValue placeholder="Select provider" />
|
<SelectValue placeholder={t('fileUploader.selectProvider')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-surface-elevated border-border">
|
<SelectContent className="bg-surface-elevated border-border">
|
||||||
{providers.map((p) => (
|
{providers.map((p) => (
|
||||||
@@ -536,7 +543,7 @@ export function FileUploader() {
|
|||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
className="w-full justify-between text-primary hover:text-primary/80"
|
className="w-full justify-between text-primary hover:text-primary/80"
|
||||||
>
|
>
|
||||||
<span>Advanced Options</span>
|
<span>{t('fileUploader.advancedOptions')}</span>
|
||||||
<ChevronRight className={cn(
|
<ChevronRight className={cn(
|
||||||
"h-4 w-4 transition-transform duration-200",
|
"h-4 w-4 transition-transform duration-200",
|
||||||
showAdvanced && "rotate-90"
|
showAdvanced && "rotate-90"
|
||||||
@@ -547,7 +554,7 @@ export function FileUploader() {
|
|||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<div className="space-y-4 p-4 rounded-lg bg-surface/50 border border-border-subtle animate-slide-up">
|
<div className="space-y-4 p-4 rounded-lg bg-surface/50 border border-border-subtle animate-slide-up">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="translate-images" className="text-text-secondary">Translate Images</Label>
|
<Label htmlFor="translate-images" className="text-text-secondary">{t('fileUploader.translateImages')}</Label>
|
||||||
<Switch
|
<Switch
|
||||||
id="translate-images"
|
id="translate-images"
|
||||||
checked={translateImages}
|
checked={translateImages}
|
||||||
@@ -568,12 +575,12 @@ export function FileUploader() {
|
|||||||
{isTranslating ? (
|
{isTranslating ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="me-2 h-5 w-5 animate-spin" />
|
<Loader2 className="me-2 h-5 w-5 animate-spin" />
|
||||||
Translating...
|
{t('fileUploader.translating')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Zap className="me-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
<Zap className="me-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
||||||
Translate Document
|
{t('fileUploader.translateDocument')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -583,7 +590,7 @@ export function FileUploader() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-text-secondary">
|
<span className="text-text-secondary">
|
||||||
{translationStatus || "Processing..."}
|
{translationStatus || t('fileUploader.processing')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-primary font-medium">{Math.round(progress)}%</span>
|
<span className="text-primary font-medium">{Math.round(progress)}%</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -591,7 +598,7 @@ export function FileUploader() {
|
|||||||
{provider === "webllm" && (
|
{provider === "webllm" && (
|
||||||
<div className="flex items-center gap-2 text-xs text-text-tertiary p-3 rounded-lg bg-primary/5">
|
<div className="flex items-center gap-2 text-xs text-text-tertiary p-3 rounded-lg bg-primary/5">
|
||||||
<Cpu className="h-3 w-3" />
|
<Cpu className="h-3 w-3" />
|
||||||
Translating locally with WebLLM...
|
{t('fileUploader.translatingLocally')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -603,7 +610,7 @@ export function FileUploader() {
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertTriangle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
|
<AlertTriangle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-destructive mb-1">Translation Error</p>
|
<p className="text-sm font-medium text-destructive mb-1">{t('fileUploader.translationError')}</p>
|
||||||
<p className="text-sm text-destructive/80">{error}</p>
|
<p className="text-sm text-destructive/80">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -620,9 +627,9 @@ export function FileUploader() {
|
|||||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/20 flex items-center justify-center animate-pulse">
|
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/20 flex items-center justify-center animate-pulse">
|
||||||
<CheckCircle className="w-8 h-8 text-white" />
|
<CheckCircle className="w-8 h-8 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl mb-2">Translation Complete!</CardTitle>
|
<CardTitle className="text-2xl mb-2">{t('fileUploader.translationComplete')}</CardTitle>
|
||||||
<CardDescription className="mb-6">
|
<CardDescription className="mb-6">
|
||||||
Your document has been translated successfully while preserving all formatting.
|
{t('fileUploader.translationCompleteDesc')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
@@ -631,7 +638,7 @@ export function FileUploader() {
|
|||||||
className="group px-8"
|
className="group px-8"
|
||||||
>
|
>
|
||||||
<Download className="me-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
<Download className="me-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
||||||
Download Translated Document
|
{t('fileUploader.download')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import Link from "next/link"
|
import Link from "next/link";
|
||||||
|
import { useI18n } from "@/lib/i18n";
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-border py-6 text-center text-xs text-muted-foreground">
|
<footer className="border-t border-border py-6 text-center text-xs text-muted-foreground">
|
||||||
<div className="mx-auto max-w-5xl px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="mx-auto max-w-5xl px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<span>© 2026 Wordly. All rights reserved.</span>
|
<span>{t('landing.footer.rights')}</span>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<Link href="/pricing" className="hover:text-foreground transition-colors">
|
<Link href="/pricing" className="hover:text-foreground transition-colors">
|
||||||
Pricing
|
{t('landing.nav.pricing')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/pricing#terms" className="hover:text-foreground transition-colors">
|
<Link href="/pricing#terms" className="hover:text-foreground transition-colors">
|
||||||
Terms
|
{t('layout.footer.terms')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/pricing#privacy" className="hover:text-foreground transition-colors">
|
<Link href="/pricing#privacy" className="hover:text-foreground transition-colors">
|
||||||
Privacy
|
{t('layout.footer.privacy')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import Link from "next/link"
|
"use client";
|
||||||
import { Languages } from "lucide-react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import Link from "next/link";
|
||||||
|
import { Languages } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useI18n } from "@/lib/i18n";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-lg">
|
<header className="sticky top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-lg">
|
||||||
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
||||||
@@ -17,23 +21,23 @@ export function SiteHeader() {
|
|||||||
|
|
||||||
<nav className="hidden items-center gap-1 md:flex">
|
<nav className="hidden items-center gap-1 md:flex">
|
||||||
<Button variant="ghost" size="sm" asChild>
|
<Button variant="ghost" size="sm" asChild>
|
||||||
<Link href="/pricing">Pricing</Link>
|
<Link href="/pricing">{t('landing.nav.pricing')}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" asChild>
|
<Button variant="ghost" size="sm" asChild>
|
||||||
<Link href="/pricing#api">API Access</Link>
|
<Link href="/pricing#api">{t('layout.nav.apiAccess')}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div className="mx-2 h-4 w-px bg-border" />
|
<div className="mx-2 h-4 w-px bg-border" />
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link href="/dashboard">Login</Link>
|
<Link href="/dashboard">{t('landing.nav.login')}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link href="/dashboard">Login</Link>
|
<Link href="/dashboard">{t('landing.nav.login')}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user