deepl_rustls/endpoint/
translate.rs

1use std::{collections::HashMap, future::IntoFuture};
2
3use crate::{
4    endpoint::{Formality, Pollable, Result},
5    impl_requester, Lang,
6};
7
8use serde::Deserialize;
9
10/// Response from basic translation API
11#[derive(Deserialize)]
12pub struct TranslateTextResp {
13    pub translations: Vec<Sentence>,
14}
15
16impl std::fmt::Display for TranslateTextResp {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(
19            f,
20            "{}",
21            self.translations
22                .iter()
23                .map(|sent| sent.text.to_string())
24                .collect::<String>()
25        )
26    }
27}
28
29/// Translated result for a sentence
30#[derive(Deserialize)]
31pub struct Sentence {
32    pub detected_source_language: Lang,
33    pub text: String,
34}
35
36///
37/// Sets whether the translation engine should respect the original formatting,
38/// even if it would usually correct some aspects.
39/// The formatting aspects affected by this setting include:
40/// - Punctuation at the beginning and end of the sentence
41/// - Upper/lower case at the beginning of the sentence
42///
43pub enum PreserveFormatting {
44    Preserve,
45    DontPreserve,
46}
47
48impl AsRef<str> for PreserveFormatting {
49    fn as_ref(&self) -> &str {
50        match self {
51            PreserveFormatting::Preserve => "1",
52            PreserveFormatting::DontPreserve => "0",
53        }
54    }
55}
56
57///
58/// Sets whether the translation engine should first split the input into sentences
59///
60/// For applications that send one sentence per text parameter, it is advisable to set this to `None`,
61/// in order to prevent the engine from splitting the sentence unintentionally.
62/// Please note that newlines will split sentences. You should therefore clean files to avoid breaking sentences or set this to `PunctuationOnly`.
63///
64pub enum SplitSentences {
65    /// Perform no splitting at all, whole input is treated as one sentence
66    None,
67    /// Split on punctuation and on newlines (default)
68    PunctuationAndNewlines,
69    /// Split on punctuation only, ignoring newlines
70    PunctuationOnly,
71}
72
73impl AsRef<str> for SplitSentences {
74    fn as_ref(&self) -> &str {
75        match self {
76            SplitSentences::None => "0",
77            SplitSentences::PunctuationAndNewlines => "1",
78            SplitSentences::PunctuationOnly => "nonewlines",
79        }
80    }
81}
82
83///
84/// Sets which kind of tags should be handled. Options currently available
85///
86pub enum TagHandling {
87    /// Enable XML tag handling
88    /// see: <https://www.deepl.com/docs-api/xml>
89    Xml,
90    /// Enable HTML tag handling
91    /// see: <https://www.deepl.com/docs-api/html>
92    Html,
93}
94
95impl AsRef<str> for TagHandling {
96    fn as_ref(&self) -> &str {
97        match self {
98            TagHandling::Xml => "xml",
99            TagHandling::Html => "html",
100        }
101    }
102}
103
104impl_requester! {
105    TranslateRequester {
106        @required{
107            text: String,
108            target_lang: Lang,
109        };
110        @optional{
111            context: String,
112            source_lang: Lang,
113            split_sentences: SplitSentences,
114            preserve_formatting: PreserveFormatting,
115            formality: Formality,
116            glossary_id: String,
117            tag_handling: TagHandling,
118            non_splitting_tags: Vec<String>,
119            splitting_tags: Vec<String>,
120            ignore_tags: Vec<String>,
121        };
122    } -> Result<TranslateTextResp, Error>;
123}
124
125impl<'a> IntoFuture for TranslateRequester<'a> {
126    type Output = Result<TranslateTextResp>;
127    type IntoFuture = Pollable<'a, Self::Output>;
128
129    fn into_future(mut self) -> Self::IntoFuture {
130        self.send()
131    }
132}
133
134impl<'a> IntoFuture for &mut TranslateRequester<'a> {
135    type Output = Result<TranslateTextResp>;
136    type IntoFuture = Pollable<'a, Self::Output>;
137
138    fn into_future(self) -> Self::IntoFuture {
139        self.send()
140    }
141}
142
143impl<'a> TranslateRequester<'a> {
144    fn to_form(&self) -> HashMap<&'static str, String> {
145        let mut param = HashMap::new();
146        param.insert("text", self.text.to_string());
147
148        if let Some(la) = &self.source_lang {
149            param.insert("source_lang", la.as_ref().to_string());
150        }
151
152        param.insert("target_lang", self.target_lang.as_ref().to_string());
153
154        if let Some(ss) = &self.split_sentences {
155            param.insert("split_sentences", ss.as_ref().to_string());
156        }
157
158        if let Some(pf) = &self.preserve_formatting {
159            param.insert("preserve_formatting", pf.as_ref().to_string());
160        }
161
162        if let Some(fm) = &self.formality {
163            param.insert("formality", fm.as_ref().to_string());
164        }
165
166        if let Some(id) = &self.glossary_id {
167            param.insert("glossary_id", id.to_string());
168        }
169
170        if let Some(th) = &self.tag_handling {
171            param.insert("tag_handling", th.as_ref().to_string());
172        }
173
174        if let Some(tags) = &self.non_splitting_tags {
175            if !tags.is_empty() {
176                param.insert("non_splitting_tags", tags.join(","));
177            }
178        }
179
180        if let Some(tags) = &self.splitting_tags {
181            if !tags.is_empty() {
182                param.insert("splitting_tags", tags.join(","));
183            }
184        }
185
186        if let Some(tags) = &self.ignore_tags {
187            if !tags.is_empty() {
188                param.insert("ignore_tags", tags.join(","));
189            }
190        }
191
192        param
193    }
194
195    fn send(&mut self) -> Pollable<'a, Result<TranslateTextResp>> {
196        let client = self.client.clone();
197        let form = self.to_form();
198
199        let fut = async move {
200            let response = client
201                .post(client.inner.endpoint.join("translate").unwrap())
202                .form(&form)
203                .send()
204                .await
205                .map_err(|err| Error::RequestFail(err.to_string()))?;
206
207            if !response.status().is_success() {
208                return super::extract_deepl_error(response).await;
209            }
210
211            let response: TranslateTextResp = response.json().await.map_err(|err| {
212                Error::InvalidResponse(format!("convert json bytes to Rust type: {err}"))
213            })?;
214
215            Ok(response)
216        };
217
218        Box::pin(fut)
219    }
220}
221
222impl DeepLApi {
223    /// Translate the given text with specific target language.
224    ///
225    /// # Error
226    ///
227    /// Return [`Error`] if the http request fail
228    ///
229    /// # Example
230    ///
231    /// * Simple translation
232    ///
233    /// ```rust
234    /// use deepl::{DeepLApi, Lang};
235    ///
236    /// let key = std::env::var("DEEPL_API_KEY").unwrap();
237    /// let deepl = DeepLApi::with(&key).new();
238    ///
239    /// let response = deepl.translate_text("Hello World", Lang::ZH).await.unwrap();
240    /// assert!(!response.translations.is_empty());
241    /// ```
242    ///
243    /// * Translation with XML tag enabled
244    ///
245    /// ```rust
246    /// use deepl::{DeepLApi, Lang};
247    ///
248    /// let key = std::env::var("DEEPL_API_KEY").unwrap();
249    /// let deepl = DeepLApi::with(&key).new();
250    ///
251    /// let str = "Hello World <keep>This will stay exactly the way it was</keep>";
252    /// let response = deepl
253    ///     .translate_text(str, Lang::DE)
254    ///     .source_lang(Lang::EN)
255    ///     .ignore_tags(vec!["keep".to_owned()])
256    ///     .tag_handling(TagHandling::Xml)
257    ///     .await
258    ///     .unwrap();
259    ///
260    /// let translated_results = response.translations;
261    /// let should = "Hallo Welt <keep>This will stay exactly the way it was</keep>";
262    /// assert_eq!(translated_results[0].text, should);
263    /// ```
264    pub fn translate_text(&self, text: impl ToString, target_lang: Lang) -> TranslateRequester {
265        TranslateRequester::new(self, text.to_string(), target_lang)
266    }
267}
268
269#[tokio::test]
270async fn test_translate_text() {
271    let key = std::env::var("DEEPL_API_KEY").unwrap();
272    let api = DeepLApi::with(&key).new();
273    let response = api.translate_text("Hello World", Lang::ZH).await.unwrap();
274
275    assert!(!response.translations.is_empty());
276
277    let translated_results = response.translations;
278    assert_eq!(translated_results[0].text, "你好,世界");
279    assert_eq!(translated_results[0].detected_source_language, Lang::EN);
280}
281
282#[tokio::test]
283async fn test_advanced_translate() {
284    let key = std::env::var("DEEPL_API_KEY").unwrap();
285    let api = DeepLApi::with(&key).new();
286
287    let response = api.translate_text(
288            "Hello World <keep additionalarg=\"test0\">This will stay exactly the way it was</keep>",
289            Lang::DE
290        )
291        .source_lang(Lang::EN)
292        .ignore_tags(vec!["keep".to_string()])
293        .tag_handling(TagHandling::Xml)
294        .await
295        .unwrap();
296
297    assert!(!response.translations.is_empty());
298
299    let translated_results = response.translations;
300    assert_eq!(
301        translated_results[0].text,
302        "Hallo Welt <keep additionalarg=\"test0\">This will stay exactly the way it was</keep>"
303    );
304    assert_eq!(translated_results[0].detected_source_language, Lang::EN);
305}
306
307#[tokio::test]
308async fn test_advanced_translator_html() {
309    let key = std::env::var("DEEPL_API_KEY").unwrap();
310    let api = DeepLApi::with(&key).new();
311
312    let response = api
313        .translate_text(
314            "Hello World <keep translate=\"no\">This will stay exactly the way it was</keep>",
315            Lang::DE,
316        )
317        .tag_handling(TagHandling::Html)
318        .await
319        .unwrap();
320
321    assert!(!response.translations.is_empty());
322
323    let translated_results = response.translations;
324    assert_eq!(
325        translated_results[0].text,
326        "Hallo Welt <keep translate=\"no\">This will stay exactly the way it was</keep>"
327    );
328    assert_eq!(translated_results[0].detected_source_language, Lang::EN);
329}
330
331#[tokio::test]
332async fn test_formality() {
333    let api = DeepLApi::with(&std::env::var("DEEPL_API_KEY").unwrap()).new();
334
335    // sends and returns a formality
336    let text = "How are you?";
337    let src = Lang::EN;
338    let trg = Lang::ES;
339    let more = Formality::More;
340
341    let response = api
342        .translate_text(text, trg)
343        .source_lang(src)
344        .formality(more)
345        .await
346        .unwrap();
347
348    assert!(!response.translations.is_empty());
349    assert_eq!(response.translations[0].text, "¿Cómo está?");
350
351    // response ok, despite target lang not supporting formality
352    let text = "¿Cómo estás?";
353    let src = Lang::ES;
354    let trg = Lang::EN_US;
355    let less = Formality::PreferLess;
356
357    let response = api
358        .translate_text(text, trg)
359        .source_lang(src)
360        .formality(less)
361        .await
362        .unwrap();
363
364    assert!(!response.translations.is_empty());
365    assert_eq!(response.translations[0].text, "How are you doing?");
366}