deepl_api/
lib.rs

1//! Provides a lightweight wrapper for the DeepL Pro REST API.
2//!
3//! *If you are looking for the `deepl` commandline utility, please refer
4//! to [its documentation](../deepl/index.html) instead.*
5//!
6//! # Requirements
7//!
8//! You need to have a valid [DeepL Pro Developer](https://www.deepl.com/pro#developer) account
9//! with an associated API key. This key must be made available to the application, e. g. via
10//! environment variable:
11//!
12//! ```bash
13//! export DEEPL_API_KEY=YOUR_KEY
14//! ```
15//!
16//! # Example
17//!
18//! ```rust
19//! use deepl_api::*;
20//!
21//! // Create a DeepL instance for our account.
22//! let deepl = DeepL::new(std::env::var("DEEPL_API_KEY").unwrap());
23//!
24//! // Translate Text
25//! let texts = TranslatableTextList {
26//!     source_language: Some("DE".to_string()),
27//!     target_language: "EN-US".to_string(),
28//!     texts: vec!("ja".to_string()),
29//! };
30//! let translated = deepl.translate(None, texts).unwrap();
31//! assert_eq!(translated[0].text, "yes");
32//!
33//! // Fetch Usage Information
34//! let usage_information = deepl.usage_information().unwrap();
35//! assert!(usage_information.character_limit > 0);
36//! ```
37//!
38//! # See Also
39//!
40//! The main API functions are documented in the [DeepL] struct.
41
42use chrono::{DateTime, Utc};
43use error_chain::*;
44use reqwest::{self, Method, blocking::Response};
45use serde::Deserialize;
46
47/// Information about API usage & limits for this account.
48#[derive(Debug, Deserialize)]
49pub struct UsageInformation {
50    /// How many characters can be translated per billing period, based on the account settings.
51    pub character_limit: u64,
52    /// How many characters were already translated in the current billing period.
53    pub character_count: u64,
54}
55
56/// Information about available languages.
57pub type LanguageList = Vec<LanguageInformation>;
58
59/// Information about a single language.
60#[derive(Debug, Deserialize)]
61pub struct LanguageInformation {
62    /// Custom language identifier used by DeepL, e. g. "EN-US". Use this
63    /// when specifying source or target language.
64    pub language: String,
65    /// English name of the language, e. g. `English (America)`.
66    pub name: String,
67}
68
69/// Translation option that controls the splitting of sentences before the translation.
70#[derive(Clone)]
71pub enum SplitSentences {
72    /// Don't split sentences.
73    None,
74    /// Split on punctuation only.
75    Punctuation,
76    /// Split on punctuation and newlines.
77    PunctuationAndNewlines,
78}
79
80/// Translation option that controls the desired translation formality.
81#[derive(Clone)]
82pub enum Formality {
83    /// Default formality.
84    Default,
85    /// Translate less formally.
86    More,
87    /// Translate more formally.
88    Less,
89}
90
91/// Custom [flags for the translation request](https://www.deepl.com/docs-api/translating-text/request/).
92#[derive(Clone)]
93pub struct TranslationOptions {
94    /// Sets whether the translation engine should first split the input into sentences. This is enabled by default.
95    pub split_sentences: Option<SplitSentences>,
96    /// Sets whether the translation engine should respect the original formatting, even if it would usually correct some aspects.
97    pub preserve_formatting: Option<bool>,
98    /// Sets whether the translated text should lean towards formal or informal language.
99    pub formality: Option<Formality>,
100    /// Specify the glossary to use for the translation.
101    pub glossary_id: Option<String>,
102}
103
104/// Format of glossary entries when creating a glossary.
105pub enum GlossaryEntriesFormat {
106    /// tab-separated values
107    Tsv,
108    /// comma-separated values
109    Csv,
110}
111
112/// Representation of a glossary.
113#[derive(Debug, Deserialize)]
114pub struct Glossary {
115    /// A unique ID assigned to a glossary.
116    pub glossary_id: String,
117    /// Name associated with the glossary.
118    pub name: String,
119    /// Indicates if the newly created glossary can already be used in translate requests. If the created glossary is not yet ready, you have to wait and check the ready status of the glossary before using it in a translate request.
120    pub ready: bool,
121    /// The language in which the source texts in the glossary are specified.
122    pub source_lang: String,
123    /// The language in which the target texts in the glossary are specified.
124    pub target_lang: String,
125    /// The creation time of the glossary.
126    pub creation_time: DateTime<Utc>,
127    /// The number of entries in the glossary.
128    pub entry_count: u64,
129}
130// Representation of a glossary listing response.
131#[derive(Debug, Deserialize)]
132pub struct GlossaryListing {
133    /// A list of glossaries.
134    pub glossaries: Vec<Glossary>,
135}
136
137/// Holds a list of strings to be translated.
138#[derive(Debug, Deserialize)]
139pub struct TranslatableTextList {
140    /// Source language, if known. Will be auto-detected by the DeepL API
141    /// if not provided.
142    pub source_language: Option<String>,
143    /// Target language (required).
144    pub target_language: String,
145    /// List of texts that are supposed to be translated.
146    pub texts: Vec<String>,
147}
148
149/// Holds one unit of translated text.
150#[derive(Debug, Deserialize, PartialEq)]
151pub struct TranslatedText {
152    /// Source language. Holds the value provided, or otherwise the value that DeepL auto-detected.
153    pub detected_source_language: String,
154    /// Translated text.
155    pub text: String,
156}
157
158// Only needed for JSON deserialization.
159#[derive(Debug, Deserialize)]
160struct TranslatedTextList {
161    translations: Vec<TranslatedText>,
162}
163
164// Only needed for JSON deserialization.
165#[derive(Debug, Deserialize)]
166struct ServerErrorMessage {
167    message: String,
168    detail: Option<String>
169}
170
171/// The main API entry point representing a DeepL developer account with an associated API key.
172///
173/// # Example
174///
175/// See [Example](crate#example).
176///
177/// # Error Handling
178///
179/// None of the functions will panic. Instead, the API methods usually return a [Result<T>] which may
180/// contain an [Error] of one of the defined [ErrorKinds](ErrorKind) with more information about what went wrong.
181///
182/// If you get an [AuthorizationError](ErrorKind::AuthorizationError), then something was wrong with your API key, for example.
183pub struct DeepL {
184    api_key: String,
185}
186
187/// Implements the actual REST API. See also the [online documentation](https://www.deepl.com/docs-api/).
188impl DeepL {
189    /// Use this to create a new DeepL API client instance where multiple function calls can be performed.
190    /// A valid `api_key` is required.
191    ///
192    /// Should you ever need to use more than one DeepL account in our program, then you can create one
193    /// instance for each account / API key.
194    pub fn new(api_key: String) -> DeepL {
195        DeepL { api_key }
196    }
197
198    /// Private method that performs the HTTP calls.
199    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            // DeepL sends back error messages in the response body.
239            //   Try to fetch them to construct more helpful exceptions.
240            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    /// Retrieve information about API usage & limits.
255    /// This can also be used to verify an API key without consuming translation contingent.
256    ///
257    /// See also the [vendor documentation](https://www.deepl.com/docs-api/other-functions/monitoring-usage/).
258    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    /// Retrieve all currently available source languages.
270    ///
271    /// See also the [vendor documentation](https://www.deepl.com/docs-api/other-functions/listing-supported-languages/).
272    pub fn source_languages(&self) -> Result<LanguageList> {
273        return self.languages("source");
274    }
275
276    /// Retrieve all currently available target languages.
277    ///
278    /// See also the [vendor documentation](https://www.deepl.com/docs-api/other-functions/listing-supported-languages/).
279    pub fn target_languages(&self) -> Result<LanguageList> {
280        return self.languages("target");
281    }
282
283    /// Private method to make the API calls for the language lists.
284    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    /// Translate one or more [text chunks](TranslatableTextList) at once. You can pass in optional
294    /// [translation flags](TranslationOptions) if you need non-default behaviour.
295    ///
296    /// Please see the parameter documentation and the
297    /// [vendor documentation](https://www.deepl.com/docs-api/translating-text/) for details.
298    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    /// Create a glossary.
356    ///
357    /// Please take a look at the [vendor documentation](https://www.deepl.com/de/docs-api/glossaries/create-glossary/) for details.
358    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    /// List all glossaries.
384    ///
385    /// Please take a look at the [vendor documentation](https://www.deepl.com/de/docs-api/glossaries/list-glossaries/) for details.
386    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    /// Delete a glossary.
396    ///
397    /// Please take a look at the [vendor documentation](https://www.deepl.com/de/docs-api/glossaries/delete-glossary/) for details.
398    pub fn delete_glossary(&self, glossary_id: String) -> Result<Response> {
399        self.http_request(Method::DELETE, &format!("/glossaries/{}", glossary_id), None)
400    }
401
402    /// Retrieve Glossary Details.
403    ///
404    /// Please take a look at the [vendor documentation](https://www.deepl.com/de/docs-api/glossaries/get-glossary/) for details.
405    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        /// Indicates that the provided API key was refused by the DeepL server.
429        AuthorizationError {
430            description("Authorization failed, is your API key correct?")
431            display("Authorization failed, is your API key correct?")
432        }
433        /// An error occurred on the server side when processing a request. If possible, details
434        /// will be provided in the error message.
435        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        /// An error occurred on the client side when deserializing the response data.
440        DeserializationError {
441            description("An error occurred while deserializing the response data.")
442            display("An error occurred while deserializing the response data.")
443        }
444        /// Resource was not found
445        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}