1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use reqwest::blocking::multipart;
use crate::{builder, DeepL, Error, Formality, Language};
/// Document handle
#[derive(Debug, Deserialize, Serialize)]
pub struct Document {
/// A unique ID assigned to the uploaded document
pub document_id: String,
/// Document encryption key
pub document_key: String,
}
/// Document translation status
#[derive(Debug, Deserialize)]
pub struct DocumentStatus {
/// A unique ID assigned to the uploaded document
pub document_id: String,
/// A short description of the current state of the document translation process
pub status: DocState,
/// Estimated number of seconds until the translation is done.
/// This parameter is only included while status is "translating".
pub seconds_remaining: Option<u64>,
/// The number of characters billed to your account
pub billed_characters: Option<u64>,
/// Description of the error, if available.
/// This parameter may be included if an error occurred during translation.
pub error_message: Option<String>,
}
/// Document state
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DocState {
/// The translation job is waiting in line to be processed
Queued,
/// The translation is currently ongoing
Translating,
/// The translation is done and the document is ready for download
Done,
/// An irrecoverable error occurred while translating the document
Error,
}
// DocumentOptions builder
builder! {
Document {
@must{
target_lang: Language,
file_path: PathBuf,
};
@optional{
source_lang: Language,
filename: String,
formality: Formality,
glossary_id: String,
};
}
}
impl DocumentStatus {
/// Whether the document is done translating and ready to be downloaded
pub fn is_done(&self) -> bool {
matches!(self.status, DocState::Done)
}
}
impl DocumentOptions {
/// Creates a multipart request form from an instance of `DocumentOptions`
fn into_multipart(self) -> Result<multipart::Form, Error> {
let mut form = multipart::Form::new()
.file("file", self.file_path)
.map_err(|_| Error::Api("failed to attach file".to_string()))?
.text("target_lang", self.target_lang.to_string());
if let Some(src) = self.source_lang {
form = form.text("source_lang", src.to_string());
}
if let Some(name) = self.filename {
form = form.text("filename", name);
}
if let Some(formality) = self.formality {
form = form.text("formality", formality.to_string());
}
if let Some(glos) = self.glossary_id {
form = form.text("glossary_id", glos);
}
Ok(form)
}
}
impl DeepL {
/// POST /document
///
/// Upload a document.
///
/// Translating a document consists of three steps: upload, polling translation status,
/// and download. `document_upload` returns a document handle that we need in order to
/// complete the remaining steps: getting the [`document_status`](Self::document_status)
/// and finally fetching the translation result with
/// [`document_download`](Self::document_download).
///
/// ## Example
///
/// ```rust,no_run
/// # use deeprl::*;
/// # use std::{env, fs, path::PathBuf, thread};
/// # let dl = DeepL::new(&env::var("DEEPL_API_KEY").unwrap());
/// # use std::time::Duration;
/// let file_path = PathBuf::from("test.txt");
/// let target_lang = Language::De;
/// let opt = DocumentOptions::new(target_lang, file_path);
/// let doc = dl.document_upload(opt).unwrap();
///
/// while !dl.document_status(&doc).unwrap().is_done() {
/// // wait a second
/// thread::sleep(Duration::from_secs(1));
/// }
///
/// let out_file = PathBuf::from("test-translated.txt");
/// let _ = dl.document_download(doc, Some(out_file.clone())).unwrap();
/// let content = fs::read_to_string(out_file).unwrap();
/// assert!(!content.is_empty());
/// ```
pub fn document_upload(&self, opt: DocumentOptions) -> Result<Document, Error> {
let url = format!("{}/document", self.url);
let form = opt.into_multipart()?;
let resp = self
.post(url)
.multipart(form)
.send()
.map_err(Error::Reqwest)?;
if !resp.status().is_success() {
return Err(Error::Response(
resp.status(),
resp.text().unwrap_or_default(),
));
}
Ok(resp.json()?)
}
/// POST /document/`{document_id}`
///
/// Get document translation status. In case there's an issue with translation,
/// [`DocumentStatus`] contains a field `error_message` that may provide context
/// for the cause of the error.
pub fn document_status(&self, doc: &Document) -> Result<DocumentStatus, Error> {
let doc_id = doc.document_id.clone();
let url = format!("{}/document/{}", self.url, doc_id);
let key = doc.document_key.clone();
let params = vec![("document_key", key)];
let resp = self
.post(url)
.form(¶ms)
.send()
.map_err(Error::Reqwest)?;
if !resp.status().is_success() {
return Err(Error::Response(
resp.status(),
resp.text().unwrap_or_default(),
));
}
Ok(resp.json()?)
}
/// POST /document/`{document_id}`/result
///
/// Download translated document.
///
/// If no `out_file` is given, the returned file path will have the name of the
/// [`Document`] id.
pub fn document_download(
&self,
doc: Document,
out_file: Option<PathBuf>,
) -> Result<PathBuf, Error> {
let doc_id = doc.document_id;
let url = format!("{}/document/{}/result", self.url, doc_id);
let params = vec![("document_key", doc.document_key)];
let mut resp = self
.post(url)
.form(¶ms)
.send()
.map_err(Error::Reqwest)?;
if !resp.status().is_success() {
return Err(Error::Response(
resp.status(),
resp.text().unwrap_or_default(),
));
}
// write out file
let mut buf: Vec<u8> = vec![];
let _ = resp.copy_to(&mut buf).map_err(Error::Reqwest)?;
let path = out_file.unwrap_or(PathBuf::from(doc_id));
std::fs::write(&path, buf).map_err(Error::Io)?;
Ok(path)
}
}