use chrono::{DateTime, Utc};
use error_chain::*;
use reqwest::{self, Method, blocking::Response};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct UsageInformation {
pub character_limit: u64,
pub character_count: u64,
}
pub type LanguageList = Vec<LanguageInformation>;
#[derive(Debug, Deserialize)]
pub struct LanguageInformation {
pub language: String,
pub name: String,
}
#[derive(Clone)]
pub enum SplitSentences {
None,
Punctuation,
PunctuationAndNewlines,
}
#[derive(Clone)]
pub enum Formality {
Default,
More,
Less,
}
#[derive(Clone)]
pub struct TranslationOptions {
pub split_sentences: Option<SplitSentences>,
pub preserve_formatting: Option<bool>,
pub formality: Option<Formality>,
pub glossary_id: Option<String>,
}
pub enum GlossaryEntriesFormat {
Tsv,
Csv,
}
#[derive(Debug, Deserialize)]
pub struct Glossary {
pub glossary_id: String,
pub name: String,
pub ready: bool,
pub source_lang: String,
pub target_lang: String,
pub creation_time: DateTime<Utc>,
pub entry_count: u64,
}
#[derive(Debug, Deserialize)]
pub struct GlossaryListing {
pub glossaries: Vec<Glossary>,
}
#[derive(Debug, Deserialize)]
pub struct TranslatableTextList {
pub source_language: Option<String>,
pub target_language: String,
pub texts: Vec<String>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct TranslatedText {
pub detected_source_language: String,
pub text: String,
}
#[derive(Debug, Deserialize)]
struct TranslatedTextList {
translations: Vec<TranslatedText>,
}
#[derive(Debug, Deserialize)]
struct ServerErrorMessage {
message: String,
detail: Option<String>
}
pub struct DeepL {
api_key: String,
}
impl DeepL {
pub fn new(api_key: String) -> DeepL {
DeepL { api_key }
}
fn http_request(
&self,
method: Method,
url: &str,
params: Option<&[(&str, std::string::String)]>,
) -> Result<reqwest::blocking::Response> {
let url = match self.api_key.ends_with(":fx") {
true => format!("https://api-free.deepl.com/v2{}", url),
false => format!("https://api.deepl.com/v2{}", url),
};
let client = reqwest::blocking::Client::new();
let request = client.request(method.clone(), &url).header("Authorization", format!("DeepL-Auth-Key {}", self.api_key));
let response = match params {
Some(params) => {
match method {
Method::GET => request.query(params).send(),
Method::PATCH | Method::POST | Method::PUT => {
request.form(params).send()
},
_ => unreachable!("Only GET, PATCH, POST and PUT are supported with params."),
}
},
None => request.send(),
};
let res = match response {
Ok(response) if response.status().is_success() => response,
Ok(response) if response.status() == reqwest::StatusCode::UNAUTHORIZED => {
bail!(ErrorKind::AuthorizationError)
}
Ok(response) if response.status() == reqwest::StatusCode::FORBIDDEN => {
bail!(ErrorKind::AuthorizationError)
}
Ok(response) if response.status() == reqwest::StatusCode::NOT_FOUND => {
bail!(ErrorKind::NotFoundError)
}
Ok(response) => {
let status = response.status();
match response.json::<ServerErrorMessage>() {
Ok(server_error) => bail!(ErrorKind::ServerError(format!("{}: {}", server_error.message, server_error.detail.unwrap_or_default()))),
_ => bail!(ErrorKind::ServerError(status.to_string())),
}
}
Err(e) => {
bail!(e)
}
};
Ok(res)
}
pub fn usage_information(&self) -> Result<UsageInformation> {
let res = self.http_request(Method::POST, "/usage", None)?;
match res.json::<UsageInformation>() {
Ok(content) => return Ok(content),
_ => {
bail!(ErrorKind::DeserializationError);
}
};
}
pub fn source_languages(&self) -> Result<LanguageList> {
return self.languages("source");
}
pub fn target_languages(&self) -> Result<LanguageList> {
return self.languages("target");
}
fn languages(&self, language_type: &str) -> Result<LanguageList> {
let res = self.http_request(Method::POST, "/languages", Some(&[("type", language_type.to_string())]))?;
match res.json::<LanguageList>() {
Ok(content) => return Ok(content),
_ => bail!(ErrorKind::DeserializationError),
}
}
pub fn translate(
&self,
options: Option<TranslationOptions>,
text_list: TranslatableTextList,
) -> Result<Vec<TranslatedText>> {
let mut query = vec![
("target_lang", text_list.target_language),
];
if let Some(source_language_content) = text_list.source_language {
query.push(("source_lang", source_language_content));
}
for text in text_list.texts {
query.push(("text", text));
}
if let Some(opt) = options {
if let Some(split_sentences) = opt.split_sentences {
query.push((
"split_sentences",
match split_sentences {
SplitSentences::None => "0".to_string(),
SplitSentences::PunctuationAndNewlines => "1".to_string(),
SplitSentences::Punctuation => "nonewlines".to_string(),
},
));
}
if let Some(preserve_formatting) = opt.preserve_formatting {
query.push((
"preserve_formatting",
match preserve_formatting {
false => "0".to_string(),
true => "1".to_string(),
},
));
}
if let Some(formality) = opt.formality {
query.push((
"formality",
match formality {
Formality::Default => "default".to_string(),
Formality::More => "more".to_string(),
Formality::Less => "less".to_string(),
},
));
}
if let Some(glossary_id) = opt.glossary_id {
query.push(("glossary_id", glossary_id));
}
}
let res = self.http_request(Method::POST, "/translate", Some(&query))?;
match res.json::<TranslatedTextList>() {
Ok(content) => Ok(content.translations),
_ => bail!(ErrorKind::DeserializationError),
}
}
pub fn create_glossary(
&self,
name: String,
source_lang: String,
target_lang: String,
entries: String,
entries_format: GlossaryEntriesFormat
) -> Result<Glossary> {
let res = self.http_request(Method::POST, "/glossaries", Some(&[
("name", name),
("source_lang", source_lang),
("target_lang", target_lang),
("entries", entries),
("entries_format", match entries_format {
GlossaryEntriesFormat::Tsv => "tsv".to_string(),
GlossaryEntriesFormat::Csv => "csv".to_string(),
})])
)?;
match res.json::<Glossary>() {
Ok(content) => Ok(content),
_ => bail!(ErrorKind::DeserializationError),
}
}
pub fn list_glossaries(&self) -> Result<GlossaryListing> {
let res = self.http_request(Method::GET, "/glossaries", None)?;
match res.json::<GlossaryListing>() {
Ok(content) => Ok(content),
_ => bail!(ErrorKind::DeserializationError),
}
}
pub fn delete_glossary(&self, glossary_id: String) -> Result<Response> {
self.http_request(Method::DELETE, &format!("/glossaries/{}", glossary_id), None)
}
pub fn get_glossary(&self, glossary_id: String) -> Result<Glossary> {
let res = self.http_request(Method::GET, &format!("/glossaries/{}", glossary_id), None)?;
match res.json::<Glossary>() {
Ok(content) => Ok(content),
_ => bail!(ErrorKind::DeserializationError),
}
}
}
mod errors {
use error_chain::*;
error_chain! {}
}
pub use errors::*;
error_chain! {
foreign_links {
IO(std::io::Error);
Transport(reqwest::Error);
}
errors {
AuthorizationError {
description("Authorization failed, is your API key correct?")
display("Authorization failed, is your API key correct?")
}
ServerError(message: String) {
description("An error occurred while communicating with the DeepL server.")
display("An error occurred while communicating with the DeepL server: '{}'.", message)
}
DeserializationError {
description("An error occurred while deserializing the response data.")
display("An error occurred while deserializing the response data.")
}
NotFoundError {
description("The requested resource was not found.")
display("The requested resource was not found.")
}
}
skip_msg_variant
}
#[cfg(test)]
mod tests {
use super::*;
fn create_deepl() -> DeepL {
let key = std::env::var("DEEPL_API_KEY").unwrap();
DeepL::new(key)
}
#[test]
fn usage_information() {
let usage_information = create_deepl().usage_information().unwrap();
assert!(usage_information.character_limit > 0);
}
#[test]
fn source_languages() {
let source_languages = create_deepl().source_languages().unwrap();
assert_eq!(source_languages.last().unwrap().name, "Chinese");
}
#[test]
fn target_languages() {
let target_languages = create_deepl().target_languages().unwrap();
assert_eq!(target_languages.last().unwrap().name, "Chinese (simplified)");
}
#[test]
fn translate() {
let deepl = create_deepl();
let tests = vec![
(
None,
TranslatableTextList {
source_language: Some("DE".to_string()),
target_language: "EN-US".to_string(),
texts: vec!["ja".to_string()],
},
vec![TranslatedText {
detected_source_language: "DE".to_string(),
text: "yes".to_string(),
}],
),
(
Some(TranslationOptions {
split_sentences: None,
preserve_formatting: Some(true),
glossary_id: None,
formality: None,
}),
TranslatableTextList {
source_language: Some("DE".to_string()),
target_language: "EN-US".to_string(),
texts: vec!["ja\n nein".to_string()],
},
vec![TranslatedText {
detected_source_language: "DE".to_string(),
text: "yes\n no".to_string(),
}],
),
(
Some(TranslationOptions {
split_sentences: Some(SplitSentences::None),
preserve_formatting: None,
glossary_id: None,
formality: None,
}),
TranslatableTextList {
source_language: Some("DE".to_string()),
target_language: "EN-US".to_string(),
texts: vec!["Ja. Nein.".to_string()],
},
vec![TranslatedText {
detected_source_language: "DE".to_string(),
text: "Yes. No.".to_string(),
}],
),
(
Some(TranslationOptions {
split_sentences: None,
preserve_formatting: None,
glossary_id: None,
formality: Some(Formality::More),
}),
TranslatableTextList {
source_language: Some("EN".to_string()),
target_language: "DE".to_string(),
texts: vec!["Please go home.".to_string()],
},
vec![TranslatedText {
detected_source_language: "EN".to_string(),
text: "Bitte gehen Sie nach Hause.".to_string(),
}],
),
(
Some(TranslationOptions {
split_sentences: None,
preserve_formatting: None,
glossary_id: None,
formality: Some(Formality::Less),
}),
TranslatableTextList {
source_language: Some("EN".to_string()),
target_language: "DE".to_string(),
texts: vec!["Please go home.".to_string()],
},
vec![TranslatedText {
detected_source_language: "EN".to_string(),
text: "Bitte geh nach Hause.".to_string(),
}],
),
];
for test in tests {
assert_eq!(deepl.translate(test.0, test.1).unwrap(), test.2);
}
}
#[test]
#[should_panic(expected = "Error(ServerError(\"Parameter 'text' not specified.")]
fn translate_empty() {
let texts = TranslatableTextList {
source_language: Some("DE".to_string()),
target_language: "EN-US".to_string(),
texts: vec![],
};
create_deepl().translate(None, texts).unwrap();
}
#[test]
#[should_panic(expected = "Error(ServerError(\"Value for 'target_lang' not supported.")]
fn translate_wrong_language() {
let texts = TranslatableTextList {
source_language: None,
target_language: "NONEXISTING".to_string(),
texts: vec!["ja".to_string()],
};
create_deepl().translate(None, texts).unwrap();
}
#[test]
#[should_panic(expected = "Error(AuthorizationError")]
fn translate_unauthorized() {
let key = "wrong_key".to_string();
let texts = TranslatableTextList {
source_language: Some("DE".to_string()),
target_language: "EN-US".to_string(),
texts: vec!["ja".to_string()],
};
DeepL::new(key).translate(None, texts).unwrap();
}
#[test]
fn glossaries() {
let deepl = create_deepl();
let glossary_name = "test_glossary".to_string();
let mut glossary = deepl.create_glossary(
glossary_name.clone(),
"en".to_string(),
"de".to_string(),
"Action,Handlung".to_string(),
GlossaryEntriesFormat::Csv
).unwrap();
assert_eq!(glossary.name, glossary_name);
assert_eq!(glossary.entry_count, 1);
glossary = deepl.get_glossary(glossary.glossary_id).unwrap();
assert_eq!(glossary.name, glossary_name);
assert_eq!(glossary.entry_count, 1);
let mut glossaries = deepl.list_glossaries().unwrap().glossaries;
glossaries.retain(|glossary| glossary.name == glossary_name);
let glossary = glossaries.pop().unwrap();
assert_eq!(glossary.name, glossary_name);
assert_eq!(glossary.entry_count, 1);
assert_eq!(deepl.translate(
Some(
TranslationOptions {
split_sentences: None,
preserve_formatting: None,
glossary_id: Some(glossary.glossary_id.clone()),
formality: None,
}
),
TranslatableTextList {
source_language: Some("en".to_string()),
target_language: "de".to_string(),
texts: vec!["Action".to_string()],
}
).unwrap().pop().unwrap().text, "Handlung");
deepl.delete_glossary(glossary.glossary_id.clone()).unwrap();
let glossary_response = deepl.get_glossary(glossary.glossary_id);
assert_eq!(glossary_response.unwrap_err().to_string(), crate::ErrorKind::NotFoundError.to_string());
}
}