deepl/endpoint/
translate.rs

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