Compare commits
2 Commits
fcabe882cd
...
b65e683d32
| Author | SHA1 | Date | |
|---|---|---|---|
| b65e683d32 | |||
| d2b820c6f1 |
@ -2,19 +2,18 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback, memo } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
Cloud,
|
Cloud,
|
||||||
BookText,
|
BookText,
|
||||||
Upload,
|
Upload,
|
||||||
Shield,
|
|
||||||
CreditCard,
|
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogIn,
|
LogIn,
|
||||||
Crown,
|
Crown,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
Server,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -38,12 +37,6 @@ const navigation = [
|
|||||||
icon: Upload,
|
icon: Upload,
|
||||||
description: "Translate documents",
|
description: "Translate documents",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "General Settings",
|
|
||||||
href: "/settings",
|
|
||||||
icon: Settings,
|
|
||||||
description: "Configure general settings",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Translation Services",
|
name: "Translation Services",
|
||||||
href: "/settings/services",
|
href: "/settings/services",
|
||||||
@ -56,23 +49,61 @@ const navigation = [
|
|||||||
icon: BookText,
|
icon: BookText,
|
||||||
description: "System prompts and glossary",
|
description: "System prompts and glossary",
|
||||||
},
|
},
|
||||||
];
|
|
||||||
|
|
||||||
const adminNavigation = [
|
|
||||||
{
|
{
|
||||||
name: "Admin Dashboard",
|
name: "General Settings",
|
||||||
href: "/admin",
|
href: "/settings",
|
||||||
icon: Shield,
|
icon: Settings,
|
||||||
description: "System monitoring (login required)",
|
description: "Configure general settings",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const planColors: Record<string, string> = {
|
||||||
|
free: "bg-zinc-600",
|
||||||
|
starter: "bg-blue-500",
|
||||||
|
pro: "bg-teal-500",
|
||||||
|
business: "bg-purple-500",
|
||||||
|
enterprise: "bg-amber-500",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memoized NavItem for performance
|
||||||
|
const NavItem = memo(function NavItem({
|
||||||
|
item,
|
||||||
|
isActive
|
||||||
|
}: {
|
||||||
|
item: typeof navigation[0];
|
||||||
|
isActive: boolean;
|
||||||
|
}) {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-teal-500/10 text-teal-400"
|
||||||
|
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>{item.description}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check for user in localStorage
|
setMounted(true);
|
||||||
const storedUser = localStorage.getItem("user");
|
const storedUser = localStorage.getItem("user");
|
||||||
if (storedUser) {
|
if (storedUser) {
|
||||||
try {
|
try {
|
||||||
@ -81,26 +112,38 @@ export function Sidebar() {
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Listen for storage changes
|
||||||
|
const handleStorage = (e: StorageEvent) => {
|
||||||
|
if (e.key === "user") {
|
||||||
|
setUser(e.newValue ? JSON.parse(e.newValue) : null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("storage", handleStorage);
|
||||||
|
return () => window.removeEventListener("storage", handleStorage);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = useCallback(() => {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem("refresh_token");
|
localStorage.removeItem("refresh_token");
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem("user");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const planColors: Record<string, string> = {
|
// Prevent hydration mismatch
|
||||||
free: "bg-zinc-600",
|
if (!mounted) {
|
||||||
starter: "bg-blue-500",
|
return (
|
||||||
pro: "bg-teal-500",
|
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-zinc-800 bg-[#1a1a1a]">
|
||||||
business: "bg-purple-500",
|
<div className="flex h-16 items-center gap-3 border-b border-zinc-800 px-6">
|
||||||
enterprise: "bg-amber-500",
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-teal-500 text-white font-bold">文A</div>
|
||||||
};
|
<span className="text-lg font-semibold text-white">Translate Co.</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider delayDuration={300}>
|
||||||
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-zinc-800 bg-[#1a1a1a]">
|
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-zinc-800 bg-[#1a1a1a]">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex h-16 items-center gap-3 border-b border-zinc-800 px-6">
|
<div className="flex h-16 items-center gap-3 border-b border-zinc-800 px-6">
|
||||||
@ -164,57 +207,51 @@ export function Sidebar() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
{user.plan === "free" && (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Link
|
|
||||||
href="/pricing"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
|
||||||
pathname === "/pricing"
|
|
||||||
? "bg-amber-500/10 text-amber-400"
|
|
||||||
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Crown className="h-5 w-5" />
|
|
||||||
<span>Upgrade Plan</span>
|
|
||||||
</Link>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p>View plans and pricing</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Admin Section */}
|
|
||||||
<div className="mt-4 pt-4 border-t border-zinc-800">
|
|
||||||
<p className="px-3 mb-2 text-xs font-medium text-zinc-600 uppercase tracking-wider">Admin</p>
|
|
||||||
{adminNavigation.map((item) => {
|
|
||||||
const isActive = pathname === item.href;
|
|
||||||
const Icon = item.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip key={item.name}>
|
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href="/pricing"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||||
isActive
|
pathname === "/pricing"
|
||||||
? "bg-blue-500/10 text-blue-400"
|
? "bg-amber-500/10 text-amber-400"
|
||||||
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
|
: "text-amber-400/70 hover:bg-zinc-800 hover:text-amber-400"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Crown className="h-5 w-5" />
|
||||||
<span>{item.name}</span>
|
<span>Upgrade Plan</span>
|
||||||
</Link>
|
</Link>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>{item.description}</p>
|
<p>Get more translations and features</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Self-Host Option */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-zinc-800">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href="/ollama-setup"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||||
|
pathname === "/ollama-setup"
|
||||||
|
? "bg-orange-500/10 text-orange-400"
|
||||||
|
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Server className="h-5 w-5" />
|
||||||
|
<span>Self-Host (Free)</span>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Run your own Ollama for unlimited free translations</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -222,17 +259,17 @@ export function Sidebar() {
|
|||||||
<div className="absolute bottom-0 left-0 right-0 border-t border-zinc-800 p-4">
|
<div className="absolute bottom-0 left-0 right-0 border-t border-zinc-800 p-4">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<Link href="/dashboard" className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-teal-600 text-white text-sm font-medium">
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-teal-500 to-teal-600 text-white text-sm font-medium shrink-0">
|
||||||
{user.name.charAt(0).toUpperCase()}
|
{user.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="text-sm font-medium text-white">{user.name}</span>
|
<span className="text-sm font-medium text-white truncate">{user.name}</span>
|
||||||
<Badge className={cn("text-xs mt-0.5", planColors[user.plan] || "bg-zinc-600")}>
|
<Badge className={cn("text-xs mt-0.5 w-fit", planColors[user.plan] || "bg-zinc-600")}>
|
||||||
{user.plan.charAt(0).toUpperCase() + user.plan.slice(1)}
|
{user.plan.charAt(0).toUpperCase() + user.plan.slice(1)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800"
|
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||||
|
|||||||
4
main.py
4
main.py
@ -21,6 +21,7 @@ import time
|
|||||||
from config import config
|
from config import config
|
||||||
from translators import excel_translator, word_translator, pptx_translator
|
from translators import excel_translator, word_translator, pptx_translator
|
||||||
from utils import file_handler, handle_translation_error, DocumentProcessingError
|
from utils import file_handler, handle_translation_error, DocumentProcessingError
|
||||||
|
from services.translation_service import _translation_cache
|
||||||
|
|
||||||
# Import auth routes
|
# Import auth routes
|
||||||
from routes.auth_routes import router as auth_router
|
from routes.auth_routes import router as auth_router
|
||||||
@ -228,7 +229,8 @@ async def health_check():
|
|||||||
"rate_limits": {
|
"rate_limits": {
|
||||||
"requests_per_minute": rate_limit_config.requests_per_minute,
|
"requests_per_minute": rate_limit_config.requests_per_minute,
|
||||||
"translations_per_minute": rate_limit_config.translations_per_minute,
|
"translations_per_minute": rate_limit_config.translations_per_minute,
|
||||||
}
|
},
|
||||||
|
"translation_cache": _translation_cache.stats()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,87 @@
|
|||||||
"""
|
"""
|
||||||
Translation Service Abstraction
|
Translation Service Abstraction
|
||||||
Provides a unified interface for different translation providers
|
Provides a unified interface for different translation providers
|
||||||
|
Optimized for high performance with parallel processing and caching
|
||||||
"""
|
"""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict, Tuple
|
||||||
import requests
|
import requests
|
||||||
from deep_translator import GoogleTranslator, DeeplTranslator, LibreTranslator
|
from deep_translator import GoogleTranslator, DeeplTranslator, LibreTranslator
|
||||||
from config import config
|
from config import config
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import threading
|
import threading
|
||||||
|
import asyncio
|
||||||
|
from functools import lru_cache
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
# Global thread pool for parallel translations
|
||||||
|
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=8)
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationCache:
|
||||||
|
"""Thread-safe LRU cache for translations to avoid redundant API calls"""
|
||||||
|
|
||||||
|
def __init__(self, maxsize: int = 5000):
|
||||||
|
self.cache: OrderedDict = OrderedDict()
|
||||||
|
self.maxsize = maxsize
|
||||||
|
self.lock = threading.RLock()
|
||||||
|
self.hits = 0
|
||||||
|
self.misses = 0
|
||||||
|
|
||||||
|
def _make_key(self, text: str, target_language: str, source_language: str, provider: str) -> str:
|
||||||
|
"""Create a unique cache key"""
|
||||||
|
content = f"{provider}:{source_language}:{target_language}:{text}"
|
||||||
|
return hashlib.md5(content.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
def get(self, text: str, target_language: str, source_language: str, provider: str) -> Optional[str]:
|
||||||
|
"""Get a cached translation if available"""
|
||||||
|
key = self._make_key(text, target_language, source_language, provider)
|
||||||
|
with self.lock:
|
||||||
|
if key in self.cache:
|
||||||
|
self.hits += 1
|
||||||
|
# Move to end (most recently used)
|
||||||
|
self.cache.move_to_end(key)
|
||||||
|
return self.cache[key]
|
||||||
|
self.misses += 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, text: str, target_language: str, source_language: str, provider: str, translation: str):
|
||||||
|
"""Cache a translation result"""
|
||||||
|
key = self._make_key(text, target_language, source_language, provider)
|
||||||
|
with self.lock:
|
||||||
|
if key in self.cache:
|
||||||
|
self.cache.move_to_end(key)
|
||||||
|
self.cache[key] = translation
|
||||||
|
# Remove oldest if exceeding maxsize
|
||||||
|
while len(self.cache) > self.maxsize:
|
||||||
|
self.cache.popitem(last=False)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clear the cache"""
|
||||||
|
with self.lock:
|
||||||
|
self.cache.clear()
|
||||||
|
self.hits = 0
|
||||||
|
self.misses = 0
|
||||||
|
|
||||||
|
def stats(self) -> Dict:
|
||||||
|
"""Get cache statistics"""
|
||||||
|
with self.lock:
|
||||||
|
total = self.hits + self.misses
|
||||||
|
hit_rate = (self.hits / total * 100) if total > 0 else 0
|
||||||
|
return {
|
||||||
|
"size": len(self.cache),
|
||||||
|
"maxsize": self.maxsize,
|
||||||
|
"hits": self.hits,
|
||||||
|
"misses": self.misses,
|
||||||
|
"hit_rate": f"{hit_rate:.1f}%"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Global translation cache
|
||||||
|
_translation_cache = TranslationCache(maxsize=5000)
|
||||||
|
|
||||||
|
|
||||||
class TranslationProvider(ABC):
|
class TranslationProvider(ABC):
|
||||||
@ -22,13 +95,44 @@ class TranslationProvider(ABC):
|
|||||||
def translate_batch(self, texts: List[str], target_language: str, source_language: str = 'auto') -> List[str]:
|
def translate_batch(self, texts: List[str], target_language: str, source_language: str = 'auto') -> List[str]:
|
||||||
"""Translate multiple texts at once - default implementation"""
|
"""Translate multiple texts at once - default implementation"""
|
||||||
return [self.translate(text, target_language, source_language) for text in texts]
|
return [self.translate(text, target_language, source_language) for text in texts]
|
||||||
|
|
||||||
|
def translate_batch_parallel(self, texts: List[str], target_language: str, source_language: str = 'auto', max_workers: int = 4) -> List[str]:
|
||||||
|
"""Parallel batch translation using thread pool"""
|
||||||
|
if not texts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
results = [''] * len(texts)
|
||||||
|
non_empty = [(i, t) for i, t in enumerate(texts) if t and t.strip()]
|
||||||
|
|
||||||
|
if not non_empty:
|
||||||
|
return [t if t else '' for t in texts]
|
||||||
|
|
||||||
|
def translate_one(item: Tuple[int, str]) -> Tuple[int, str]:
|
||||||
|
idx, text = item
|
||||||
|
try:
|
||||||
|
return (idx, self.translate(text, target_language, source_language))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Translation error at index {idx}: {e}")
|
||||||
|
return (idx, text)
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
for idx, translated in executor.map(translate_one, non_empty):
|
||||||
|
results[idx] = translated
|
||||||
|
|
||||||
|
# Fill empty positions
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
if not text or not text.strip():
|
||||||
|
results[i] = text if text else ''
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
class GoogleTranslationProvider(TranslationProvider):
|
class GoogleTranslationProvider(TranslationProvider):
|
||||||
"""Google Translate implementation with batch support"""
|
"""Google Translate implementation with batch support and caching"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._local = threading.local()
|
self._local = threading.local()
|
||||||
|
self.provider_name = "google"
|
||||||
|
|
||||||
def _get_translator(self, source_language: str, target_language: str) -> GoogleTranslator:
|
def _get_translator(self, source_language: str, target_language: str) -> GoogleTranslator:
|
||||||
"""Get or create a translator instance for the current thread"""
|
"""Get or create a translator instance for the current thread"""
|
||||||
@ -43,9 +147,17 @@ class GoogleTranslationProvider(TranslationProvider):
|
|||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
# Check cache first
|
||||||
|
cached = _translation_cache.get(text, target_language, source_language, self.provider_name)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
try:
|
try:
|
||||||
translator = self._get_translator(source_language, target_language)
|
translator = self._get_translator(source_language, target_language)
|
||||||
return translator.translate(text)
|
result = translator.translate(text)
|
||||||
|
# Cache the result
|
||||||
|
_translation_cache.set(text, target_language, source_language, self.provider_name, result)
|
||||||
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Translation error: {e}")
|
print(f"Translation error: {e}")
|
||||||
return text
|
return text
|
||||||
@ -53,7 +165,7 @@ class GoogleTranslationProvider(TranslationProvider):
|
|||||||
def translate_batch(self, texts: List[str], target_language: str, source_language: str = 'auto', batch_size: int = 50) -> List[str]:
|
def translate_batch(self, texts: List[str], target_language: str, source_language: str = 'auto', batch_size: int = 50) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Translate multiple texts using batch processing for speed.
|
Translate multiple texts using batch processing for speed.
|
||||||
Uses deep_translator's batch capability when possible.
|
Uses caching to avoid redundant translations.
|
||||||
"""
|
"""
|
||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
@ -62,15 +174,24 @@ class GoogleTranslationProvider(TranslationProvider):
|
|||||||
results = [''] * len(texts)
|
results = [''] * len(texts)
|
||||||
non_empty_indices = []
|
non_empty_indices = []
|
||||||
non_empty_texts = []
|
non_empty_texts = []
|
||||||
|
texts_to_translate = []
|
||||||
|
indices_to_translate = []
|
||||||
|
|
||||||
for i, text in enumerate(texts):
|
for i, text in enumerate(texts):
|
||||||
if text and text.strip():
|
if text and text.strip():
|
||||||
non_empty_indices.append(i)
|
# Check cache first
|
||||||
non_empty_texts.append(text)
|
cached = _translation_cache.get(text, target_language, source_language, self.provider_name)
|
||||||
|
if cached is not None:
|
||||||
|
results[i] = cached
|
||||||
|
else:
|
||||||
|
non_empty_indices.append(i)
|
||||||
|
non_empty_texts.append(text)
|
||||||
|
texts_to_translate.append(text)
|
||||||
|
indices_to_translate.append(i)
|
||||||
else:
|
else:
|
||||||
results[i] = text if text else ''
|
results[i] = text if text else ''
|
||||||
|
|
||||||
if not non_empty_texts:
|
if not texts_to_translate:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -78,8 +199,8 @@ class GoogleTranslationProvider(TranslationProvider):
|
|||||||
|
|
||||||
# Process in batches
|
# Process in batches
|
||||||
translated_texts = []
|
translated_texts = []
|
||||||
for i in range(0, len(non_empty_texts), batch_size):
|
for i in range(0, len(texts_to_translate), batch_size):
|
||||||
batch = non_empty_texts[i:i + batch_size]
|
batch = texts_to_translate[i:i + batch_size]
|
||||||
try:
|
try:
|
||||||
# Use translate_batch if available
|
# Use translate_batch if available
|
||||||
if hasattr(translator, 'translate_batch'):
|
if hasattr(translator, 'translate_batch'):
|
||||||
@ -107,16 +228,19 @@ class GoogleTranslationProvider(TranslationProvider):
|
|||||||
except:
|
except:
|
||||||
translated_texts.append(text)
|
translated_texts.append(text)
|
||||||
|
|
||||||
# Map back to original positions
|
# Map back to original positions and cache results
|
||||||
for idx, translated in zip(non_empty_indices, translated_texts):
|
for idx, (original, translated) in zip(indices_to_translate, zip(texts_to_translate, translated_texts)):
|
||||||
results[idx] = translated if translated else texts[idx]
|
result = translated if translated else texts[idx]
|
||||||
|
results[idx] = result
|
||||||
|
# Cache successful translations
|
||||||
|
_translation_cache.set(texts[idx], target_language, source_language, self.provider_name, result)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Batch translation failed: {e}")
|
print(f"Batch translation failed: {e}")
|
||||||
# Fallback to individual translations
|
# Fallback to individual translations
|
||||||
for idx, text in zip(non_empty_indices, non_empty_texts):
|
for idx, text in zip(indices_to_translate, texts_to_translate):
|
||||||
try:
|
try:
|
||||||
results[idx] = GoogleTranslator(source=source_language, target=target_language).translate(text) or text
|
results[idx] = GoogleTranslator(source=source_language, target=target_language).translate(text) or text
|
||||||
except:
|
except:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user