use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Instant, Duration};
use crate::{
TranslationService as BaseTranslationService,
functional::{TextItem, TextFilter, BatchManager, Batch},
collector::{DomNode, TextCollector},
simple_config::SimpleTranslationConfig,
error::{TranslationError, Result as TranslationResult},
};
#[derive(Debug, Default)]
pub struct EngineStats {
pub texts_processed: AtomicUsize,
pub batches_processed: AtomicUsize,
pub cache_hits: AtomicUsize,
pub cache_misses: AtomicUsize,
pub api_calls: AtomicUsize,
pub total_translation_time_ms: AtomicU64,
pub error_count: AtomicUsize,
}
impl EngineStats {
pub fn new() -> Self {
Self::default()
}
pub fn texts_processed(&self) -> usize {
self.texts_processed.load(Ordering::Relaxed)
}
pub fn batches_processed(&self) -> usize {
self.batches_processed.load(Ordering::Relaxed)
}
pub fn cache_hit_rate(&self) -> f64 {
let hits = self.cache_hits.load(Ordering::Relaxed);
let misses = self.cache_misses.load(Ordering::Relaxed);
let total = hits + misses;
if total == 0 {
0.0
} else {
hits as f64 / total as f64
}
}
pub fn average_translation_time_ms(&self) -> f64 {
let total_time = self.total_translation_time_ms.load(Ordering::Relaxed);
let api_calls = self.api_calls.load(Ordering::Relaxed);
if api_calls == 0 {
0.0
} else {
total_time as f64 / api_calls as f64
}
}
pub fn error_rate(&self) -> f64 {
let errors = self.error_count.load(Ordering::Relaxed);
let total = self.api_calls.load(Ordering::Relaxed);
if total == 0 {
0.0
} else {
errors as f64 / total as f64
}
}
fn increment_texts_processed(&self, count: usize) {
self.texts_processed.fetch_add(count, Ordering::Relaxed);
}
fn increment_batches_processed(&self) {
self.batches_processed.fetch_add(1, Ordering::Relaxed);
}
fn increment_cache_hits(&self) {
self.cache_hits.fetch_add(1, Ordering::Relaxed);
}
fn increment_cache_misses(&self) {
self.cache_misses.fetch_add(1, Ordering::Relaxed);
}
fn increment_api_calls(&self) {
self.api_calls.fetch_add(1, Ordering::Relaxed);
}
fn add_translation_time(&self, duration: Duration) {
self.total_translation_time_ms.fetch_add(
duration.as_millis() as u64,
Ordering::Relaxed
);
}
fn increment_errors(&self) {
self.error_count.fetch_add(1, Ordering::Relaxed);
}
}
use std::collections::HashMap;
use std::sync::Mutex;
struct SimpleCache {
cache: Mutex<HashMap<String, (String, Instant)>>,
ttl: Duration,
}
impl SimpleCache {
fn new(ttl: Duration) -> Self {
Self {
cache: Mutex::new(HashMap::new()),
ttl,
}
}
fn get(&self, key: &str) -> Option<String> {
let mut cache = self.cache.lock().ok()?;
if let Some((value, timestamp)) = cache.get(key) {
if timestamp.elapsed() < self.ttl {
return Some(value.clone());
} else {
cache.remove(key);
}
}
None
}
fn set(&self, key: String, value: String) {
if let Ok(mut cache) = self.cache.lock() {
cache.insert(key, (value, Instant::now()));
}
}
fn clear_expired(&self) {
if let Ok(mut cache) = self.cache.lock() {
let now = Instant::now();
cache.retain(|_, (_, timestamp)| now.duration_since(*timestamp) < self.ttl);
}
}
}
pub struct UnifiedTranslationEngine {
base_service: BaseTranslationService,
config: SimpleTranslationConfig,
filter: TextFilter,
collector: TextCollector,
batch_manager: BatchManager,
cache: Option<SimpleCache>,
stats: Arc<EngineStats>,
}
impl UnifiedTranslationEngine {
pub fn new(config: SimpleTranslationConfig) -> TranslationResult<Self> {
let base_service = BaseTranslationService::new(crate::types::TranslationConfig {
enabled: config.enabled,
source_lang: config.source_lang.clone(),
target_lang: config.target_lang.clone(),
deeplx_api_url: config.api_url.clone(),
max_requests_per_second: config.requests_per_second,
max_text_length: config.max_text_length,
max_paragraphs_per_request: 10, });
let cache = if config.cache_enabled {
Some(SimpleCache::new(config.cache_ttl()))
} else {
None
};
Ok(Self {
base_service,
config,
filter: TextFilter::new(),
collector: TextCollector::new(),
batch_manager: BatchManager::new(),
cache,
stats: Arc::new(EngineStats::new()),
})
}
pub fn quick(target_lang: &str, api_url: Option<&str>) -> TranslationResult<Self> {
let config = crate::simple_config::quick_config(target_lang, api_url);
Self::new(config)
}
pub async fn translate_text(&self, text: &str) -> TranslationResult<String> {
if !self.config.enabled {
return Ok(text.to_string());
}
if !self.filter.should_translate(text) {
return Ok(text.to_string());
}
let cache_key = format!("{}:{}", text, self.config.target_lang);
if let Some(ref cache) = self.cache {
if let Some(cached) = cache.get(&cache_key) {
self.stats.increment_cache_hits();
return Ok(cached);
}
self.stats.increment_cache_misses();
}
let start = Instant::now();
let result = self.base_service.translate(text).await
.map_err(|e| TranslationError::ApiError { code: 500, message: e.to_string() })?;
let duration = start.elapsed();
self.stats.add_translation_time(duration);
self.stats.increment_api_calls();
self.stats.increment_texts_processed(1);
if let Some(ref cache) = self.cache {
cache.set(cache_key, result.clone());
}
Ok(result)
}
pub async fn translate_texts(&self, texts: &[String]) -> TranslationResult<Vec<String>> {
if !self.config.enabled {
return Ok(texts.to_vec());
}
let translatable_items: Vec<TextItem> = texts
.iter()
.enumerate()
.filter_map(|(i, text)| {
if self.filter.should_translate(text) {
Some(crate::functional::create_text_item(
text.clone(),
format!("text[{}]", i)
))
} else {
None
}
})
.collect();
let batches = self.batch_manager.create_batches(translatable_items);
let mut results = HashMap::new();
for batch in batches {
let batch_result = self.translate_batch(&batch).await?;
for (item, translated) in batch.items.iter().zip(batch_result.iter()) {
results.insert(item.location.clone(), translated.clone());
}
}
let translated: Vec<String> = texts
.iter()
.enumerate()
.map(|(i, original)| {
let location = format!("text[{}]", i);
results.get(&location).cloned().unwrap_or_else(|| original.clone())
})
.collect();
Ok(translated)
}
async fn translate_batch(&self, batch: &Batch) -> TranslationResult<Vec<String>> {
if batch.items.is_empty() {
return Ok(Vec::new());
}
let texts: Vec<&str> = batch.items.iter().map(|item| item.text.as_str()).collect();
let combined_text = texts.join("\n\n");
let start = Instant::now();
let translated = self.base_service.translate(&combined_text).await
.map_err(|e| {
self.stats.increment_errors();
TranslationError::ApiError { code: 500, message: e.to_string() }
})?;
let duration = start.elapsed();
self.stats.add_translation_time(duration);
self.stats.increment_api_calls();
self.stats.increment_batches_processed();
self.stats.increment_texts_processed(batch.items.len());
let translated_parts: Vec<String> = translated
.split("\n\n")
.map(|s| s.trim().to_string())
.collect();
if translated_parts.len() == texts.len() {
Ok(translated_parts)
} else {
Ok(texts.iter().map(|s| s.to_string()).collect())
}
}
pub async fn translate_dom_texts(&self, root: &dyn DomNode) -> TranslationResult<Vec<(String, String)>> {
if !self.config.enabled {
return Ok(Vec::new());
}
let text_items = self.collector.collect_texts(root);
let batches = self.batch_manager.create_batches(text_items);
let mut results = Vec::new();
for batch in batches {
let translations = self.translate_batch(&batch).await?;
for (item, translation) in batch.items.iter().zip(translations.iter()) {
results.push((item.location.clone(), translation.clone()));
}
}
Ok(results)
}
pub fn stats(&self) -> Arc<EngineStats> {
Arc::clone(&self.stats)
}
pub fn config(&self) -> &SimpleTranslationConfig {
&self.config
}
pub fn cleanup_cache(&self) {
if let Some(ref cache) = self.cache {
cache.clear_expired();
}
}
pub fn health_check(&self) -> EngineHealth {
let stats = &self.stats;
let error_rate = stats.error_rate();
let avg_time = stats.average_translation_time_ms();
let status = if error_rate > 0.5 {
HealthStatus::Critical
} else if error_rate > 0.2 || avg_time > 5000.0 {
HealthStatus::Warning
} else {
HealthStatus::Healthy
};
EngineHealth {
status,
error_rate,
average_response_time_ms: avg_time,
cache_hit_rate: stats.cache_hit_rate(),
total_requests: stats.api_calls.load(Ordering::Relaxed),
}
}
}
#[derive(Debug, Clone)]
pub struct EngineHealth {
pub status: HealthStatus,
pub error_rate: f64,
pub average_response_time_ms: f64,
pub cache_hit_rate: f64,
pub total_requests: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HealthStatus {
Healthy,
Warning,
Critical,
}
pub fn create_engine(target_lang: &str, api_url: Option<&str>) -> TranslationResult<UnifiedTranslationEngine> {
UnifiedTranslationEngine::quick(target_lang, api_url)
}
pub fn create_dev_engine() -> TranslationResult<UnifiedTranslationEngine> {
let config = crate::simple_config::presets::development();
UnifiedTranslationEngine::new(config)
}
pub fn create_prod_engine() -> TranslationResult<UnifiedTranslationEngine> {
let config = crate::simple_config::presets::production();
UnifiedTranslationEngine::new(config)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collector::TestDomNode;
#[tokio::test]
async fn test_engine_creation() {
let engine = UnifiedTranslationEngine::quick("zh", Some("http://localhost:1188/translate"));
assert!(engine.is_ok());
}
#[tokio::test]
async fn test_engine_stats() {
let engine = UnifiedTranslationEngine::quick("zh", Some("http://localhost:1188/translate"))
.expect("Failed to create engine");
let stats = engine.stats();
assert_eq!(stats.texts_processed(), 0);
assert_eq!(stats.batches_processed(), 0);
}
#[tokio::test]
async fn test_health_check() {
let engine = UnifiedTranslationEngine::quick("zh", Some("http://localhost:1188/translate"))
.expect("Failed to create engine");
let health = engine.health_check();
assert_eq!(health.status, HealthStatus::Healthy);
}
#[tokio::test]
async fn test_disabled_engine() {
let mut config = crate::simple_config::quick_config("zh", Some("http://localhost:1188/translate"));
config.enabled = false;
let engine = UnifiedTranslationEngine::new(config)
.expect("Failed to create engine");
let result = engine.translate_text("Hello World").await.unwrap();
assert_eq!(result, "Hello World");
}
#[tokio::test]
async fn test_dom_text_collection() {
let mut config = crate::simple_config::quick_config("zh", Some("http://localhost:1188/translate"));
config.enabled = false;
let engine = UnifiedTranslationEngine::new(config)
.expect("Failed to create engine");
let root = TestDomNode::new_element("div")
.with_child(TestDomNode::new_text("Hello World"));
let results = engine.translate_dom_texts(&root).await;
assert!(results.is_ok());
}
#[test]
fn test_convenience_functions() {
let engine = create_engine("zh", Some("http://localhost:1188/translate"));
assert!(engine.is_ok());
let dev_engine = create_dev_engine();
assert!(dev_engine.is_ok());
let prod_engine = create_prod_engine();
assert!(prod_engine.is_ok());
}
}