use crate::{error::TranslationError, options::TranslateOptions, translator::Translator};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, Semaphore};
use tokio::time::sleep;
use unic_langid::LanguageIdentifier;
#[derive(Debug, Clone)]
pub struct MicrosoftConfig {
pub endpoint: Option<String>,
pub api_key: Option<String>,
pub concurrent_limit: usize,
}
impl Default for MicrosoftConfig {
fn default() -> Self {
Self {
endpoint: None, api_key: None, concurrent_limit: 10,
}
}
}
impl MicrosoftConfig {
pub fn builder() -> MicrosoftConfigBuilder {
MicrosoftConfigBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct MicrosoftConfigBuilder {
endpoint: Option<String>,
api_key: Option<String>,
concurrent_limit: Option<usize>,
}
impl MicrosoftConfigBuilder {
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
pub fn api_key(mut self, api_key: Option<impl Into<String>>) -> Self {
self.api_key = api_key.map(|s| s.into());
self
}
pub fn concurrent_limit(mut self, concurrent_limit: usize) -> Self {
self.concurrent_limit = Some(concurrent_limit);
self
}
pub fn build(self) -> MicrosoftConfig {
MicrosoftConfig {
endpoint: self.endpoint,
api_key: self.api_key,
concurrent_limit: self.concurrent_limit.unwrap_or(10),
}
}
}
#[derive(Debug, Deserialize)]
struct MicrosoftErrorResponse {
error: MicrosoftErrorDetails,
}
#[derive(Debug, Deserialize)]
struct MicrosoftErrorDetails {
code: u32,
message: String,
}
#[derive(Debug, Deserialize)]
pub struct DetectedLanguage {
pub language: String,
pub score: f64,
}
#[derive(Debug, Deserialize)]
pub struct MicrosoftTranslation {
#[serde(rename = "detectedLanguage")]
pub detected_language: Option<DetectedLanguage>,
pub translations: Vec<TranslationResult>,
}
#[derive(Debug, Deserialize)]
pub struct TranslationResult {
pub text: String,
pub to: String,
}
#[derive(Serialize)]
struct BatchTranslationRequest {
text: String,
}
pub struct MicrosoftTranslator {
client: Client,
config: MicrosoftConfig,
semaphore: Arc<Semaphore>,
cached_token: Arc<Mutex<Option<String>>>,
token_expiry: Arc<Mutex<Option<Instant>>>,
}
impl MicrosoftTranslator {
pub fn new(config: MicrosoftConfig) -> Self {
let concurrent_limit = config.concurrent_limit;
Self {
client: Client::new(),
config,
semaphore: Arc::new(Semaphore::new(concurrent_limit)),
cached_token: Arc::new(Mutex::new(None)),
token_expiry: Arc::new(Mutex::new(None)),
}
}
async fn get_auth_token(&self) -> Result<String, TranslationError> {
if let Some(api_key) = &self.config.api_key {
return Ok(api_key.clone());
}
let mut token_guard = self.cached_token.lock().await;
let mut expiry_guard = self.token_expiry.lock().await;
if let (Some(token), Some(expiry)) = (token_guard.as_ref(), expiry_guard.as_ref()) {
if expiry.saturating_duration_since(Instant::now()) > Duration::from_secs(60) {
return Ok(token.clone());
}
}
let mut auth_attempts = 3;
while auth_attempts > 0 {
auth_attempts -= 1;
match self.client
.get("https://edge.microsoft.com/translate/auth")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
let token = response.text().await.map_err(|e| {
TranslationError::AuthenticationError(format!("Failed to read auth response: {}", e))
})?;
*token_guard = Some(token.clone());
*expiry_guard = Some(Instant::now() + Duration::from_secs(540)); return Ok(token);
} else {
if auth_attempts <= 0 {
return Err(TranslationError::AuthenticationError(
format!("Failed to authenticate with Microsoft Translator: HTTP {}", response.status())
));
}
}
}
Err(e) => {
if auth_attempts <= 0 {
return Err(TranslationError::NetworkError(e));
}
}
}
sleep(Duration::from_secs(1)).await;
}
Err(TranslationError::AuthenticationError(
"Failed to get Microsoft Translator authorization after retries".to_string(),
))
}
async fn clear_cached_token(&self) {
*self.cached_token.lock().await = None;
*self.token_expiry.lock().await = None;
}
pub async fn translate_batch(
&self,
texts: &[&str],
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<Vec<MicrosoftTranslation>, TranslationError> {
let mut errors = Vec::new();
for attempt in 0..=options.max_retries {
if attempt > 0 {
let delay = Duration::from_millis(100 * 2u64.pow(attempt - 1));
sleep(delay).await;
}
match self
.try_translate_batch(texts, target_lang, source_lang, options)
.await
{
Ok(result) => return Ok(result),
Err(e) => {
if e.is_retryable() {
errors.push(e);
} else {
return Err(e);
}
}
}
}
Err(TranslationError::MaxRetriesExceeded {
attempts: options.max_retries + 1,
errors,
})
}
async fn try_translate_batch(
&self,
texts: &[&str],
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<Vec<MicrosoftTranslation>, TranslationError> {
let _permit =
self.semaphore.acquire().await.map_err(|e| {
TranslationError::Other(format!("Failed to acquire semaphore: {}", e))
})?;
let token = self.get_auth_token().await?;
let endpoint = self
.config
.endpoint
.as_deref()
.unwrap_or("https://api-edge.cognitive.microsofttranslator.com");
let client = if let Some(timeout) = options.timeout {
Client::builder()
.timeout(timeout)
.build()
.map_err(|e| TranslationError::NetworkError(e))?
} else {
self.client.clone()
};
let requests: Vec<BatchTranslationRequest> = texts
.iter()
.map(|text| BatchTranslationRequest {
text: text.to_string(),
})
.collect();
let target_lang_str = target_lang.to_string();
let source_lang_str = source_lang.map(|s| s.to_string());
let mut params = vec![
("api-version", "3.0"),
("to", target_lang_str.as_str()),
("includeSentenceLength", "true"),
];
if let Some(ref source_str) = source_lang_str {
params.push(("from", source_str.as_str()));
}
let auth_header = if self.config.api_key.is_some() {
format!("Ocp-Apim-Subscription-Key {}", token)
} else {
format!("Bearer {}", token)
};
let response = client
.post(&format!("{}/translate", endpoint))
.header("Authorization", auth_header)
.header("Content-Type", "application/json")
.query(¶ms)
.json(&requests)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
if status == reqwest::StatusCode::UNAUTHORIZED {
self.clear_cached_token().await;
}
if let Ok(error_response) = serde_json::from_str::<MicrosoftErrorResponse>(&error_text)
{
return Err(TranslationError::HttpError {
status,
body: format!(
"Error {}: {}",
error_response.error.code, error_response.error.message
),
});
}
return Err(TranslationError::HttpError {
status,
body: error_text,
});
}
let response_body: Vec<MicrosoftTranslation> = response.json().await?;
Ok(response_body)
}
pub async fn translate_text(
&self,
text: &str,
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<String, TranslationError> {
let results = self
.translate_batch(&[text], target_lang, source_lang, options)
.await?;
if results.is_empty() || results[0].translations.is_empty() {
return Err(TranslationError::ServiceError(
"No translation results returned".to_string(),
));
}
Ok(results[0].translations[0].text.clone())
}
pub async fn translate_batch_to_strings(
&self,
texts: &[&str],
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<Vec<String>, TranslationError> {
let results = self
.translate_batch(texts, target_lang, source_lang, options)
.await?;
let translated_texts = results
.into_iter()
.filter_map(|res| res.translations.into_iter().next())
.map(|trans_result| trans_result.text)
.collect();
Ok(translated_texts)
}
}
#[async_trait::async_trait]
impl Translator for MicrosoftTranslator {
async fn translate_with_options(
&self,
text: &str,
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<String, TranslationError> {
self.translate_text(text, target_lang, source_lang, options)
.await
}
}
#[cfg(test)]
mod tests;