From eda682163216550c1c3ec5880b65b5ba5b68c2eb Mon Sep 17 00:00:00 2001
From: sepehr
Date: Sun, 14 Jun 2026 12:45:12 +0200
Subject: [PATCH] i18n: fix missing keys and translate all non-admin frontend
strings
- 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)
---
frontend/src/app/checkout/success/page.tsx | 24 +-
.../app/dashboard/api-keys/ApiKeyTable.tsx | 2 +-
frontend/src/app/dashboard/api-keys/page.tsx | 2 +-
frontend/src/app/dashboard/page.tsx | 10 +-
frontend/src/app/dashboard/profile/page.tsx | 2 +-
frontend/src/app/dashboard/services/page.tsx | 7 +-
frontend/src/app/dashboard/settings/page.tsx | 6 +-
.../dashboard/translate/LanguageSelector.tsx | 11 +-
.../dashboard/translate/ProviderSelector.tsx | 95 +-
.../translate/TranslationComplete.tsx | 8 +-
frontend/src/app/dashboard/translate/page.tsx | 103 +-
frontend/src/app/pricing/page.tsx | 4 +-
frontend/src/components/file-uploader.tsx | 115 +-
.../src/components/layout/site-footer.tsx | 14 +-
.../src/components/layout/site-header.tsx | 20 +-
frontend/src/lib/i18n.tsx | 1956 +++++++++++++++++
16 files changed, 2188 insertions(+), 191 deletions(-)
diff --git a/frontend/src/app/checkout/success/page.tsx b/frontend/src/app/checkout/success/page.tsx
index 32ac2dc..a4a8084 100644
--- a/frontend/src/app/checkout/success/page.tsx
+++ b/frontend/src/app/checkout/success/page.tsx
@@ -5,6 +5,7 @@ import { useSearchParams, useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { API_BASE } from '@/lib/config';
import { CheckCircle2, XCircle, RefreshCw } from 'lucide-react';
+import { useI18n } from '@/lib/i18n';
/**
* /checkout/success
@@ -17,6 +18,7 @@ export default function CheckoutSuccessPage() {
const router = useRouter();
const sessionId = searchParams.get('session_id');
const queryClient = useQueryClient();
+ const { t } = useI18n();
const [status, setStatus] = useState<'syncing' | 'ok' | 'error'>('syncing');
const [message, setMessage] = useState('');
@@ -43,19 +45,23 @@ export default function CheckoutSuccessPage() {
try { data = await res.json(); } catch { /* ignore */ }
if (res.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'] });
// Redirect after 2s
setTimeout(() => router.replace('/dashboard/profile?tab=subscription'), 2000);
} else {
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);
}
})
.catch(() => {
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);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -67,24 +73,24 @@ export default function CheckoutSuccessPage() {
{status === 'syncing' && (
<>
- Activation en cours…
- Nous mettons à jour votre abonnement, veuillez patienter.
+ {t('checkout.activating')}
+ {t('checkout.activatingDesc')}
>
)}
{status === 'ok' && (
<>
- Paiement confirmé !
+ {t('checkout.paymentConfirmed')}
{message}
- Redirection vers votre profil…
+ {t('checkout.redirectingToProfile')}
>
)}
{status === 'error' && (
<>
- Paiement reçu
+ {t('checkout.paymentReceived')}
{message}
- Redirection…
+ {t('checkout.redirecting')}
>
)}
diff --git a/frontend/src/app/dashboard/api-keys/ApiKeyTable.tsx b/frontend/src/app/dashboard/api-keys/ApiKeyTable.tsx
index e77f0e6..5bfa105 100644
--- a/frontend/src/app/dashboard/api-keys/ApiKeyTable.tsx
+++ b/frontend/src/app/dashboard/api-keys/ApiKeyTable.tsx
@@ -109,7 +109,7 @@ export function ApiKeyTable({ keys, onRevoke, isRevoking }: ApiKeyTableProps) {
- {copiedId === key.id ? 'Copied!' : t('apiKeys.table.copyPrefix')}
+ {copiedId === key.id ? t('apiKeys.copied') : t('apiKeys.table.copyPrefix')}
diff --git a/frontend/src/app/dashboard/api-keys/page.tsx b/frontend/src/app/dashboard/api-keys/page.tsx
index fc28f84..6319f6d 100644
--- a/frontend/src/app/dashboard/api-keys/page.tsx
+++ b/frontend/src/app/dashboard/api-keys/page.tsx
@@ -197,7 +197,7 @@ export default function ApiKeysPage() {
{t('apiKeys.sectionTitle')}
- {keys.length > 0 ? `${keys[0].key_prefix}************************************` : 'No keys generated'}
+ {keys.length > 0 ? `${keys[0].key_prefix}************************************` : t('apiKeys.noKeysGenerated')}
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
index 112aa7c..2a31b0f 100644
--- a/frontend/src/app/dashboard/page.tsx
+++ b/frontend/src/app/dashboard/page.tsx
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { useUser } from './useUser';
+import { useI18n } from '@/lib/i18n';
import { API_BASE } from '@/lib/config';
/**
@@ -18,6 +19,7 @@ export default function DashboardPage() {
const checkoutSessionId = searchParams.get('session_id');
const [syncError, setSyncError] = useState(null);
const { refetch } = useUser();
+ const { t } = useI18n();
useEffect(() => {
if (!checkoutSessionId) {
@@ -42,20 +44,20 @@ export default function DashboardPage() {
if (!cancelled) {
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
- setSyncError(errData.message || 'Erreur lors de la synchronisation du paiement.');
+ setSyncError(errData.message || t('dashboard.checkoutSyncError'));
} else {
await refetch();
router.replace('/dashboard/translate');
}
}
} catch {
- if (!cancelled) setSyncError('Erreur réseau. Veuillez rafraîchir la page.');
+ if (!cancelled) setSyncError(t('dashboard.networkRefresh'));
}
};
runSync();
return () => { cancelled = true; };
- }, [checkoutSessionId, refetch, router]);
+ }, [checkoutSessionId, refetch, router, t]);
if (syncError) {
return (
@@ -65,7 +67,7 @@ export default function DashboardPage() {
onClick={() => router.replace('/dashboard/translate')}
className="text-xs text-muted-foreground underline"
>
- Continuer vers la traduction
+ {t('dashboard.continueToTranslate')}
);
diff --git a/frontend/src/app/dashboard/profile/page.tsx b/frontend/src/app/dashboard/profile/page.tsx
index 19a3332..ae8ba2a 100644
--- a/frontend/src/app/dashboard/profile/page.tsx
+++ b/frontend/src/app/dashboard/profile/page.tsx
@@ -207,7 +207,7 @@ export default function ProfilePage() {
)}>
{statusMsg.type === 'ok' ? : }
{statusMsg.text}
-
+
)}
diff --git a/frontend/src/app/dashboard/services/page.tsx b/frontend/src/app/dashboard/services/page.tsx
index abd0e86..4dd192c 100644
--- a/frontend/src/app/dashboard/services/page.tsx
+++ b/frontend/src/app/dashboard/services/page.tsx
@@ -5,8 +5,8 @@ import { Zap, CheckCircle2, Lock, Loader2, Globe, Brain } from 'lucide-react';
import { API_BASE } from '@/lib/config';
import { useI18n } from '@/lib/i18n';
-const FALLBACK_PROVIDERS = [
- { id: "google", label: "Google Traduction", description: "Traduction rapide, 130+ langues", mode: "classic" as const },
+const FALLBACK_PROVIDERS_FACTORY = (t: (k: string) => string) => [
+ { id: "google", label: t('services.fallback.google.label'), description: t('services.fallback.google.desc'), mode: "classic" as const },
];
interface AvailableProvider {
@@ -19,6 +19,7 @@ interface AvailableProvider {
export default function TranslationServicesPage() {
const { t } = useI18n();
+ const FALLBACK_PROVIDERS = FALLBACK_PROVIDERS_FACTORY(t);
const [providers, setProviders] = useState([]);
const [isLoading, setIsLoading] = useState(true);
@@ -43,7 +44,7 @@ export default function TranslationServicesPage() {
}
};
fetchProviders();
- }, []);
+ }, [FALLBACK_PROVIDERS]);
const classicProviders = providers.filter((p) => p.mode === "classic");
const llmProviders = providers.filter((p) => p.mode === "llm");
diff --git a/frontend/src/app/dashboard/settings/page.tsx b/frontend/src/app/dashboard/settings/page.tsx
index 86398e4..852828a 100644
--- a/frontend/src/app/dashboard/settings/page.tsx
+++ b/frontend/src/app/dashboard/settings/page.tsx
@@ -28,9 +28,9 @@ export default function GeneralSettingsPage() {
};
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: FileText, color: 'text-blue-500', name: 'Word', 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: 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: 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: t('settings.formats.powerpoint.name'), ext: '.pptx, .ppt', features: [t('settings.formats.slides'), t('settings.formats.notes'), t('settings.formats.images')] },
];
return (
diff --git a/frontend/src/app/dashboard/translate/LanguageSelector.tsx b/frontend/src/app/dashboard/translate/LanguageSelector.tsx
index 283f03f..ceecd66 100644
--- a/frontend/src/app/dashboard/translate/LanguageSelector.tsx
+++ b/frontend/src/app/dashboard/translate/LanguageSelector.tsx
@@ -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(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"
/>
{filtered.length === 0 && (
-
No results
+
{t('langSelector.noResults')}
)}
{filtered.map(lang => (
) : (
- Aucun modèle IA configuré.
+ {t('providerSelector.noLlm')}
)
)}
@@ -278,7 +283,7 @@ export function ProviderSelector({
{meta?.labelOverride || activeP.label} :
{meta?.descriptionOverride || activeP.description}
- Coût : 1 crédit par page
+ {t('providerSelector.costOne')}
) : null;
})()
@@ -291,7 +296,7 @@ export function ProviderSelector({
{activeP.label} :
{theme.descriptionOverride} ({activeP.model || 'default'})
- 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')}
) : null;
@@ -306,16 +311,16 @@ export function ProviderSelector({
)}
diff --git a/frontend/src/app/dashboard/translate/TranslationComplete.tsx b/frontend/src/app/dashboard/translate/TranslationComplete.tsx
index 930bc0b..59e4ddb 100644
--- a/frontend/src/app/dashboard/translate/TranslationComplete.tsx
+++ b/frontend/src/app/dashboard/translate/TranslationComplete.tsx
@@ -111,7 +111,7 @@ export function TranslationComplete({
: t('dashboard.translate.complete.descGeneric')}
- Haute qualité
+ {t('translateComplete.highQuality')}
@@ -122,17 +122,17 @@ export function TranslationComplete({
142
-
Segments
+
{t('translateComplete.segments')}
12.8k
-
Caractères
+
{t('translateComplete.characters')}
96%
-
Confiance
+
{t('translateComplete.confidence')}
diff --git a/frontend/src/app/dashboard/translate/page.tsx b/frontend/src/app/dashboard/translate/page.tsx
index 92d755b..5122121 100644
--- a/frontend/src/app/dashboard/translate/page.tsx
+++ b/frontend/src/app/dashboard/translate/page.tsx
@@ -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: },
+ { label: t('translate.fileType.excel'), type: 'excel' as const, icon: },
+ { label: t('translate.fileType.slides'), type: 'slides' as const, icon: },
+ { label: t('translate.fileType.pdf'), type: 'pdf' as const, icon: },
+ ];
return (
@@ -191,19 +197,27 @@ export default function TranslatePage() {
{showProcessing ? (
<>
-
Traitement en cours
+
{t('translate.header.processing')}
- Analyse IA Active
+ {(() => {
+ const full = t('translate.header.aiActive');
+ const i = full.lastIndexOf(' ');
+ return <>{i === -1 ? full : <>{full.slice(0, i)} {full.slice(i + 1)}>}>;
+ })()}
- Votre mise en page est en cours de préservation par notre moteur contextuel.
+ {t('translate.header.aiActiveDesc')}
>
) : showComplete ? (
<>
-
Complété
+
{t('translate.header.completed')}
- Traduction terminée
+ {(() => {
+ const full = t('translate.header.completedTitle');
+ const i = full.lastIndexOf(' ');
+ return <>{i === -1 ? full : <>{full.slice(0, i)} {full.slice(i + 1)}>}>;
+ })()}
{submit.fileName}
@@ -211,12 +225,16 @@ export default function TranslatePage() {
>
) : (
<>
- Espace Pro
+ {t('translate.header.proSpace')}
- Traduire un document
+ {(() => {
+ const full = t('translate.header.translateDoc');
+ const i = full.lastIndexOf(' ');
+ return <>{i === -1 ? full : <>{full.slice(0, i)} {full.slice(i + 1)}>}>;
+ })()}
- Conservez la mise en page d'origine grâce au moteur de traduction ultra-haute fidélité.
+ {t('translate.header.translateDocDesc')}
>
)}
@@ -240,7 +258,7 @@ export default function TranslatePage() {
onClick={() => dropzoneInputRef.current?.click()}
>
- Format natif
+ {t('translate.upload.nativeFormat')}
@@ -256,12 +274,7 @@ export default function TranslatePage() {
{/* Simulated file triggers */}
e.stopPropagation()}>
- {[
- { label: 'Word (.docx)', type: 'word' as const, icon:
},
- { label: 'Excel (.xlsx)', type: 'excel' as const, icon:
},
- { label: 'Slides (.pptx)', type: 'slides' as const, icon:
},
- { label: 'PDF (.pdf)', type: 'pdf' as const, icon:
},
- ].map(f => (
+ {fileTypeButtons.map(f => (
{submit.isSubmitting ? (
- <> {t('dashboard.translate.actions.uploading') || 'Soumission...'}>
+ <> {t('translate.submit')}>
) : (
- <>Lancer la traduction >
+ <>{t('translate.startTranslation')} >
)}
{!upload.file && (
-
{t('dashboard.translate.noFile') || 'Veuillez charger un fichier'}
+
{t('translate.pleaseLoadFile')}
)}
{upload.file && !config.targetLang && (
-
{t('dashboard.translate.noTargetLang') || 'Veuillez choisir une langue cible'}
+
{t('translate.chooseTargetLang')}
)}
@@ -343,7 +356,7 @@ export default function TranslatePage() {
- Moteur contextuel actif
+ {t('translate.contextEngineActive')}
{submit.fileName || upload.file?.name}
@@ -376,7 +389,7 @@ export default function TranslatePage() {
- {activeStepIdx < 2 ? 'Phase 1: Initialisation' : 'Phase 2: Reconstruction Contextuelle'}
+ {activeStepIdx < 2 ? t('translate.phase1') : t('translate.phase2')}
{Math.round(submit.progress)}%
@@ -384,10 +397,10 @@ export default function TranslatePage() {
- } value={`${Math.round(submit.progress)}%`} label="segments" />
- } value="99.9%" label="précision" />
- } value="Turbo" label="vitesse" />
- } value={formatElapsed(elapsed)} label="temps" />
+ } value={`${Math.round(submit.progress)}%`} label={t('translate.stat.segments')} />
+ } value="99.9%" label={t('translate.stat.precision')} />
+ } value="Turbo" label={t('translate.stat.speedLabel')} />
+ } value={formatElapsed(elapsed)} label={t('translate.stat.time')} />
)}
@@ -402,7 +415,7 @@ export default function TranslatePage() {
- Traduction terminée
+ {t('translate.header.completedTitle')}
{submit.fileName}
@@ -410,7 +423,7 @@ export default function TranslatePage() {
- ✓ Qualité Maître
+ {t('translate.complete.masterQuality')}
@@ -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"
>
- Télécharger
+ {t('translate.download')}
- + Nouvelle traduction
+ {t('translate.newTranslation')}
@@ -441,7 +454,7 @@ export default function TranslatePage() {
-
Erreur lors de la traduction
+
{t('translate.failedTitle')}
{humanFriendlyError(submit.error)}
@@ -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"
>
- Réessayer
+ {t('translate.retry')}
)}
- Téléverser un autre fichier
+ {t('translate.uploadAnother')}
@@ -556,7 +569,7 @@ export default function TranslatePage() {
{config.mode === 'classic' ? (
- Indisponible en mode Standard (IA uniquement)
+ {t('translate.unavailableStandard')}
) : (
@@ -590,7 +603,7 @@ export default function TranslatePage() {
{t('dashboard.translate.pdfMode.preserveLayout') || 'Mise en page'}
- Conserver la mise en page
+ {t('translate.preserveLayoutDesc')}
- Texte brut
+ {t('translate.textOnly')}
- Traduction rapide du texte uniquement
+ {t('translate.textOnlyDesc')}
@@ -624,7 +637,7 @@ export default function TranslatePage() {
- Moniteur IA
+ {t('translate.monitor')}
{/* File summary */}
@@ -670,8 +683,8 @@ export default function TranslatePage() {
{/* Quality progress */}
- Intégrité Layout
- 100% SECURE
+ {t('translate.layoutIntegrity')}
+ {t('translate.secureHundred')}
- ⟳ Annuler le processus
+ {t('translate.cancelProcess')}
)}
@@ -695,7 +708,7 @@ export default function TranslatePage() {
- Récapitulatif
+ {t('translate.summary')}
@@ -717,8 +730,8 @@ export default function TranslatePage() {
- Intégrité Layout
- 100% OK
+ {t('translate.layoutIntegrity')}
+ {t('translate.okHundred')}
@@ -773,9 +786,9 @@ export default function TranslatePage() {
)}
>
{submit.isSubmitting ? (
- <>
{t('dashboard.translate.actions.uploading') || 'Soumission...'}>
+ <>
{t('translate.submit')}>
) : (
- <>Lancer la traduction
>
+ <>{t('translate.startTranslation')}
>
)}
diff --git a/frontend/src/app/pricing/page.tsx b/frontend/src/app/pricing/page.tsx
index cb0a8d1..29ae928 100644
--- a/frontend/src/app/pricing/page.tsx
+++ b/frontend/src/app/pricing/page.tsx
@@ -464,7 +464,7 @@ export default function PricingPage() {
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"
>
- Dashboard
+ {t('pricing.dashboard')}
{isLoggedIn && (
-
{toastMsg.type === 'ok' ? '✓' : '✕'}
+
{toastMsg.type === 'ok' ? t('pricing.okSymbol') : t('pricing.errSymbol')}
{toastMsg.text}
setToastMsg(null)} className="text-muted-foreground hover:text-foreground text-lg leading-none">×
diff --git a/frontend/src/components/file-uploader.tsx b/frontend/src/components/file-uploader.tsx
index d74b40c..a5cb9bb 100644
--- a/frontend/src/components/file-uploader.tsx
+++ b/frontend/src/components/file-uploader.tsx
@@ -2,17 +2,17 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { useDropzone } from "react-dropzone";
-import {
- Upload,
- FileText,
- FileSpreadsheet,
- Presentation,
- X,
- Download,
- Loader2,
- Cpu,
- AlertTriangle,
- Brain,
+import {
+ Upload,
+ FileText,
+ FileSpreadsheet,
+ Presentation,
+ X,
+ Download,
+ Loader2,
+ Cpu,
+ AlertTriangle,
+ Brain,
CheckCircle,
File,
Zap,
@@ -35,6 +35,7 @@ import { Input } from "@/components/ui/input";
import { useTranslationStore, openaiModels, openrouterModels } from "@/lib/store";
import { translateDocument, languages, providers, extractTextsFromDocument, reconstructDocument, TranslatedText } from "@/lib/api";
import { useWebLLM } from "@/lib/webllm";
+import { useI18n } from "@/lib/i18n";
import { cn } from "@/lib/utils";
const fileIcons: Record
= {
@@ -54,13 +55,14 @@ interface FilePreviewProps {
}
const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
+ const { t } = useI18n();
const [preview, setPreview] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const generatePreview = async () => {
if (!file) return;
-
+
setLoading(true);
try {
if (file.type.startsWith('image/')) {
@@ -120,7 +122,7 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
-
+
{getFileExtension(file.name).toUpperCase()}
@@ -145,9 +147,9 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
) : preview ? (
{file.type.startsWith('image/') ? (
-

) : (
@@ -167,7 +169,7 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
- Preview
+ {t('fileUploader.preview')}
@@ -184,6 +186,7 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
};
export function FileUploader() {
+ const { t } = useI18n();
const { settings } = useTranslationStore();
const webllm = useWebLLM();
@@ -198,7 +201,7 @@ export function FileUploader() {
const [isTranslating, setTranslating] = useState(false);
const [progress, setProgress] = useState(0);
const fileInputRef = useRef(null);
-
+
// Sync with store settings when they change
useEffect(() => {
setTargetLanguage(settings.defaultTargetLanguage);
@@ -233,11 +236,11 @@ export function FileUploader() {
// WebLLM specific validation
if (provider === "webllm") {
if (!webllm.isWebGPUSupported()) {
- setError("WebGPU is not supported in this browser. Please use Chrome 113+ or Edge 113+.");
+ setError(t('fileUploader.webgpuUnsupported'));
return;
}
if (!webllm.isLoaded) {
- setError("WebLLM model not loaded. Go to Settings > Translation Services to load a model first.");
+ setError(t('fileUploader.webllmNotLoaded'));
return;
}
}
@@ -256,7 +259,7 @@ export function FileUploader() {
await handleServerTranslation();
}
} catch (err) {
- setError(err instanceof Error ? err.message : "Translation failed");
+ setError(err instanceof Error ? err.message : t('fileUploader.translationError'));
} finally {
setTranslating(false);
setTranslationStatus("");
@@ -275,15 +278,15 @@ export function FileUploader() {
try {
// Step 1: Extract texts from document
- setTranslationStatus("Extracting texts from document...");
+ setTranslationStatus(t('fileUploader.extracting'));
setProgress(5);
const extractResult = await extractTextsFromDocument(file);
-
+
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);
// Step 2: Translate each text using WebLLM
@@ -293,15 +296,19 @@ export function FileUploader() {
for (let i = 0; i < totalTexts; 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(
item.text,
langName,
settings.systemPrompt || undefined,
settings.glossary || undefined
);
-
+
translations.push({
id: item.id,
translated_text: translatedText,
@@ -313,7 +320,7 @@ export function FileUploader() {
}
// Step 3: Reconstruct document with translations
- setTranslationStatus("Reconstructing document...");
+ setTranslationStatus(t('fileUploader.reconstructing'));
setProgress(92);
const blob = await reconstructDocument(
extractResult.session_id,
@@ -322,7 +329,7 @@ export function FileUploader() {
);
setProgress(100);
- setTranslationStatus("Translation complete!");
+ setTranslationStatus(t('fileUploader.translationComplete'));
const url = URL.createObjectURL(blob);
setDownloadUrl(url);
@@ -406,10 +413,10 @@ export function FileUploader() {
- Upload Document
+ {t('fileUploader.uploadDocument')}
- Drag and drop or click to select a file (Excel, Word, PowerPoint)
+ {t('fileUploader.uploadDesc')}
@@ -424,7 +431,7 @@ export function FileUploader() {
)}
>
-
+
{/* Upload Icon with animation */}
-
+
{isDragActive
- ? "Drop your file here..."
- : "Drag & drop your document here"}
+ ? t('fileUploader.dropHere')
+ : t('fileUploader.dragAndDrop')}
- or click to browse
+ {t('fileUploader.orClickBrowse')}
-
+
{/* Supported formats */}
{[
@@ -472,19 +479,19 @@ export function FileUploader() {
- Translation Options
+ {t('fileUploader.translationOptions')}
- Configure your translation settings
+ {t('fileUploader.configureSettings')}
{/* Target Language */}
-
+