use crate::{Error, Lang, Result};
use reqwest::{Client, RequestBuilder};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{fmt, str::FromStr};
use url::Url;
const ENDPOINT_FREE: &str = "https://api-free.deepl.com";
const ENDPOINT_PRO: &str = "https://api.deepl.com";
#[derive(Debug, Serialize, Deserialize)]
pub enum SplitSentences {
#[serde(rename = "0")]
None,
#[serde(rename = "1")]
One,
#[serde(rename = "nonewlines")]
NoNewlines,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub enum Formality {
#[default]
Default,
More,
Less,
PreferMore,
PreferLess,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Language {
pub language: Lang,
pub name: String,
pub supports_formality: Option<bool>,
}
#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum LanguageType {
#[default]
Source,
Target,
}
impl AsRef<str> for LanguageType {
fn as_ref(&self) -> &str {
match self {
Self::Source => "source",
Self::Target => "target",
}
}
}
impl fmt::Display for LanguageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_ref(),)
}
}
impl FromStr for LanguageType {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(match s {
"source" => Self::Source,
"target" => Self::Target,
_ => return Err(Error::InvalidLanguageType(s.to_string())),
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Usage {
pub character_count: u64,
pub character_limit: u64,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TagHandling {
Xml,
Html,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TextTranslation {
pub text: String,
pub detected_source_language: Lang,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TranslateTextRequest {
pub text: Vec<String>,
pub target_lang: Lang,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag_handling: Option<TagHandling>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_lang: Option<Lang>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preserve_formatting: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub glossary_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outline_detection: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub non_splitting_tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub splitting_tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub formality: Option<Formality>,
#[serde(skip_serializing_if = "Option::is_none")]
pub split_sentences: Option<SplitSentences>,
}
impl TranslateTextRequest {
pub fn new(text: Vec<String>, target_lang: Lang) -> Self {
Self {
text,
target_lang,
source_lang: None,
context: None,
preserve_formatting: None,
glossary_id: None,
outline_detection: None,
non_splitting_tags: None,
splitting_tags: None,
ignore_tags: None,
tag_handling: None,
formality: None,
split_sentences: None,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TranslateTextResponse {
pub translations: Vec<TextTranslation>,
}
pub struct ApiOptions {
api_key: String,
endpoint: Url,
client: Option<Client>,
}
impl ApiOptions {
pub fn new(api_key: impl AsRef<str>) -> Self {
if api_key.as_ref().ends_with(":fx") {
Self::new_free(api_key)
} else {
Self::new_pro(api_key)
}
}
pub fn new_with_client(api_key: impl AsRef<str>, client: Client) -> Self {
let mut options = Self::new(api_key);
options.client = Some(client);
options
}
fn new_free(api_key: impl AsRef<str>) -> Self {
Self {
api_key: api_key.as_ref().to_owned(),
endpoint: Url::parse(ENDPOINT_FREE).unwrap(),
client: None,
}
}
fn new_pro(api_key: impl AsRef<str>) -> Self {
Self {
api_key: api_key.as_ref().to_owned(),
endpoint: Url::parse(ENDPOINT_PRO).unwrap(),
client: None,
}
}
}
pub struct DeeplApi {
client: Client,
options: ApiOptions,
}
impl DeeplApi {
pub fn new(mut options: ApiOptions) -> Self {
Self {
client: options.client.take().unwrap_or_else(|| Client::new()),
options,
}
}
pub async fn usage(&self) -> Result<Usage> {
let url = self.options.endpoint.join("v2/usage")?;
let req = self.client.get(url);
self.make_typed_request::<Usage>(req).await
}
pub async fn languages(&self, lang_type: LanguageType) -> Result<Vec<Language>> {
let mut url = self.options.endpoint.join("v2/languages")?;
url.query_pairs_mut()
.append_pair("type", lang_type.as_ref());
let req = self.client.get(url);
self.make_typed_request::<Vec<Language>>(req).await
}
pub async fn translate_text(
&self,
request: &TranslateTextRequest,
) -> Result<TranslateTextResponse> {
let url = self.options.endpoint.join("v2/translate")?;
let req = self.client.post(url).json(request);
self.make_typed_request::<TranslateTextResponse>(req).await
}
async fn make_typed_request<T: DeserializeOwned>(&self, req: RequestBuilder) -> Result<T> {
let res = req
.header(
"Authorization",
format!("DeepL-Auth-Key {}", self.options.api_key),
)
.send()
.await?;
res.error_for_status_ref()?;
Ok(res.json::<T>().await?)
}
}