Skip to main content

translation/
translation_session.rs

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")]
17/// Mirrors `TranslationSession.Strategy` from Translation.framework.
18pub enum TranslationStrategy {
19    /// Prefer higher-quality translations when available.
20    #[default]
21    HighFidelity,
22    /// Prefer lower-latency translations when available.
23    LowLatency,
24}
25
26/// Alias mirroring `TranslationSession.Strategy` inside the `translation_session` module.
27pub 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")]
48/// Serializable mirror of `TranslationSession.Configuration` from Translation.framework.
49pub struct TranslationSessionConfiguration {
50    source: String,
51    target: Option<String>,
52    #[serde(default)]
53    preferred_strategy: TranslationStrategy,
54}
55
56impl TranslationSessionConfiguration {
57    #[must_use]
58    /// Creates a session configuration from explicit source and target identifiers.
59    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    /// Creates a session configuration with an optional target identifier.
69    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    /// Creates a session configuration from a `LanguagePair`.
79    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    /// Converts a `TranslationConfiguration` into a session configuration.
91    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    /// Returns the source language identifier.
108    pub fn source(&self) -> &str {
109        &self.source
110    }
111
112    #[must_use]
113    /// Returns the target identifier or an empty string when unspecified.
114    pub fn target(&self) -> &str {
115        self.target.as_deref().unwrap_or("")
116    }
117
118    #[must_use]
119    /// Returns the optional target language identifier.
120    pub fn optional_target(&self) -> Option<&str> {
121        self.target.as_deref()
122    }
123
124    #[must_use]
125    /// Returns the preferred Translation.framework strategy.
126    pub const fn preferred_strategy(&self) -> TranslationStrategy {
127        self.preferred_strategy
128    }
129
130    #[must_use]
131    /// Returns a copy with the given preferred strategy.
132    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    /// Returns this configuration as a `LanguagePair`.
139    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")]
149/// Serializable counterpart to `TranslationSession.Request` in Translation.framework.
150pub 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    /// Creates a translation request from source text.
160    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    /// Creates a translation request from attributed source text.
170    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    /// Returns the source text to translate.
183    pub fn source_text(&self) -> &str {
184        &self.source_text
185    }
186
187    #[must_use]
188    /// Returns the attributed source text when this request carries attributed input.
189    pub fn attributed_source_text(&self) -> Option<&TranslationAttributedString> {
190        self.attributed_source_text.as_ref()
191    }
192
193    /// Replaces the source text to translate and clears attributed input.
194    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    /// Replaces the attributed source text to translate.
200    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    /// Clears the attributed source text payload while preserving plain text.
210    pub fn clear_attributed_source_text(&mut self) {
211        self.attributed_source_text = None;
212    }
213
214    #[must_use]
215    /// Returns the client identifier, if one is set.
216    pub fn client_identifier(&self) -> Option<&str> {
217        self.client_identifier.as_deref()
218    }
219
220    /// Sets the client identifier for correlation with framework responses.
221    pub fn set_client_identifier(&mut self, client_identifier: impl Into<String>) {
222        self.client_identifier = Some(client_identifier.into());
223    }
224
225    /// Clears the client identifier.
226    pub fn clear_client_identifier(&mut self) {
227        self.client_identifier = None;
228    }
229
230    #[must_use]
231    /// Returns a copy with the given attributed source text.
232    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    /// Returns a copy with the given client identifier.
242    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
248/// Streams responses from `TranslationSession.translations(from:)`.
249pub 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    /// Advances to the next streaming translation response.
265    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    /// Collects all remaining streaming responses into a vector.
287    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
311/// Wraps Translation.framework's `TranslationSession`.
312pub 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    /// Creates a session from a `TranslationSessionConfiguration`.
328    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    /// Creates a session from a `LanguagePair`.
345    pub fn from_language_pair(pair: impl Into<LanguagePair>) -> Result<Self, TranslationError> {
346        Self::new(TranslationSessionConfiguration::from_language_pair(pair))
347    }
348
349    /// Creates a session from a mutable `TranslationConfiguration`.
350    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    /// Returns the session configuration.
365    pub fn configuration(&self) -> &TranslationSessionConfiguration {
366        &self.configuration
367    }
368
369    #[must_use]
370    /// Returns the configured source language.
371    pub fn source_language(&self) -> Option<Language> {
372        Some(Language::from(self.configuration.source().to_owned()))
373    }
374
375    #[must_use]
376    /// Returns the configured target language, if one was set.
377    pub fn target_language(&self) -> Option<Language> {
378        self.configuration
379            .optional_target()
380            .map(|language| Language::from(language.to_owned()))
381    }
382
383    /// Returns the Translation.framework preferred strategy for this session.
384    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    /// Reports whether the session can request language pack downloads.
402    pub fn can_request_downloads(&self) -> Result<bool, TranslationError> {
403        self.read_bool(ffi::trl_session_can_request_downloads)
404    }
405
406    /// Reports whether Translation.framework considers the session ready.
407    pub fn is_ready(&self) -> Result<bool, TranslationError> {
408        self.read_bool(ffi::trl_session_is_ready)
409    }
410
411    /// Cancels the underlying Translation.framework session.
412    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    /// Prepares language resources via `TranslationSession.prepareTranslation()`.
423    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    /// Translates a single string with `TranslationSession.translate(_:)`.
434    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    /// Translates attributed text with `TranslationSession.translate(_:)`.
454    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    /// Translates a batch with `TranslationSession.translations(from:)`.
477    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    /// Starts a streaming batch translation for `TranslationSession.translations(from:)`.
500    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}