1use chrono::{DateTime, Utc};
43use error_chain::*;
44use reqwest::{self, Method, blocking::Response};
45use serde::Deserialize;
46
47#[derive(Debug, Deserialize)]
49pub struct UsageInformation {
50 pub character_limit: u64,
52 pub character_count: u64,
54}
55
56pub type LanguageList = Vec<LanguageInformation>;
58
59#[derive(Debug, Deserialize)]
61pub struct LanguageInformation {
62 pub language: String,
65 pub name: String,
67}
68
69#[derive(Clone)]
71pub enum SplitSentences {
72 None,
74 Punctuation,
76 PunctuationAndNewlines,
78}
79
80#[derive(Clone)]
82pub enum Formality {
83 Default,
85 More,
87 Less,
89}
90
91#[derive(Clone)]
93pub struct TranslationOptions {
94 pub split_sentences: Option<SplitSentences>,
96 pub preserve_formatting: Option<bool>,
98 pub formality: Option<Formality>,
100 pub glossary_id: Option<String>,
102}
103
104pub enum GlossaryEntriesFormat {
106 Tsv,
108 Csv,
110}
111
112#[derive(Debug, Deserialize)]
114pub struct Glossary {
115 pub glossary_id: String,
117 pub name: String,
119 pub ready: bool,
121 pub source_lang: String,
123 pub target_lang: String,
125 pub creation_time: DateTime<Utc>,
127 pub entry_count: u64,
129}
130#[derive(Debug, Deserialize)]
132pub struct GlossaryListing {
133 pub glossaries: Vec<Glossary>,
135}
136
137#[derive(Debug, Deserialize)]
139pub struct TranslatableTextList {
140 pub source_language: Option<String>,
143 pub target_language: String,
145 pub texts: Vec<String>,
147}
148
149#[derive(Debug, Deserialize, PartialEq)]
151pub struct TranslatedText {
152 pub detected_source_language: String,
154 pub text: String,
156}
157
158#[derive(Debug, Deserialize)]
160struct TranslatedTextList {
161 translations: Vec<TranslatedText>,
162}
163
164#[derive(Debug, Deserialize)]
166struct ServerErrorMessage {
167 message: String,
168 detail: Option<String>
169}
170
171pub struct DeepL {
184 api_key: String,
185}
186
187impl DeepL {
189 pub fn new(api_key: String) -> DeepL {
195 DeepL { api_key }
196 }
197
198 fn http_request(
200 &self,
201 method: Method,
202 url: &str,
203 params: Option<&[(&str, std::string::String)]>,
204 ) -> Result<reqwest::blocking::Response> {
205
206 let url = match self.api_key.ends_with(":fx") {
207 true => format!("https://api-free.deepl.com/v2{}", url),
208 false => format!("https://api.deepl.com/v2{}", url),
209 };
210
211 let client = reqwest::blocking::Client::new();
212 let request = client.request(method.clone(), &url).header("Authorization", format!("DeepL-Auth-Key {}", self.api_key));
213
214 let response = match params {
215 Some(params) => {
216 match method {
217 Method::GET => request.query(params).send(),
218 Method::PATCH | Method::POST | Method::PUT => {
219 request.form(params).send()
220 },
221 _ => unreachable!("Only GET, PATCH, POST and PUT are supported with params."),
222 }
223 },
224 None => request.send(),
225 };
226
227 let res = match response {
228 Ok(response) if response.status().is_success() => response,
229 Ok(response) if response.status() == reqwest::StatusCode::UNAUTHORIZED => {
230 bail!(ErrorKind::AuthorizationError)
231 }
232 Ok(response) if response.status() == reqwest::StatusCode::FORBIDDEN => {
233 bail!(ErrorKind::AuthorizationError)
234 }
235 Ok(response) if response.status() == reqwest::StatusCode::NOT_FOUND => {
236 bail!(ErrorKind::NotFoundError)
237 }
238 Ok(response) => {
241 let status = response.status();
242 match response.json::<ServerErrorMessage>() {
243 Ok(server_error) => bail!(ErrorKind::ServerError(format!("{}: {}", server_error.message, server_error.detail.unwrap_or_default()))),
244 _ => bail!(ErrorKind::ServerError(status.to_string())),
245 }
246 }
247 Err(e) => {
248 bail!(e)
249 }
250 };
251 Ok(res)
252 }
253
254 pub fn usage_information(&self) -> Result<UsageInformation> {
259 let res = self.http_request(Method::POST, "/usage", None)?;
260
261 match res.json::<UsageInformation>() {
262 Ok(content) => return Ok(content),
263 _ => {
264 bail!(ErrorKind::DeserializationError);
265 }
266 };
267 }
268
269 pub fn source_languages(&self) -> Result<LanguageList> {
273 return self.languages("source");
274 }
275
276 pub fn target_languages(&self) -> Result<LanguageList> {
280 return self.languages("target");
281 }
282
283 fn languages(&self, language_type: &str) -> Result<LanguageList> {
285 let res = self.http_request(Method::POST, "/languages", Some(&[("type", language_type.to_string())]))?;
286
287 match res.json::<LanguageList>() {
288 Ok(content) => return Ok(content),
289 _ => bail!(ErrorKind::DeserializationError),
290 }
291 }
292
293 pub fn translate(
299 &self,
300 options: Option<TranslationOptions>,
301 text_list: TranslatableTextList,
302 ) -> Result<Vec<TranslatedText>> {
303 let mut query = vec![
304 ("target_lang", text_list.target_language),
305 ];
306 if let Some(source_language_content) = text_list.source_language {
307 query.push(("source_lang", source_language_content));
308 }
309 for text in text_list.texts {
310 query.push(("text", text));
311 }
312 if let Some(opt) = options {
313 if let Some(split_sentences) = opt.split_sentences {
314 query.push((
315 "split_sentences",
316 match split_sentences {
317 SplitSentences::None => "0".to_string(),
318 SplitSentences::PunctuationAndNewlines => "1".to_string(),
319 SplitSentences::Punctuation => "nonewlines".to_string(),
320 },
321 ));
322 }
323 if let Some(preserve_formatting) = opt.preserve_formatting {
324 query.push((
325 "preserve_formatting",
326 match preserve_formatting {
327 false => "0".to_string(),
328 true => "1".to_string(),
329 },
330 ));
331 }
332 if let Some(formality) = opt.formality {
333 query.push((
334 "formality",
335 match formality {
336 Formality::Default => "default".to_string(),
337 Formality::More => "more".to_string(),
338 Formality::Less => "less".to_string(),
339 },
340 ));
341 }
342 if let Some(glossary_id) = opt.glossary_id {
343 query.push(("glossary_id", glossary_id));
344 }
345 }
346
347 let res = self.http_request(Method::POST, "/translate", Some(&query))?;
348
349 match res.json::<TranslatedTextList>() {
350 Ok(content) => Ok(content.translations),
351 _ => bail!(ErrorKind::DeserializationError),
352 }
353 }
354
355 pub fn create_glossary(
359 &self,
360 name: String,
361 source_lang: String,
362 target_lang: String,
363 entries: String,
364 entries_format: GlossaryEntriesFormat
365 ) -> Result<Glossary> {
366 let res = self.http_request(Method::POST, "/glossaries", Some(&[
367 ("name", name),
368 ("source_lang", source_lang),
369 ("target_lang", target_lang),
370 ("entries", entries),
371 ("entries_format", match entries_format {
372 GlossaryEntriesFormat::Tsv => "tsv".to_string(),
373 GlossaryEntriesFormat::Csv => "csv".to_string(),
374 })])
375 )?;
376
377 match res.json::<Glossary>() {
378 Ok(content) => Ok(content),
379 _ => bail!(ErrorKind::DeserializationError),
380 }
381 }
382
383 pub fn list_glossaries(&self) -> Result<GlossaryListing> {
387 let res = self.http_request(Method::GET, "/glossaries", None)?;
388
389 match res.json::<GlossaryListing>() {
390 Ok(content) => Ok(content),
391 _ => bail!(ErrorKind::DeserializationError),
392 }
393 }
394
395 pub fn delete_glossary(&self, glossary_id: String) -> Result<Response> {
399 self.http_request(Method::DELETE, &format!("/glossaries/{}", glossary_id), None)
400 }
401
402 pub fn get_glossary(&self, glossary_id: String) -> Result<Glossary> {
406 let res = self.http_request(Method::GET, &format!("/glossaries/{}", glossary_id), None)?;
407
408 match res.json::<Glossary>() {
409 Ok(content) => Ok(content),
410 _ => bail!(ErrorKind::DeserializationError),
411 }
412 }
413}
414
415mod errors {
416 use error_chain::*;
417 error_chain! {}
418}
419
420pub use errors::*;
421
422error_chain! {
423 foreign_links {
424 IO(std::io::Error);
425 Transport(reqwest::Error);
426 }
427 errors {
428 AuthorizationError {
430 description("Authorization failed, is your API key correct?")
431 display("Authorization failed, is your API key correct?")
432 }
433 ServerError(message: String) {
436 description("An error occurred while communicating with the DeepL server.")
437 display("An error occurred while communicating with the DeepL server: '{}'.", message)
438 }
439 DeserializationError {
441 description("An error occurred while deserializing the response data.")
442 display("An error occurred while deserializing the response data.")
443 }
444 NotFoundError {
446 description("The requested resource was not found.")
447 display("The requested resource was not found.")
448 }
449 }
450
451 skip_msg_variant
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn create_deepl() -> DeepL {
459 let key = std::env::var("DEEPL_API_KEY").unwrap();
460 DeepL::new(key)
461 }
462
463 #[test]
464 fn usage_information() {
465 let usage_information = create_deepl().usage_information().unwrap();
466 assert!(usage_information.character_limit > 0);
467 }
468
469 #[test]
470 fn source_languages() {
471 let source_languages = create_deepl().source_languages().unwrap();
472 assert_eq!(source_languages.last().unwrap().name, "Chinese");
473 }
474
475 #[test]
476 fn target_languages() {
477 let target_languages = create_deepl().target_languages().unwrap();
478 assert_eq!(target_languages.last().unwrap().name, "Chinese (simplified)");
479 }
480
481 #[test]
482 fn translate() {
483 let deepl = create_deepl();
484 let tests = vec![
485 (
486 None,
487 TranslatableTextList {
488 source_language: Some("DE".to_string()),
489 target_language: "EN-US".to_string(),
490 texts: vec!["ja".to_string()],
491 },
492 vec![TranslatedText {
493 detected_source_language: "DE".to_string(),
494 text: "yes".to_string(),
495 }],
496 ),
497 (
498 Some(TranslationOptions {
499 split_sentences: None,
500 preserve_formatting: Some(true),
501 glossary_id: None,
502 formality: None,
503 }),
504 TranslatableTextList {
505 source_language: Some("DE".to_string()),
506 target_language: "EN-US".to_string(),
507 texts: vec!["ja\n nein".to_string()],
508 },
509 vec![TranslatedText {
510 detected_source_language: "DE".to_string(),
511 text: "yes\n no".to_string(),
512 }],
513 ),
514 (
515 Some(TranslationOptions {
516 split_sentences: Some(SplitSentences::None),
517 preserve_formatting: None,
518 glossary_id: None,
519 formality: None,
520 }),
521 TranslatableTextList {
522 source_language: Some("DE".to_string()),
523 target_language: "EN-US".to_string(),
524 texts: vec!["Ja. Nein.".to_string()],
525 },
526 vec![TranslatedText {
527 detected_source_language: "DE".to_string(),
528 text: "Yes. No.".to_string(),
529 }],
530 ),
531 (
532 Some(TranslationOptions {
533 split_sentences: None,
534 preserve_formatting: None,
535 glossary_id: None,
536 formality: Some(Formality::More),
537 }),
538 TranslatableTextList {
539 source_language: Some("EN".to_string()),
540 target_language: "DE".to_string(),
541 texts: vec!["Please go home.".to_string()],
542 },
543 vec![TranslatedText {
544 detected_source_language: "EN".to_string(),
545 text: "Bitte gehen Sie nach Hause.".to_string(),
546 }],
547 ),
548 (
549 Some(TranslationOptions {
550 split_sentences: None,
551 preserve_formatting: None,
552 glossary_id: None,
553 formality: Some(Formality::Less),
554 }),
555 TranslatableTextList {
556 source_language: Some("EN".to_string()),
557 target_language: "DE".to_string(),
558 texts: vec!["Please go home.".to_string()],
559 },
560 vec![TranslatedText {
561 detected_source_language: "EN".to_string(),
562 text: "Bitte geh nach Hause.".to_string(),
563 }],
564 ),
565 ];
566 for test in tests {
567 assert_eq!(deepl.translate(test.0, test.1).unwrap(), test.2);
568 }
569 }
570
571 #[test]
572 #[should_panic(expected = "Error(ServerError(\"Parameter 'text' not specified.")]
573 fn translate_empty() {
574 let texts = TranslatableTextList {
575 source_language: Some("DE".to_string()),
576 target_language: "EN-US".to_string(),
577 texts: vec![],
578 };
579 create_deepl().translate(None, texts).unwrap();
580 }
581
582 #[test]
583 #[should_panic(expected = "Error(ServerError(\"Value for 'target_lang' not supported.")]
584 fn translate_wrong_language() {
585 let texts = TranslatableTextList {
586 source_language: None,
587 target_language: "NONEXISTING".to_string(),
588 texts: vec!["ja".to_string()],
589 };
590 create_deepl().translate(None, texts).unwrap();
591 }
592
593 #[test]
594 #[should_panic(expected = "Error(AuthorizationError")]
595 fn translate_unauthorized() {
596 let key = "wrong_key".to_string();
597 let texts = TranslatableTextList {
598 source_language: Some("DE".to_string()),
599 target_language: "EN-US".to_string(),
600 texts: vec!["ja".to_string()],
601 };
602 DeepL::new(key).translate(None, texts).unwrap();
603 }
604
605 #[test]
606 fn glossaries() {
607 let deepl = create_deepl();
608 let glossary_name = "test_glossary".to_string();
609
610 let mut glossary = deepl.create_glossary(
611 glossary_name.clone(),
612 "en".to_string(),
613 "de".to_string(),
614 "Action,Handlung".to_string(),
615 GlossaryEntriesFormat::Csv
616 ).unwrap();
617
618 assert_eq!(glossary.name, glossary_name);
619 assert_eq!(glossary.entry_count, 1);
620
621 glossary = deepl.get_glossary(glossary.glossary_id).unwrap();
622 assert_eq!(glossary.name, glossary_name);
623 assert_eq!(glossary.entry_count, 1);
624
625 let mut glossaries = deepl.list_glossaries().unwrap().glossaries;
626 glossaries.retain(|glossary| glossary.name == glossary_name);
627 let glossary = glossaries.pop().unwrap();
628 assert_eq!(glossary.name, glossary_name);
629 assert_eq!(glossary.entry_count, 1);
630
631 assert_eq!(deepl.translate(
632 Some(
633 TranslationOptions {
634 split_sentences: None,
635 preserve_formatting: None,
636 glossary_id: Some(glossary.glossary_id.clone()),
637 formality: None,
638 }
639 ),
640 TranslatableTextList {
641 source_language: Some("en".to_string()),
642 target_language: "de".to_string(),
643 texts: vec!["Action".to_string()],
644 }
645 ).unwrap().pop().unwrap().text, "Handlung");
646
647 deepl.delete_glossary(glossary.glossary_id.clone()).unwrap();
648 let glossary_response = deepl.get_glossary(glossary.glossary_id);
649 assert_eq!(glossary_response.unwrap_err().to_string(), crate::ErrorKind::NotFoundError.to_string());
650 }
651}