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

@@ -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<string, React.ElementType> = {
@@ -54,13 +55,14 @@ interface FilePreviewProps {
}
const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
const { t } = useI18n();
const [preview, setPreview] = useState<string | null>(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) => {
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" size="sm">
{getFileExtension(file.name).toUpperCase()}
@@ -145,9 +147,9 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
) : preview ? (
<div className="p-4 h-full overflow-hidden">
{file.type.startsWith('image/') ? (
<img
src={preview}
alt="Preview"
<img
src={preview}
alt="Preview"
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 gap-2 text-sm text-text-tertiary">
<Eye className="h-4 w-4" />
Preview
{t('fileUploader.preview')}
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon-sm">
@@ -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<HTMLInputElement>(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() {
<CardHeader>
<CardTitle className="flex items-center gap-3">
<Upload className="h-5 w-5 text-primary" />
Upload Document
{t('fileUploader.uploadDocument')}
</CardTitle>
<CardDescription>
Drag and drop or click to select a file (Excel, Word, PowerPoint)
{t('fileUploader.uploadDesc')}
</CardDescription>
</CardHeader>
<CardContent className="p-6">
@@ -424,7 +431,7 @@ export function FileUploader() {
)}
>
<input {...getInputProps()} ref={fileInputRef} className="hidden" />
{/* Upload Icon with animation */}
<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",
@@ -435,16 +442,16 @@ export function FileUploader() {
isDragActive ? "scale-110" : ""
)} />
</div>
<p className="text-lg font-medium text-foreground mb-2">
{isDragActive
? "Drop your file here..."
: "Drag & drop your document here"}
? t('fileUploader.dropHere')
: t('fileUploader.dragAndDrop')}
</p>
<p className="text-sm text-text-tertiary mb-6">
or click to browse
{t('fileUploader.orClickBrowse')}
</p>
{/* Supported formats */}
<div className="flex flex-wrap justify-center gap-3">
{[
@@ -472,19 +479,19 @@ export function FileUploader() {
<CardHeader>
<CardTitle className="flex items-center gap-3">
<Brain className="h-5 w-5 text-primary" />
Translation Options
{t('fileUploader.translationOptions')}
</CardTitle>
<CardDescription>
Configure your translation settings
{t('fileUploader.configureSettings')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Target Language */}
<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}>
<SelectTrigger id="language" className="bg-surface border-border-subtle">
<SelectValue placeholder="Select language" />
<SelectValue placeholder={t('fileUploader.selectLanguage')} />
</SelectTrigger>
<SelectContent className="bg-surface-elevated border-border max-h-80">
{languages.map((lang) => (
@@ -505,10 +512,10 @@ export function FileUploader() {
{/* Provider Selection */}
<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)}>
<SelectTrigger className="bg-surface border-border-subtle">
<SelectValue placeholder="Select provider" />
<SelectValue placeholder={t('fileUploader.selectProvider')} />
</SelectTrigger>
<SelectContent className="bg-surface-elevated border-border">
{providers.map((p) => (
@@ -536,7 +543,7 @@ export function FileUploader() {
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full justify-between text-primary hover:text-primary/80"
>
<span>Advanced Options</span>
<span>{t('fileUploader.advancedOptions')}</span>
<ChevronRight className={cn(
"h-4 w-4 transition-transform duration-200",
showAdvanced && "rotate-90"
@@ -547,7 +554,7 @@ export function FileUploader() {
{showAdvanced && (
<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">
<Label htmlFor="translate-images" className="text-text-secondary">Translate Images</Label>
<Label htmlFor="translate-images" className="text-text-secondary">{t('fileUploader.translateImages')}</Label>
<Switch
id="translate-images"
checked={translateImages}
@@ -568,12 +575,12 @@ export function FileUploader() {
{isTranslating ? (
<>
<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" />
Translate Document
{t('fileUploader.translateDocument')}
</>
)}
</Button>
@@ -583,7 +590,7 @@ export function FileUploader() {
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-text-secondary">
{translationStatus || "Processing..."}
{translationStatus || t('fileUploader.processing')}
</span>
<span className="text-primary font-medium">{Math.round(progress)}%</span>
</div>
@@ -591,7 +598,7 @@ export function FileUploader() {
{provider === "webllm" && (
<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" />
Translating locally with WebLLM...
{t('fileUploader.translatingLocally')}
</div>
)}
</div>
@@ -603,7 +610,7 @@ export function FileUploader() {
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
<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>
</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">
<CheckCircle className="w-8 h-8 text-white" />
</div>
<CardTitle className="text-2xl mb-2">Translation Complete!</CardTitle>
<CardTitle className="text-2xl mb-2">{t('fileUploader.translationComplete')}</CardTitle>
<CardDescription className="mb-6">
Your document has been translated successfully while preserving all formatting.
{t('fileUploader.translationCompleteDesc')}
</CardDescription>
<Button
onClick={handleDownload}
@@ -631,7 +638,7 @@ export function FileUploader() {
className="group px-8"
>
<Download className="me-2 h-5 w-5 transition-transform group-hover:scale-110" />
Download Translated Document
{t('fileUploader.download')}
</Button>
</CardContent>
</Card>

View File

@@ -1,22 +1,24 @@
import Link from "next/link"
import Link from "next/link";
import { useI18n } from "@/lib/i18n";
export function SiteFooter() {
const { t } = useI18n();
return (
<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">
<span>© 2026 Wordly. All rights reserved.</span>
<span>{t('landing.footer.rights')}</span>
<div className="flex items-center gap-6">
<Link href="/pricing" className="hover:text-foreground transition-colors">
Pricing
{t('landing.nav.pricing')}
</Link>
<Link href="/pricing#terms" className="hover:text-foreground transition-colors">
Terms
{t('layout.footer.terms')}
</Link>
<Link href="/pricing#privacy" className="hover:text-foreground transition-colors">
Privacy
{t('layout.footer.privacy')}
</Link>
</div>
</div>
</footer>
)
);
}

View File

@@ -1,8 +1,12 @@
import Link from "next/link"
import { Languages } from "lucide-react"
import { Button } from "@/components/ui/button"
"use client";
import Link from "next/link";
import { Languages } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useI18n } from "@/lib/i18n";
export function SiteHeader() {
const { t } = useI18n();
return (
<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">
@@ -17,23 +21,23 @@ export function SiteHeader() {
<nav className="hidden items-center gap-1 md:flex">
<Button variant="ghost" size="sm" asChild>
<Link href="/pricing">Pricing</Link>
<Link href="/pricing">{t('landing.nav.pricing')}</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="/pricing#api">API Access</Link>
<Link href="/pricing#api">{t('layout.nav.apiAccess')}</Link>
</Button>
<div className="mx-2 h-4 w-px bg-border" />
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard">Login</Link>
<Link href="/dashboard">{t('landing.nav.login')}</Link>
</Button>
</nav>
<div className="md:hidden">
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard">Login</Link>
<Link href="/dashboard">{t('landing.nav.login')}</Link>
</Button>
</div>
</div>
</header>
)
);
}