use crate::config::TranslationConfig;
use crate::error::{TranslationError, TranslationResult};
use crate::types::{TranslationRequest, TranslationResponse};
use reqwest::Client;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;
use tokio::time::sleep;
#[cfg(feature = "cache")]
use lru::LruCache;
#[cfg(feature = "cache")]
use crate::types::CacheEntry;
#[cfg(feature = "html-support")]
use markup5ever_rcdom::{Handle, NodeData, RcDom};
#[cfg(feature = "html-support")]
use html5ever::parse_document;
#[cfg(feature = "html-support")]
use html5ever::tendril::TendrilSink;
#[cfg(feature = "cache")]
#[derive(Clone)]
struct SimpleCache {
cache: Arc<std::sync::Mutex<LruCache<String, CacheEntry>>>,
ttl: Duration,
}
#[cfg(feature = "cache")]
impl SimpleCache {
fn new(capacity: usize, ttl: Duration) -> Self {
use std::sync::Mutex;
Self {
cache: Arc::new(Mutex::new(LruCache::new(capacity.try_into().unwrap()))),
ttl,
}
}
fn get(&self, key: &str) -> Option<String> {
let mut cache = self.cache.lock().unwrap();
if let Some(entry) = cache.get_mut(key) {
if !entry.is_expired(self.ttl) {
entry.access();
return Some(entry.translated_text.clone());
} else {
cache.pop(key);
}
}
None
}
fn insert(&self, key: String, value: String) {
let mut cache = self.cache.lock().unwrap();
cache.put(key, CacheEntry::new(value));
}
}
#[cfg(not(feature = "cache"))]
#[derive(Clone)]
struct SimpleCache;
#[cfg(not(feature = "cache"))]
impl SimpleCache {
fn new(_capacity: usize, _ttl: Duration) -> Self {
Self
}
fn get(&self, _key: &str) -> Option<String> {
None
}
fn insert(&self, _key: String, _value: String) {
}
}
#[derive(Clone)]
pub struct Translator {
client: Client,
config: TranslationConfig,
cache: SimpleCache,
semaphore: Arc<Semaphore>,
}
impl Translator {
pub fn new(config: TranslationConfig) -> TranslationResult<Self> {
config.validate()?;
let client = Client::builder()
.timeout(config.timeout)
.build()
.map_err(|e| {
TranslationError::Config(format!("Failed to create HTTP client: {}", e))
})?;
let cache = SimpleCache::new(1000, Duration::from_secs(3600)); let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
Ok(Self {
client,
config,
cache,
semaphore,
})
}
pub async fn translate_text(&self, text: &str, target_lang: &str) -> TranslationResult<String> {
if !crate::should_translate(text, target_lang) {
return Ok(text.to_string());
}
let cache_key = format!("{}:{}", text, target_lang);
if let Some(cached) = self.cache.get(&cache_key) {
return Ok(cached);
}
let result = self.translate_with_retry(text, target_lang).await?;
if self.config.enable_cache {
self.cache.insert(cache_key, result.clone());
}
Ok(result)
}
#[cfg(feature = "html-support")]
pub async fn translate_html(&self, html: &str, target_lang: &str) -> TranslationResult<String> {
use std::io::Cursor;
let dom = parse_document(markup5ever_rcdom::RcDom::default(), Default::default())
.from_utf8()
.read_from(&mut Cursor::new(html.as_bytes()))
.map_err(|e| TranslationError::Parse(format!("Failed to parse HTML: {}", e)))?;
let _translated_dom = self.translate_dom(dom, target_lang).await?;
Ok(html.to_string())
}
#[cfg(not(feature = "html-support"))]
pub async fn translate_html(
&self,
_html: &str,
_target_lang: &str,
) -> TranslationResult<String> {
Err(TranslationError::Config(
"HTML support not enabled. Enable 'html-support' feature".to_string(),
))
}
#[cfg(feature = "html-support")]
pub async fn translate_dom(&self, dom: RcDom, target_lang: &str) -> TranslationResult<RcDom> {
self.translate_node_recursive(&dom.document, target_lang)
.await?;
Ok(dom)
}
#[cfg(not(feature = "html-support"))]
pub async fn translate_dom(&self, _dom: RcDom, _target_lang: &str) -> TranslationResult<RcDom> {
Err(TranslationError::Config(
"HTML support not enabled. Enable 'html-support' feature".to_string(),
))
}
pub async fn translate_texts(
&self,
texts: Vec<&str>,
target_lang: &str,
) -> TranslationResult<Vec<String>> {
let mut results = Vec::new();
for text in texts {
let result = self.translate_text(text, target_lang).await?;
results.push(result);
}
Ok(results)
}
}
impl Translator {
async fn translate_with_retry(
&self,
text: &str,
target_lang: &str,
) -> TranslationResult<String> {
let mut retries = 0;
let max_retries = 3;
loop {
let _permit = self.semaphore.acquire().await.map_err(|_| {
TranslationError::Network("Semaphore acquisition failed".to_string())
})?;
match self.call_translation_api(text, target_lang).await {
Ok(result) => return Ok(result),
Err(e) if retries >= max_retries => return Err(e),
Err(e) if e.is_retryable() => {
retries += 1;
if let Some(delay_ms) = e.retry_delay_ms() {
sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => return Err(e),
}
}
}
async fn call_translation_api(
&self,
text: &str,
target_lang: &str,
) -> TranslationResult<String> {
let request = TranslationRequest {
text: text.to_string(),
source_lang: "auto".to_string(),
target_lang: target_lang.to_string(),
};
let response = self
.client
.post(&self.config.api_url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("User-Agent", "Mozilla/5.0 (compatible; TranslationLib/1.0)")
.json(&request)
.send()
.await?;
if !response.status().is_success() {
return Err(TranslationError::Api(format!(
"API request failed with status: {}",
response.status()
)));
}
let response_text = response.text().await?;
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
if let Some(code) = json_value.get("code").and_then(|v| v.as_i64()) {
if code == 200 {
if let Some(translated) = json_value.get("data").and_then(|v| v.as_str()) {
return Ok(translated.to_string());
}
} else {
return Err(TranslationError::Api(format!(
"API returned error code: {}",
code
)));
}
}
if let Some(translated) = json_value
.get("translated_text")
.or_else(|| json_value.get("result"))
.or_else(|| json_value.get("translation"))
.and_then(|v| v.as_str())
{
return Ok(translated.to_string());
}
if let Ok(translation_response) =
serde_json::from_str::<TranslationResponse>(&response_text)
{
return Ok(translation_response.translated_text);
}
}
Ok(response_text)
}
#[cfg(feature = "html-support")]
fn translate_node_recursive<'a>(
&'a self,
node: &'a Handle,
target_lang: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = TranslationResult<()>> + 'a>> {
Box::pin(async move {
match &node.data {
NodeData::Text { contents } => {
let text = contents.borrow().to_string();
if crate::should_translate(&text, target_lang) {
let translated = self.translate_text(&text, target_lang).await?;
*contents.borrow_mut() = translated.into();
}
}
NodeData::Element { name, attrs, .. } => {
let translatable_attrs = ["title", "alt", "placeholder"];
let mut attrs_mut = attrs.borrow_mut();
for attr in attrs_mut.iter_mut() {
if translatable_attrs.contains(&attr.name.local.as_ref()) {
let attr_text = attr.value.to_string();
if crate::should_translate(&attr_text, target_lang) {
let translated =
self.translate_text(&attr_text, target_lang).await?;
attr.value = translated.into();
}
}
}
let skip_tags = ["script", "style", "code", "pre"];
if !skip_tags.contains(&name.local.as_ref()) {
for child in node.children.borrow().iter() {
self.translate_node_recursive(child, target_lang).await?;
}
}
}
_ => {
for child in node.children.borrow().iter() {
self.translate_node_recursive(child, target_lang).await?;
}
}
}
Ok(())
})
}
}