pub mod anthropic;
pub mod deepl;
pub mod gemini;
pub mod google;
pub mod ollama;
pub mod openai;
pub mod opencode;
pub mod openrouter;
use async_trait::async_trait;
use crate::error::{Result, SubtitleToolkitError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TranslationLimits {
pub max_items: usize,
pub max_request_bytes: usize,
pub reject_unchanged_output: bool,
}
impl Default for TranslationLimits {
fn default() -> Self {
Self {
max_items: 200,
max_request_bytes: 48 * 1024,
reject_unchanged_output: false,
}
}
}
#[async_trait]
pub trait Translator: Send + Sync {
async fn translate(&self, request: TranslationRequest<'_>) -> Result<String>;
fn identifier(&self) -> String {
std::any::type_name::<Self>().to_string()
}
fn limits(&self) -> TranslationLimits {
TranslationLimits::default()
}
}
#[derive(Debug, Clone)]
pub struct TranslationRequest<'a> {
pub source_text: &'a str,
pub target_language: &'a str,
}
pub(crate) async fn provider_http_error(
provider: &'static str,
response: reqwest::Response,
) -> SubtitleToolkitError {
const MAX_ERROR_BODY_CHARS: usize = 1_000;
let status = response.status();
let retry_after_seconds = response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.trim().parse::<u64>().ok());
let retryable = matches!(status.as_u16(), 408 | 425 | 429) || status.is_server_error();
let body = response
.text()
.await
.unwrap_or_else(|_| "request failed and the response body could not be read".into());
let mut message: String = body.chars().take(MAX_ERROR_BODY_CHARS).collect();
if body.chars().count() > MAX_ERROR_BODY_CHARS {
message.push_str("...");
}
SubtitleToolkitError::Provider {
provider,
status: Some(status.as_u16()),
message,
retryable,
retry_after_seconds,
}
}
pub(crate) fn provider_transport_error(
provider: &'static str,
error: reqwest::Error,
) -> SubtitleToolkitError {
let retryable = error.is_timeout() || error.is_connect() || error.is_request();
let message = if error.is_timeout() {
"request timed out"
} else if error.is_connect() {
"connection failed"
} else if error.is_request() {
"request transport failed"
} else {
"request failed"
};
SubtitleToolkitError::Provider {
provider,
status: None,
message: message.into(),
retryable,
retry_after_seconds: None,
}
}