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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user