1use core::ffi::{c_char, c_void};
2use core::ptr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::ffi;
7use crate::language::Language;
8use crate::language_pair::LanguagePair;
9use crate::private::{error_from_status, json_cstring, parse_json_ptr, to_cstring};
10use crate::translation_attributes::TranslationAttributedString;
11use crate::translation_configuration::TranslationConfiguration;
12use crate::translation_error::TranslationError;
13use crate::translation_response::TranslationResponse;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "camelCase")]
17pub enum TranslationStrategy {
19 #[default]
21 HighFidelity,
22 LowLatency,
24}
25
26pub use TranslationStrategy as Strategy;
28
29impl TranslationStrategy {
30 pub(crate) const fn from_raw(raw: i32) -> Option<Self> {
31 match raw {
32 0 => Some(Self::HighFidelity),
33 1 => Some(Self::LowLatency),
34 _ => None,
35 }
36 }
37
38 pub(crate) const fn raw(self) -> i32 {
39 match self {
40 Self::HighFidelity => 0,
41 Self::LowLatency => 1,
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct TranslationSessionConfiguration {
50 source: String,
51 target: Option<String>,
52 #[serde(default)]
53 preferred_strategy: TranslationStrategy,
54}
55
56impl TranslationSessionConfiguration {
57 #[must_use]
58 pub fn new(source: impl Into<String>, target: impl Into<String>) -> Self {
60 Self {
61 source: source.into(),
62 target: Some(target.into()),
63 preferred_strategy: TranslationStrategy::default(),
64 }
65 }
66
67 #[must_use]
68 pub fn with_optional_target(source: impl Into<String>, target: Option<String>) -> Self {
70 Self {
71 source: source.into(),
72 target,
73 preferred_strategy: TranslationStrategy::default(),
74 }
75 }
76
77 #[must_use]
78 pub fn from_language_pair(pair: impl Into<LanguagePair>) -> Self {
80 let pair = pair.into();
81 Self {
82 source: pair.source().identifier().to_owned(),
83 target: pair
84 .target()
85 .map(|language| language.identifier().to_owned()),
86 preferred_strategy: TranslationStrategy::default(),
87 }
88 }
89
90 pub fn try_from_translation_configuration(
92 configuration: &TranslationConfiguration,
93 ) -> Result<Self, TranslationError> {
94 let source = configuration.source_identifier().ok_or_else(|| {
95 TranslationError::InvalidArgument(
96 "manual TranslationSession construction requires a source language".to_owned(),
97 )
98 })?;
99 Ok(Self::with_optional_target(
100 source.to_owned(),
101 configuration.target_identifier().map(ToOwned::to_owned),
102 )
103 .with_preferred_strategy(configuration.preferred_strategy()))
104 }
105
106 #[must_use]
107 pub fn source(&self) -> &str {
109 &self.source
110 }
111
112 #[must_use]
113 pub fn target(&self) -> &str {
115 self.target.as_deref().unwrap_or("")
116 }
117
118 #[must_use]
119 pub fn optional_target(&self) -> Option<&str> {
121 self.target.as_deref()
122 }
123
124 #[must_use]
125 pub const fn preferred_strategy(&self) -> TranslationStrategy {
127 self.preferred_strategy
128 }
129
130 #[must_use]
131 pub fn with_preferred_strategy(mut self, preferred_strategy: TranslationStrategy) -> Self {
133 self.preferred_strategy = preferred_strategy;
134 self
135 }
136
137 #[must_use]
138 pub fn language_pair(&self) -> LanguagePair {
140 LanguagePair::new(
141 Language::from(self.source.clone()),
142 self.target.clone().map(Language::from),
143 )
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct TranslationRequest {
151 source_text: String,
152 #[serde(default)]
153 attributed_source_text: Option<TranslationAttributedString>,
154 client_identifier: Option<String>,
155}
156
157impl TranslationRequest {
158 #[must_use]
159 pub fn new(source_text: impl Into<String>) -> Self {
161 Self {
162 source_text: source_text.into(),
163 attributed_source_text: None,
164 client_identifier: None,
165 }
166 }
167
168 #[must_use]
169 pub fn from_attributed_source_text(
171 source_text: impl Into<TranslationAttributedString>,
172 ) -> Self {
173 let source_text = source_text.into();
174 Self {
175 source_text: source_text.text().to_owned(),
176 attributed_source_text: Some(source_text),
177 client_identifier: None,
178 }
179 }
180
181 #[must_use]
182 pub fn source_text(&self) -> &str {
184 &self.source_text
185 }
186
187 #[must_use]
188 pub fn attributed_source_text(&self) -> Option<&TranslationAttributedString> {
190 self.attributed_source_text.as_ref()
191 }
192
193 pub fn set_source_text(&mut self, source_text: impl Into<String>) {
195 self.source_text = source_text.into();
196 self.attributed_source_text = None;
197 }
198
199 pub fn set_attributed_source_text(
201 &mut self,
202 attributed_source_text: impl Into<TranslationAttributedString>,
203 ) {
204 let attributed_source_text = attributed_source_text.into();
205 attributed_source_text.text().clone_into(&mut self.source_text);
206 self.attributed_source_text = Some(attributed_source_text);
207 }
208
209 pub fn clear_attributed_source_text(&mut self) {
211 self.attributed_source_text = None;
212 }
213
214 #[must_use]
215 pub fn client_identifier(&self) -> Option<&str> {
217 self.client_identifier.as_deref()
218 }
219
220 pub fn set_client_identifier(&mut self, client_identifier: impl Into<String>) {
222 self.client_identifier = Some(client_identifier.into());
223 }
224
225 pub fn clear_client_identifier(&mut self) {
227 self.client_identifier = None;
228 }
229
230 #[must_use]
231 pub fn with_attributed_source_text(
233 mut self,
234 attributed_source_text: impl Into<TranslationAttributedString>,
235 ) -> Self {
236 self.set_attributed_source_text(attributed_source_text);
237 self
238 }
239
240 #[must_use]
241 pub fn with_client_identifier(mut self, client_identifier: impl Into<String>) -> Self {
243 self.set_client_identifier(client_identifier);
244 self
245 }
246}
247
248pub struct TranslationBatchResponse {
250 token: *mut c_void,
251 finished: bool,
252}
253
254impl Drop for TranslationBatchResponse {
255 fn drop(&mut self) {
256 if !self.token.is_null() {
257 unsafe { ffi::trl_batch_response_release(self.token) };
258 self.token = ptr::null_mut();
259 }
260 }
261}
262
263impl TranslationBatchResponse {
264 pub fn try_next(&mut self) -> Result<Option<TranslationResponse>, TranslationError> {
266 if self.finished {
267 return Ok(None);
268 }
269
270 let mut response_json: *mut c_char = ptr::null_mut();
271 let mut err_msg: *mut c_char = ptr::null_mut();
272 let status = unsafe {
273 ffi::trl_batch_response_next_json(self.token, &mut response_json, &mut err_msg)
274 };
275 if status != ffi::status::OK {
276 self.finished = true;
277 return Err(unsafe { error_from_status(status, err_msg) });
278 }
279 if response_json.is_null() {
280 self.finished = true;
281 return Ok(None);
282 }
283 unsafe { parse_json_ptr(response_json, "streaming translation response") }.map(Some)
284 }
285
286 pub fn collect_all(mut self) -> Result<Vec<TranslationResponse>, TranslationError> {
288 let mut responses = Vec::new();
289 while let Some(response) = self.try_next()? {
290 responses.push(response);
291 }
292 Ok(responses)
293 }
294}
295
296impl Iterator for TranslationBatchResponse {
297 type Item = Result<TranslationResponse, TranslationError>;
298
299 fn next(&mut self) -> Option<Self::Item> {
300 match self.try_next() {
301 Ok(Some(response)) => Some(Ok(response)),
302 Ok(None) => None,
303 Err(error) => {
304 self.finished = true;
305 Some(Err(error))
306 }
307 }
308 }
309}
310
311pub struct TranslationSession {
313 token: *mut c_void,
314 configuration: TranslationSessionConfiguration,
315}
316
317impl Drop for TranslationSession {
318 fn drop(&mut self) {
319 if !self.token.is_null() {
320 unsafe { ffi::trl_session_release(self.token) };
321 self.token = ptr::null_mut();
322 }
323 }
324}
325
326impl TranslationSession {
327 pub fn new(configuration: TranslationSessionConfiguration) -> Result<Self, TranslationError> {
329 let configuration_json = json_cstring(&configuration)?;
330 let mut token: *mut c_void = ptr::null_mut();
331 let mut err_msg: *mut c_char = ptr::null_mut();
332 let status =
333 unsafe { ffi::trl_session_new(configuration_json.as_ptr(), &mut token, &mut err_msg) };
334 if status == ffi::status::OK && !token.is_null() {
335 Ok(Self {
336 token,
337 configuration,
338 })
339 } else {
340 Err(unsafe { error_from_status(status, err_msg) })
341 }
342 }
343
344 pub fn from_language_pair(pair: impl Into<LanguagePair>) -> Result<Self, TranslationError> {
346 Self::new(TranslationSessionConfiguration::from_language_pair(pair))
347 }
348
349 pub fn from_translation_configuration(
351 configuration: &TranslationConfiguration,
352 ) -> Result<Self, TranslationError> {
353 Self::new(
354 TranslationSessionConfiguration::try_from_translation_configuration(configuration)?,
355 )
356 }
357
358 #[cfg(feature = "async")]
359 pub(crate) const fn raw_token(&self) -> *mut c_void {
360 self.token
361 }
362
363 #[must_use]
364 pub fn configuration(&self) -> &TranslationSessionConfiguration {
366 &self.configuration
367 }
368
369 #[must_use]
370 pub fn source_language(&self) -> Option<Language> {
372 Some(Language::from(self.configuration.source().to_owned()))
373 }
374
375 #[must_use]
376 pub fn target_language(&self) -> Option<Language> {
378 self.configuration
379 .optional_target()
380 .map(|language| Language::from(language.to_owned()))
381 }
382
383 pub fn preferred_strategy(&self) -> Result<TranslationStrategy, TranslationError> {
385 let mut raw = 0;
386 let mut err_msg: *mut c_char = ptr::null_mut();
387 let status = unsafe {
388 ffi::trl_session_preferred_strategy(self.token, &mut raw, &mut err_msg)
389 };
390 if status == ffi::status::OK {
391 TranslationStrategy::from_raw(raw).ok_or_else(|| {
392 TranslationError::Unknown(format!(
393 "unknown TranslationSession.Strategy raw value returned by Swift bridge: {raw}"
394 ))
395 })
396 } else {
397 Err(unsafe { error_from_status(status, err_msg) })
398 }
399 }
400
401 pub fn can_request_downloads(&self) -> Result<bool, TranslationError> {
403 self.read_bool(ffi::trl_session_can_request_downloads)
404 }
405
406 pub fn is_ready(&self) -> Result<bool, TranslationError> {
408 self.read_bool(ffi::trl_session_is_ready)
409 }
410
411 pub fn cancel(&self) -> Result<(), TranslationError> {
413 let mut err_msg: *mut c_char = ptr::null_mut();
414 let status = unsafe { ffi::trl_session_cancel(self.token, &mut err_msg) };
415 if status == ffi::status::OK {
416 Ok(())
417 } else {
418 Err(unsafe { error_from_status(status, err_msg) })
419 }
420 }
421
422 pub fn prepare_translation(&self) -> Result<(), TranslationError> {
424 let mut err_msg: *mut c_char = ptr::null_mut();
425 let status = unsafe { ffi::trl_session_prepare_translation(self.token, &mut err_msg) };
426 if status == ffi::status::OK {
427 Ok(())
428 } else {
429 Err(unsafe { error_from_status(status, err_msg) })
430 }
431 }
432
433 pub fn translate(&self, text: &str) -> Result<TranslationResponse, TranslationError> {
435 let text = to_cstring(text)?;
436 let mut response_json: *mut c_char = ptr::null_mut();
437 let mut err_msg: *mut c_char = ptr::null_mut();
438 let status = unsafe {
439 ffi::trl_session_translate_text_json(
440 self.token,
441 text.as_ptr(),
442 &mut response_json,
443 &mut err_msg,
444 )
445 };
446 if status == ffi::status::OK {
447 unsafe { parse_json_ptr(response_json, "translation response") }
448 } else {
449 Err(unsafe { error_from_status(status, err_msg) })
450 }
451 }
452
453 pub fn translate_attributed(
455 &self,
456 text: &TranslationAttributedString,
457 ) -> Result<TranslationResponse, TranslationError> {
458 let text_json = json_cstring(text)?;
459 let mut response_json: *mut c_char = ptr::null_mut();
460 let mut err_msg: *mut c_char = ptr::null_mut();
461 let status = unsafe {
462 ffi::trl_session_translate_attributed_json(
463 self.token,
464 text_json.as_ptr(),
465 &mut response_json,
466 &mut err_msg,
467 )
468 };
469 if status == ffi::status::OK {
470 unsafe { parse_json_ptr(response_json, "translation response") }
471 } else {
472 Err(unsafe { error_from_status(status, err_msg) })
473 }
474 }
475
476 pub fn translate_batch(
478 &self,
479 requests: &[TranslationRequest],
480 ) -> Result<Vec<TranslationResponse>, TranslationError> {
481 let requests_json = json_cstring(requests)?;
482 let mut responses_json: *mut c_char = ptr::null_mut();
483 let mut err_msg: *mut c_char = ptr::null_mut();
484 let status = unsafe {
485 ffi::trl_session_translate_batch_json(
486 self.token,
487 requests_json.as_ptr(),
488 &mut responses_json,
489 &mut err_msg,
490 )
491 };
492 if status == ffi::status::OK {
493 unsafe { parse_json_ptr(responses_json, "translation batch responses") }
494 } else {
495 Err(unsafe { error_from_status(status, err_msg) })
496 }
497 }
498
499 pub fn translate_batch_streaming(
501 &self,
502 requests: &[TranslationRequest],
503 ) -> Result<TranslationBatchResponse, TranslationError> {
504 let requests_json = json_cstring(requests)?;
505 let mut batch_token: *mut c_void = ptr::null_mut();
506 let mut err_msg: *mut c_char = ptr::null_mut();
507 let status = unsafe {
508 ffi::trl_session_translate_batch_stream_json(
509 self.token,
510 requests_json.as_ptr(),
511 &mut batch_token,
512 &mut err_msg,
513 )
514 };
515 if status == ffi::status::OK && !batch_token.is_null() {
516 Ok(TranslationBatchResponse {
517 token: batch_token,
518 finished: false,
519 })
520 } else {
521 Err(unsafe { error_from_status(status, err_msg) })
522 }
523 }
524
525 fn read_bool(
526 &self,
527 ffi_fn: unsafe extern "C" fn(*mut c_void, *mut i32, *mut *mut c_char) -> i32,
528 ) -> Result<bool, TranslationError> {
529 let mut value = 0;
530 let mut err_msg: *mut c_char = ptr::null_mut();
531 let status = unsafe { ffi_fn(self.token, &mut value, &mut err_msg) };
532 if status == ffi::status::OK {
533 Ok(value != 0)
534 } else {
535 Err(unsafe { error_from_status(status, err_msg) })
536 }
537 }
538}