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

- 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:
2026-06-14 12:45:12 +02:00
parent 9b0b2ae6f9
commit eda6821632
16 changed files with 2188 additions and 191 deletions

View File

@@ -32,6 +32,7 @@ function Combobox({
placeholder: string;
onChange: (code: string) => void;
}) {
const { t } = useI18n();
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const ref = useRef<HTMLDivElement>(null);
@@ -81,13 +82,13 @@ function Combobox({
type="text"
value={query}
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"
/>
</div>
<div className="max-h-[160px] overflow-y-auto p-1 mt-1 space-y-0.5">
{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 => (
<button
@@ -143,7 +144,7 @@ export default function LanguageSelector({
<div className="grid grid-cols-11 gap-2 items-center">
{/* Source */}
<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
value={sourceLang}
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'
: 'cursor-not-allowed opacity-30'
)}
title="Inverser"
title={t('langSelector.swap')}
>
<ArrowRightLeft size={12} />
</button>
@@ -174,7 +175,7 @@ export default function LanguageSelector({
{/* Target */}
<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
value={targetLang}
options={languages}

View File

@@ -22,73 +22,73 @@ interface CardTheme {
descriptionOverride: string;
}
const LLM_THEMES: Record<string, CardTheme> = {
const LLM_THEMES_FACTORY = (t: (k: string) => string): Record<string, CardTheme> => ({
deepseek: {
badge: 'Essentielle',
subBadge: 'Technique & Éco',
badge: t('providerTheme.deepseek.badge'),
subBadge: t('providerTheme.deepseek.subBadge'),
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',
descriptionOverride: 'Traduction ultra-précise et économique, idéale pour les documents techniques et le code.'
descriptionOverride: t('providerTheme.deepseek.desc')
},
openai: {
badge: 'Premium',
subBadge: 'Haute Fidélité',
badge: t('providerTheme.openai.badge'),
subBadge: t('providerTheme.openai.subBadge'),
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',
descriptionOverride: 'Le standard mondial de l\'IA. Cohérence textuelle maximale et respect strict du style.'
descriptionOverride: t('providerTheme.openai.desc')
},
minimax: {
badge: 'Avancée',
subBadge: 'Performance',
badge: t('providerTheme.minimax.badge'),
subBadge: t('providerTheme.minimax.subBadge'),
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',
descriptionOverride: 'Vitesse d\'exécution incroyable et excellente compréhension des structures complexes.'
descriptionOverride: t('providerTheme.minimax.desc')
},
openrouter: {
badge: 'Express',
subBadge: 'Multi-Modèles',
badge: t('providerTheme.openrouter.badge'),
subBadge: t('providerTheme.openrouter.subBadge'),
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',
descriptionOverride: 'Accès unifié aux meilleurs modèles open-source optimisés pour la traduction.'
descriptionOverride: t('providerTheme.openrouter.desc')
},
openrouter_premium: {
badge: 'Ultra',
subBadge: 'Maximum Context',
badge: t('providerTheme.openrouter_premium.badge'),
subBadge: t('providerTheme.openrouter_premium.subBadge'),
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',
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: {
badge: 'Spécialisée',
subBadge: 'Finance & Droit',
badge: t('providerTheme.zai.badge'),
subBadge: t('providerTheme.zai.subBadge'),
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',
descriptionOverride: 'Modèle affiné pour les terminologies métiers exigeantes (juridique, finance).'
descriptionOverride: t('providerTheme.zai.desc')
}
};
});
const DEFAULT_LLM_THEME: CardTheme = {
badge: 'Moderne',
subBadge: 'Raisonnement IA',
const makeDefaultLlmTheme = (t: (k: string) => string): CardTheme => ({
badge: t('providerTheme.default.badge'),
subBadge: t('providerTheme.default.subBadge'),
accentClass: 'border-brand-accent/30 text-brand-accent bg-brand-accent/5',
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: {
labelOverride: 'Google Traduction',
descriptionOverride: 'Traduction ultra-rapide couvrant plus de 130 langues. Recommandé pour les flux généraux.'
labelOverride: t('providerTheme.classic.google.label'),
descriptionOverride: t('providerTheme.classic.google.desc')
},
deepl: {
labelOverride: 'DeepL Pro',
descriptionOverride: 'Traduction haute précision réputée pour sa fluidité et ses formulations naturelles.'
labelOverride: t('providerTheme.classic.deepl.label'),
descriptionOverride: t('providerTheme.classic.deepl.desc')
},
google_cloud: {
labelOverride: 'Google Cloud API',
descriptionOverride: 'Moteur cloud professionnel optimisé pour le traitement de gros volumes de documents.'
labelOverride: t('providerTheme.classic.google_cloud.label'),
descriptionOverride: t('providerTheme.classic.google_cloud.desc')
}
};
});
export function ProviderSelector({
provider,
@@ -100,6 +100,11 @@ export function ProviderSelector({
const { t } = useI18n();
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
const classicProviders = availableProviders.filter((p) => p.mode === 'classic');
const llmProviders = availableProviders.filter((p) => p.mode === 'llm');
@@ -118,7 +123,7 @@ export function ProviderSelector({
return (
<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" />
<span>{t('dashboard.translate.provider.loading') || 'Chargement des moteurs...'}</span>
<span>{t('dashboard.translate.provider.loading')}</span>
</div>
);
}
@@ -126,7 +131,7 @@ export function ProviderSelector({
if (availableProviders.length === 0) {
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">
{t('dashboard.translate.provider.noneConfigured') || 'Aucun fournisseur configuré'}
{t('dashboard.translate.provider.noneConfigured')}
</p>
);
}
@@ -209,7 +214,7 @@ export function ProviderSelector({
{/* Title */}
<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">
{t('dashboard.translate.provider.sectionTitle') || 'Moteur de Traduction'}
{t('dashboard.translate.provider.sectionTitle')}
</label>
</div>
@@ -225,7 +230,7 @@ export function ProviderSelector({
: '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
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')} />
{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" />}
</button>
</div>
@@ -252,7 +257,7 @@ export function ProviderSelector({
</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">
Aucun traducteur standard disponible.
{t('providerSelector.noClassic')}
</p>
)
) : (
@@ -262,7 +267,7 @@ export function ProviderSelector({
</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">
Aucun modèle IA configuré.
{t('providerSelector.noLlm')}
</p>
)
)}
@@ -278,7 +283,7 @@ export function ProviderSelector({
<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>
{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>
) : null;
})()
@@ -291,7 +296,7 @@ export function ProviderSelector({
<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>
<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>
</p>
) : 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">
<Sparkles className="size-3.5 text-brand-accent animate-pulse" />
<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>
<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>
<a
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"
>
{t('dashboard.translate.provider.upgrade') || 'Passer Pro'}
{t('dashboard.translate.provider.upgrade')}
</a>
</div>
)}

View File

@@ -111,7 +111,7 @@ export function TranslationComplete({
: t('dashboard.translate.complete.descGeneric')}
</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">
<TrendingUp className="size-3" /> Haute qualité
<TrendingUp className="size-3" /> {t('translateComplete.highQuality')}
</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">
<FileText className="size-4 text-emerald-600" />
<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 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" />
<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 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" />
<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>

View File

@@ -182,6 +182,12 @@ export default function TranslatePage() {
const tgtLangName = config.languages.find(l => l.code === config.targetLang)?.name || config.targetLang;
const activeStepIdx = getActiveStepIdx(submit.progress);
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 (
<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">
{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">
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>
<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>
</>
) : 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">
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>
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed truncate max-w-xl">
{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">
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>
<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>
</>
)}
@@ -240,7 +258,7 @@ export default function TranslatePage() {
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]">
Format natif
{t('translate.upload.nativeFormat')}
</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">
@@ -256,12 +274,7 @@ export default function TranslatePage() {
{/* Simulated file triggers */}
<div className="flex flex-wrap justify-center gap-2.5" onClick={(e) => e.stopPropagation()}>
{[
{ 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 => (
{fileTypeButtons.map(f => (
<button
key={f.type}
type="button"
@@ -310,17 +323,17 @@ export default function TranslatePage() {
)}
>
{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>
{!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 && (
<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">
@@ -343,7 +356,7 @@ export default function TranslatePage() {
</div>
<div className="text-left">
<h3 className="text-2xl font-serif font-medium text-brand-dark dark:text-white tracking-tight">
Moteur contextuel actif
{t('translate.contextEngineActive')}
</h3>
<p className="text-[10px] text-brand-dark/30 dark:text-white/30 font-bold uppercase tracking-widest mt-1">
{submit.fileName || upload.file?.name}
@@ -376,7 +389,7 @@ export default function TranslatePage() {
<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]">
{activeStepIdx < 2 ? 'Phase 1: Initialisation' : 'Phase 2: Reconstruction Contextuelle'}
{activeStepIdx < 2 ? t('translate.phase1') : t('translate.phase2')}
</span>
<span className="text-7xl font-serif font-medium text-brand-dark dark:text-white leading-none">
{Math.round(submit.progress)}%
@@ -384,10 +397,10 @@ export default function TranslatePage() {
</div>
<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={<Zap size={18} />} value="99.9%" label="précision" />
<StatBox icon={<Clock size={18} />} value="Turbo" label="vitesse" />
<StatBox icon={<Activity size={18} />} value={formatElapsed(elapsed)} label="temps" />
<StatBox icon={<FileText size={18} />} value={`${Math.round(submit.progress)}%`} label={t('translate.stat.segments')} />
<StatBox icon={<Zap size={18} />} value="99.9%" label={t('translate.stat.precision')} />
<StatBox icon={<Clock size={18} />} value="Turbo" label={t('translate.stat.speedLabel')} />
<StatBox icon={<Activity size={18} />} value={formatElapsed(elapsed)} label={t('translate.stat.time')} />
</div>
</div>
)}
@@ -402,7 +415,7 @@ export default function TranslatePage() {
</div>
<div className="text-left">
<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 className="text-[10px] text-brand-dark/40 dark:text-white/40 font-bold uppercase mt-1 tracking-widest max-w-[300px] truncate">
{submit.fileName}
@@ -410,7 +423,7 @@ export default function TranslatePage() {
</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">
✓ Qualité Maître
{t('translate.complete.masterQuality')}
</span>
</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"
>
<Download size={28} className="group-hover:translate-y-1 transition-transform" />
Télécharger
{t('translate.download')}
</button>
<button
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"
>
+ Nouvelle traduction
{t('translate.newTranslation')}
</button>
</div>
</div>
@@ -441,7 +454,7 @@ export default function TranslatePage() {
<AlertTriangle className="size-5 text-red-500" />
</div>
<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>
</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"
>
<RotateCcw size={18} />
Réessayer
{t('translate.retry')}
</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"
>
<Upload size={16} />
Téléverser un autre fichier
{t('translate.uploadAnother')}
</button>
</div>
</div>
@@ -556,7 +569,7 @@ export default function TranslatePage() {
{config.mode === 'classic' ? (
<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">
Indisponible en mode Standard (IA uniquement)
{t('translate.unavailableStandard')}
</span>
</div>
) : (
@@ -590,7 +603,7 @@ export default function TranslatePage() {
{t('dashboard.translate.pdfMode.preserveLayout') || 'Mise en page'}
</div>
<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>
</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">
<Languages className="size-3.5 text-brand-accent" />
Texte brut
{t('translate.textOnly')}
</div>
<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>
</button>
</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">
<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" />
Moniteur IA
{t('translate.monitor')}
</h4>
{/* File summary */}
@@ -670,8 +683,8 @@ export default function TranslatePage() {
{/* Quality progress */}
<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">
<span className="text-brand-dark/40 dark:text-white/40">Intégrité Layout</span>
<span className="text-brand-accent">100% SECURE</span>
<span className="text-brand-dark/40 dark:text-white/40">{t('translate.layoutIntegrity')}</span>
<span className="text-brand-accent">{t('translate.secureHundred')}</span>
</div>
<div className="h-2 bg-brand-muted dark:bg-white/5 rounded-full overflow-hidden p-0.5">
<div
@@ -685,7 +698,7 @@ export default function TranslatePage() {
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"
>
⟳ Annuler le processus
{t('translate.cancelProcess')}
</button>
</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">
<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" />
Récapitulatif
{t('translate.summary')}
</h4>
<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="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-accent">100% OK</span>
<span className="text-brand-dark/40 dark:text-white/40">{t('translate.layoutIntegrity')}</span>
<span className="text-brand-accent">{t('translate.okHundred')}</span>
</div>
<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%' }} />
@@ -773,9 +786,9 @@ export default function TranslatePage() {
)}
>
{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>
</div>