1use std::sync::Arc;
14use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
15use std::time::{Instant, Duration};
16
17use crate::{
18 TranslationService as BaseTranslationService,
19 functional::{TextItem, TextFilter, BatchManager, Batch},
20 collector::{DomNode, TextCollector},
21 simple_config::SimpleTranslationConfig,
22 error::{TranslationError, Result as TranslationResult},
23};
24
25#[derive(Debug, Default)]
31pub struct EngineStats {
32 pub texts_processed: AtomicUsize,
34 pub batches_processed: AtomicUsize,
36 pub cache_hits: AtomicUsize,
38 pub cache_misses: AtomicUsize,
40 pub api_calls: AtomicUsize,
42 pub total_translation_time_ms: AtomicU64,
44 pub error_count: AtomicUsize,
46}
47
48impl EngineStats {
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn texts_processed(&self) -> usize {
54 self.texts_processed.load(Ordering::Relaxed)
55 }
56
57 pub fn batches_processed(&self) -> usize {
58 self.batches_processed.load(Ordering::Relaxed)
59 }
60
61 pub fn cache_hit_rate(&self) -> f64 {
62 let hits = self.cache_hits.load(Ordering::Relaxed);
63 let misses = self.cache_misses.load(Ordering::Relaxed);
64 let total = hits + misses;
65 if total == 0 {
66 0.0
67 } else {
68 hits as f64 / total as f64
69 }
70 }
71
72 pub fn average_translation_time_ms(&self) -> f64 {
73 let total_time = self.total_translation_time_ms.load(Ordering::Relaxed);
74 let api_calls = self.api_calls.load(Ordering::Relaxed);
75 if api_calls == 0 {
76 0.0
77 } else {
78 total_time as f64 / api_calls as f64
79 }
80 }
81
82 pub fn error_rate(&self) -> f64 {
83 let errors = self.error_count.load(Ordering::Relaxed);
84 let total = self.api_calls.load(Ordering::Relaxed);
85 if total == 0 {
86 0.0
87 } else {
88 errors as f64 / total as f64
89 }
90 }
91
92 fn increment_texts_processed(&self, count: usize) {
93 self.texts_processed.fetch_add(count, Ordering::Relaxed);
94 }
95
96 fn increment_batches_processed(&self) {
97 self.batches_processed.fetch_add(1, Ordering::Relaxed);
98 }
99
100 fn increment_cache_hits(&self) {
101 self.cache_hits.fetch_add(1, Ordering::Relaxed);
102 }
103
104 fn increment_cache_misses(&self) {
105 self.cache_misses.fetch_add(1, Ordering::Relaxed);
106 }
107
108 fn increment_api_calls(&self) {
109 self.api_calls.fetch_add(1, Ordering::Relaxed);
110 }
111
112 fn add_translation_time(&self, duration: Duration) {
113 self.total_translation_time_ms.fetch_add(
114 duration.as_millis() as u64,
115 Ordering::Relaxed
116 );
117 }
118
119 fn increment_errors(&self) {
120 self.error_count.fetch_add(1, Ordering::Relaxed);
121 }
122}
123
124use std::collections::HashMap;
129use std::sync::Mutex;
130
131struct SimpleCache {
133 cache: Mutex<HashMap<String, (String, Instant)>>,
134 ttl: Duration,
135}
136
137impl SimpleCache {
138 fn new(ttl: Duration) -> Self {
139 Self {
140 cache: Mutex::new(HashMap::new()),
141 ttl,
142 }
143 }
144
145 fn get(&self, key: &str) -> Option<String> {
146 let mut cache = self.cache.lock().ok()?;
147
148 if let Some((value, timestamp)) = cache.get(key) {
149 if timestamp.elapsed() < self.ttl {
150 return Some(value.clone());
151 } else {
152 cache.remove(key);
153 }
154 }
155
156 None
157 }
158
159 fn set(&self, key: String, value: String) {
160 if let Ok(mut cache) = self.cache.lock() {
161 cache.insert(key, (value, Instant::now()));
162 }
163 }
164
165 fn clear_expired(&self) {
166 if let Ok(mut cache) = self.cache.lock() {
167 let now = Instant::now();
168 cache.retain(|_, (_, timestamp)| now.duration_since(*timestamp) < self.ttl);
169 }
170 }
171}
172
173pub struct UnifiedTranslationEngine {
181 base_service: BaseTranslationService,
183 config: SimpleTranslationConfig,
185 filter: TextFilter,
187 collector: TextCollector,
189 batch_manager: BatchManager,
191 cache: Option<SimpleCache>,
193 stats: Arc<EngineStats>,
195}
196
197impl UnifiedTranslationEngine {
198 pub fn new(config: SimpleTranslationConfig) -> TranslationResult<Self> {
200 let base_service = BaseTranslationService::new(crate::types::TranslationConfig {
201 enabled: config.enabled,
202 source_lang: config.source_lang.clone(),
203 target_lang: config.target_lang.clone(),
204 deeplx_api_url: config.api_url.clone(),
205 max_requests_per_second: config.requests_per_second,
206 max_text_length: config.max_text_length,
207 max_paragraphs_per_request: 10, });
209
210 let cache = if config.cache_enabled {
211 Some(SimpleCache::new(config.cache_ttl()))
212 } else {
213 None
214 };
215
216 Ok(Self {
217 base_service,
218 config,
219 filter: TextFilter::new(),
220 collector: TextCollector::new(),
221 batch_manager: BatchManager::new(),
222 cache,
223 stats: Arc::new(EngineStats::new()),
224 })
225 }
226
227 pub fn quick(target_lang: &str, api_url: Option<&str>) -> TranslationResult<Self> {
229 let config = crate::simple_config::quick_config(target_lang, api_url);
230 Self::new(config)
231 }
232
233 pub async fn translate_text(&self, text: &str) -> TranslationResult<String> {
235 if !self.config.enabled {
236 return Ok(text.to_string());
237 }
238
239 if !self.filter.should_translate(text) {
240 return Ok(text.to_string());
241 }
242
243 let cache_key = format!("{}:{}", text, self.config.target_lang);
245 if let Some(ref cache) = self.cache {
246 if let Some(cached) = cache.get(&cache_key) {
247 self.stats.increment_cache_hits();
248 return Ok(cached);
249 }
250 self.stats.increment_cache_misses();
251 }
252
253 let start = Instant::now();
255 let result = self.base_service.translate(text).await
256 .map_err(|e| TranslationError::ApiError { code: 500, message: e.to_string() })?;
257
258 let duration = start.elapsed();
259 self.stats.add_translation_time(duration);
260 self.stats.increment_api_calls();
261 self.stats.increment_texts_processed(1);
262
263 if let Some(ref cache) = self.cache {
265 cache.set(cache_key, result.clone());
266 }
267
268 Ok(result)
269 }
270
271 pub async fn translate_texts(&self, texts: &[String]) -> TranslationResult<Vec<String>> {
273 if !self.config.enabled {
274 return Ok(texts.to_vec());
275 }
276
277 let translatable_items: Vec<TextItem> = texts
279 .iter()
280 .enumerate()
281 .filter_map(|(i, text)| {
282 if self.filter.should_translate(text) {
283 Some(crate::functional::create_text_item(
284 text.clone(),
285 format!("text[{}]", i)
286 ))
287 } else {
288 None
289 }
290 })
291 .collect();
292
293 let batches = self.batch_manager.create_batches(translatable_items);
295
296 let mut results = HashMap::new();
298 for batch in batches {
299 let batch_result = self.translate_batch(&batch).await?;
300 for (item, translated) in batch.items.iter().zip(batch_result.iter()) {
301 results.insert(item.location.clone(), translated.clone());
302 }
303 }
304
305 let translated: Vec<String> = texts
307 .iter()
308 .enumerate()
309 .map(|(i, original)| {
310 let location = format!("text[{}]", i);
311 results.get(&location).cloned().unwrap_or_else(|| original.clone())
312 })
313 .collect();
314
315 Ok(translated)
316 }
317
318 async fn translate_batch(&self, batch: &Batch) -> TranslationResult<Vec<String>> {
320 if batch.items.is_empty() {
321 return Ok(Vec::new());
322 }
323
324 let texts: Vec<&str> = batch.items.iter().map(|item| item.text.as_str()).collect();
325 let combined_text = texts.join("\n\n");
326
327 let start = Instant::now();
328 let translated = self.base_service.translate(&combined_text).await
329 .map_err(|e| {
330 self.stats.increment_errors();
331 TranslationError::ApiError { code: 500, message: e.to_string() }
332 })?;
333
334 let duration = start.elapsed();
335 self.stats.add_translation_time(duration);
336 self.stats.increment_api_calls();
337 self.stats.increment_batches_processed();
338 self.stats.increment_texts_processed(batch.items.len());
339
340 let translated_parts: Vec<String> = translated
342 .split("\n\n")
343 .map(|s| s.trim().to_string())
344 .collect();
345
346 if translated_parts.len() == texts.len() {
348 Ok(translated_parts)
349 } else {
350 Ok(texts.iter().map(|s| s.to_string()).collect())
352 }
353 }
354
355 pub async fn translate_dom_texts(&self, root: &dyn DomNode) -> TranslationResult<Vec<(String, String)>> {
357 if !self.config.enabled {
358 return Ok(Vec::new());
359 }
360
361 let text_items = self.collector.collect_texts(root);
363
364 let batches = self.batch_manager.create_batches(text_items);
366
367 let mut results = Vec::new();
369 for batch in batches {
370 let translations = self.translate_batch(&batch).await?;
371 for (item, translation) in batch.items.iter().zip(translations.iter()) {
372 results.push((item.location.clone(), translation.clone()));
373 }
374 }
375
376 Ok(results)
377 }
378
379 pub fn stats(&self) -> Arc<EngineStats> {
381 Arc::clone(&self.stats)
382 }
383
384 pub fn config(&self) -> &SimpleTranslationConfig {
386 &self.config
387 }
388
389 pub fn cleanup_cache(&self) {
391 if let Some(ref cache) = self.cache {
392 cache.clear_expired();
393 }
394 }
395
396 pub fn health_check(&self) -> EngineHealth {
398 let stats = &self.stats;
399
400 let error_rate = stats.error_rate();
401 let avg_time = stats.average_translation_time_ms();
402
403 let status = if error_rate > 0.5 {
404 HealthStatus::Critical
405 } else if error_rate > 0.2 || avg_time > 5000.0 {
406 HealthStatus::Warning
407 } else {
408 HealthStatus::Healthy
409 };
410
411 EngineHealth {
412 status,
413 error_rate,
414 average_response_time_ms: avg_time,
415 cache_hit_rate: stats.cache_hit_rate(),
416 total_requests: stats.api_calls.load(Ordering::Relaxed),
417 }
418 }
419}
420
421#[derive(Debug, Clone)]
427pub struct EngineHealth {
428 pub status: HealthStatus,
429 pub error_rate: f64,
430 pub average_response_time_ms: f64,
431 pub cache_hit_rate: f64,
432 pub total_requests: usize,
433}
434
435#[derive(Debug, Clone, PartialEq)]
437pub enum HealthStatus {
438 Healthy,
439 Warning,
440 Critical,
441}
442
443pub fn create_engine(target_lang: &str, api_url: Option<&str>) -> TranslationResult<UnifiedTranslationEngine> {
449 UnifiedTranslationEngine::quick(target_lang, api_url)
450}
451
452pub fn create_dev_engine() -> TranslationResult<UnifiedTranslationEngine> {
454 let config = crate::simple_config::presets::development();
455 UnifiedTranslationEngine::new(config)
456}
457
458pub fn create_prod_engine() -> TranslationResult<UnifiedTranslationEngine> {
460 let config = crate::simple_config::presets::production();
461 UnifiedTranslationEngine::new(config)
462}
463
464#[cfg(test)]
469mod tests {
470 use super::*;
471 use crate::collector::TestDomNode;
472
473 #[tokio::test]
474 async fn test_engine_creation() {
475 let engine = UnifiedTranslationEngine::quick("zh", Some("http://localhost:1188/translate"));
476 assert!(engine.is_ok());
477 }
478
479 #[tokio::test]
480 async fn test_engine_stats() {
481 let engine = UnifiedTranslationEngine::quick("zh", Some("http://localhost:1188/translate"))
482 .expect("Failed to create engine");
483
484 let stats = engine.stats();
485 assert_eq!(stats.texts_processed(), 0);
486 assert_eq!(stats.batches_processed(), 0);
487 }
488
489 #[tokio::test]
490 async fn test_health_check() {
491 let engine = UnifiedTranslationEngine::quick("zh", Some("http://localhost:1188/translate"))
492 .expect("Failed to create engine");
493
494 let health = engine.health_check();
495 assert_eq!(health.status, HealthStatus::Healthy);
496 }
497
498 #[tokio::test]
499 async fn test_disabled_engine() {
500 let mut config = crate::simple_config::quick_config("zh", Some("http://localhost:1188/translate"));
501 config.enabled = false;
502
503 let engine = UnifiedTranslationEngine::new(config)
504 .expect("Failed to create engine");
505
506 let result = engine.translate_text("Hello World").await.unwrap();
507 assert_eq!(result, "Hello World");
508 }
509
510 #[tokio::test]
511 async fn test_dom_text_collection() {
512 let mut config = crate::simple_config::quick_config("zh", Some("http://localhost:1188/translate"));
514 config.enabled = false;
515
516 let engine = UnifiedTranslationEngine::new(config)
517 .expect("Failed to create engine");
518
519 let root = TestDomNode::new_element("div")
520 .with_child(TestDomNode::new_text("Hello World"));
521
522 let results = engine.translate_dom_texts(&root).await;
523 assert!(results.is_ok());
524 }
525
526 #[test]
527 fn test_convenience_functions() {
528 let engine = create_engine("zh", Some("http://localhost:1188/translate"));
529 assert!(engine.is_ok());
530
531 let dev_engine = create_dev_engine();
532 assert!(dev_engine.is_ok());
533
534 let prod_engine = create_prod_engine();
535 assert!(prod_engine.is_ok());
536 }
537}