1use std::io;
2
3use rand::prelude::*;
4
5use super::data::{DeepLXTranslationResult, Lang, Params, PostData, TextItem, TranslationResponse};
6use super::error::{Error, LangDetectError};
7use super::utils::{get_i_count, get_random_number, get_timestamp};
8
9use reqwest::{
10 Client, Response, StatusCode,
11 header::{
12 ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CONNECTION, CONTENT_TYPE, COOKIE, DNT, HeaderMap,
13 HeaderValue, UPGRADE_INSECURE_REQUESTS, USER_AGENT,
14 },
15};
16#[cfg(not(target_arch = "wasm32"))]
17use reqwest::{Proxy, retry};
18
19pub struct Config {
54 pub base_url: String,
55 #[cfg(not(target_arch = "wasm32"))]
56 pub proxy: Option<String>,
57}
58
59impl Default for Config {
60 fn default() -> Self {
61 Self {
62 base_url: "https://www2.deepl.com/jsonrpc".to_string(),
63 #[cfg(not(target_arch = "wasm32"))]
64 proxy: None,
65 }
66 }
67}
68
69#[derive(Clone)]
75pub struct DeepLX {
76 base_url: String,
77 #[cfg(not(target_arch = "wasm32"))]
78 proxy: Option<String>,
79 headers: HeaderMap,
80}
81
82impl DeepLX {
83 pub fn new(config: Config) -> Self {
106 Self {
107 base_url: config.base_url,
108 #[cfg(not(target_arch = "wasm32"))]
109 proxy: config.proxy,
110 headers: headers(),
111 }
112 }
113
114 async fn make_request(
115 &self,
116 post_data: &PostData<'_>,
117 deepl_session: Option<&str>,
118 ) -> Result<(StatusCode, Response), Error> {
119 let mut headers = self.headers.clone();
120 if let Some(session) = deepl_session {
121 headers.insert(COOKIE, session.parse().unwrap());
122 }
123
124 let data = serde_json::to_string(&post_data)?;
125
126 let use_colon_spacing = ((post_data.id + 5) % 29 == 0) || ((post_data.id + 3) % 13 == 0);
127 let replacement = if use_colon_spacing {
128 r#""method" : ""#
129 } else {
130 r#""method": ""#
131 };
132
133 let data = data.replacen(r#""method":""#, replacement, 1);
134
135 let builder = Client::builder();
136
137 #[cfg(not(target_arch = "wasm32"))]
138 let builder = match &self.proxy {
139 Some(p) => builder.proxy(Proxy::all(p.clone())?),
140 None => builder,
141 };
142
143 #[cfg(not(target_arch = "wasm32"))]
144 let builder = builder.retry(
145 retry::for_host(self.base_url.clone())
146 .max_retries_per_request(3)
147 .classify_fn(|req| match req.status() {
148 Some(status) if status == StatusCode::TOO_MANY_REQUESTS => req.retryable(),
149 _ => req.success(),
150 }),
151 );
152
153 let resp = builder
154 .build()?
155 .post(&self.base_url)
156 .headers(headers)
157 .body(data)
158 .send()
159 .await?;
160
161 Ok((resp.status(), resp))
162 }
163
164 pub async fn translate(
209 &self,
210 source_lang: &str,
211 target_lang: &str,
212 text: &str,
213 deepl_session: Option<&str>,
214 ) -> Result<DeepLXTranslationResult, Error> {
215 if text.is_empty() {
217 return Ok(DeepLXTranslationResult {
218 code: 404,
219 message: Some("No text to translate".to_string()),
220 ..Default::default()
221 });
222 }
223
224 let source_lang_detached = match source_lang {
226 "auto" | "" => {
227 let iso_639_3 = whatlang::detect_lang(text)
228 .ok_or(LangDetectError::from(io::Error::new(
229 io::ErrorKind::InvalidInput,
230 "Failed to detect language",
231 )))?
232 .code();
233
234 isolang::Language::from_639_3(iso_639_3)
235 .and_then(|lang| lang.to_639_1())
236 .map(|iso_639_1| iso_639_1.to_uppercase())
237 .ok_or(LangDetectError::from(io::Error::new(
238 io::ErrorKind::InvalidInput,
239 "Could not map detected language to ISO 639-1",
240 )))?
241 }
242 _ => source_lang.to_uppercase(),
243 };
244
245 let target_lang_parts = target_lang.split('-').collect::<Vec<&str>>();
247 let (target_lang_code, _) = if target_lang_parts.len() > 1 {
248 (target_lang_parts[0].to_uppercase(), true)
249 } else {
250 (target_lang.to_uppercase(), false)
251 };
252
253 let id = get_random_number();
255 let i_count = get_i_count(text);
256 let timestamp = get_timestamp(i_count);
257
258 let post_data = PostData {
259 json_rpc: "2.0",
260 method: "LMT_handle_texts",
261 id,
262 params: Params {
263 splitting: "newlines",
264 lang: Lang {
265 source_lang_user_selected: source_lang,
266 target_lang: target_lang_code.as_str(),
267 ..Default::default()
268 },
269 texts: vec![TextItem {
270 text,
271 request_alternatives: 3,
272 }],
273 timestamp,
274 },
275 };
276
277 let (status, resp) = self.make_request(&post_data, deepl_session).await?;
279 if !status.is_success() {
280 return Ok(DeepLXTranslationResult {
281 code: status.as_u16() as i32,
282 ..Default::default()
283 });
284 }
285
286 let resp: TranslationResponse = resp.json().await?;
287
288 let texts = resp.result.texts;
289 if texts.is_empty() {
290 return Ok(DeepLXTranslationResult {
291 code: 503,
292 message: Some("Translation failed".to_string()),
293 ..Default::default()
294 });
295 }
296
297 let main_translation = texts[0].text.clone();
298
299 let alternatives: Vec<String> = texts
302 .iter()
303 .map(|t| t.text.clone())
304 .filter(|alt| !alt.is_empty())
305 .collect();
306
307 Ok(DeepLXTranslationResult {
308 code: 200,
309 id,
310 data: main_translation,
311 alternatives,
312 source_lang: if resp.result.lang.is_empty() {
313 source_lang_detached
314 } else {
315 resp.result.lang
316 },
317 target_lang: target_lang.to_string(),
318 method: if deepl_session.is_none() {
319 "Free"
320 } else {
321 "Pro"
322 }
323 .to_string(),
324 ..Default::default()
325 })
326 }
327}
328
329const USER_AGENTS: &[&str] = &[
330 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
331 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
332 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
333 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
334 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0",
335];
336
337const ACCEPT_LANGUAGES: &[&str] = &[
338 "en-US,en;q=0.9",
339 "en-GB,en;q=0.9",
340 "en-US,en;q=0.8,es;q=0.6",
341 "en-US,en;q=0.9,fr;q=0.8",
342 "en-US,en;q=0.9,de;q=0.8",
343];
344
345fn headers() -> HeaderMap {
346 let mut rng = rand::rng();
347
348 let mut headers = HeaderMap::new();
349 headers.insert(
350 CONTENT_TYPE,
351 HeaderValue::from_static("application/json; charset=utf-8"),
352 );
353 headers.insert(
354 USER_AGENT,
355 HeaderValue::from_str(USER_AGENTS.choose(&mut rng).unwrap()).unwrap(),
356 );
357 headers.insert(
358 ACCEPT_LANGUAGE,
359 HeaderValue::from_str(ACCEPT_LANGUAGES.choose(&mut rng).unwrap()).unwrap(),
360 );
361 headers.insert(
362 ACCEPT,
363 HeaderValue::from_static("application/json, text/plain, */*"),
364 );
365 headers.insert(
366 ACCEPT_ENCODING,
367 HeaderValue::from_static("gzip, deflate, br"),
368 );
369 headers.insert(DNT, HeaderValue::from_static("1"));
370 headers.insert(CONNECTION, HeaderValue::from_static("keep-alive"));
371 headers.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1"));
372
373 headers
374}