Skip to main content

translation/
language_availability.rs

1use core::ffi::{c_char, c_void};
2use core::ptr;
3
4use crate::ffi;
5use crate::language::Language;
6use crate::language_pair::LanguagePair;
7use crate::private::{error_from_status, parse_json_ptr, to_cstring};
8use crate::translation_error::TranslationError;
9use crate::translation_session::TranslationStrategy;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12/// Mirrors `LanguageAvailability.Status` from Translation.framework.
13pub enum LanguageAvailabilityStatus {
14    /// Translation resources are installed for this request.
15    Installed,
16    /// Translation resources are supported but may need installation.
17    Supported,
18    /// Translation.framework cannot fulfill this request.
19    Unsupported,
20    /// Translation.framework returned an unknown raw status value.
21    Unknown(i32),
22}
23
24impl LanguageAvailabilityStatus {
25    #[must_use]
26    /// Converts a raw Translation.framework status code into a typed status.
27    pub const fn from_raw(raw: i32) -> Self {
28        match raw {
29            0 => Self::Installed,
30            1 => Self::Supported,
31            2 => Self::Unsupported,
32            other => Self::Unknown(other),
33        }
34    }
35}
36
37/// Wraps Translation.framework's `LanguageAvailability`.
38pub struct LanguageAvailability {
39    token: *mut c_void,
40}
41
42impl Drop for LanguageAvailability {
43    fn drop(&mut self) {
44        if !self.token.is_null() {
45            unsafe { ffi::trl_language_availability_release(self.token) };
46            self.token = ptr::null_mut();
47        }
48    }
49}
50
51impl LanguageAvailability {
52    /// Creates a new `LanguageAvailability` wrapper.
53    pub fn new() -> Result<Self, TranslationError> {
54        let token = unsafe { ffi::trl_language_availability_new() };
55        if token.is_null() {
56            return Err(TranslationError::UnavailableOnThisMacOS(
57                "LanguageAvailability requires macOS 15+".to_owned(),
58            ));
59        }
60        Ok(Self { token })
61    }
62
63    /// Creates a `LanguageAvailability` wrapper with a preferred strategy.
64    pub fn with_preferred_strategy(
65        preferred_strategy: TranslationStrategy,
66    ) -> Result<Self, TranslationError> {
67        let mut token: *mut c_void = ptr::null_mut();
68        let mut err_msg: *mut c_char = ptr::null_mut();
69        let status = unsafe {
70            ffi::trl_language_availability_new_with_preferred_strategy(
71                preferred_strategy.raw(),
72                &mut token,
73                &mut err_msg,
74            )
75        };
76        if status == ffi::status::OK && !token.is_null() {
77            Ok(Self { token })
78        } else {
79            Err(unsafe { error_from_status(status, err_msg) })
80        }
81    }
82
83    #[cfg(feature = "async")]
84    pub(crate) const fn raw_token(&self) -> *mut c_void {
85        self.token
86    }
87
88    /// Returns Translation.framework's preferred strategy for this availability probe.
89    pub fn preferred_strategy(&self) -> Result<TranslationStrategy, TranslationError> {
90        let mut raw = 0;
91        let mut err_msg: *mut c_char = ptr::null_mut();
92        let status = unsafe {
93            ffi::trl_language_availability_preferred_strategy(self.token, &mut raw, &mut err_msg)
94        };
95        if status == ffi::status::OK {
96            TranslationStrategy::from_raw(raw).ok_or_else(|| {
97                TranslationError::Unknown(format!(
98                    "unknown TranslationSession.Strategy raw value returned by Swift bridge: {raw}"
99                ))
100            })
101        } else {
102            Err(unsafe { error_from_status(status, err_msg) })
103        }
104    }
105
106    /// Returns supported language identifiers from `LanguageAvailability.supportedLanguages`.
107    pub fn supported_languages(&self) -> Result<Vec<String>, TranslationError> {
108        self.supported_language_objects()
109            .map(|languages| languages.into_iter().map(String::from).collect())
110    }
111
112    /// Returns supported language objects from `LanguageAvailability.supportedLanguages`.
113    pub fn supported_language_objects(&self) -> Result<Vec<Language>, TranslationError> {
114        let mut languages_json: *mut c_char = ptr::null_mut();
115        let mut err_msg: *mut c_char = ptr::null_mut();
116        let status = unsafe {
117            ffi::trl_language_availability_supported_languages_json(
118                self.token,
119                &mut languages_json,
120                &mut err_msg,
121            )
122        };
123        if status == ffi::status::OK {
124            unsafe { parse_json_ptr(languages_json, "supported languages") }
125        } else {
126            Err(unsafe { error_from_status(status, err_msg) })
127        }
128    }
129
130    /// Returns pair availability for explicit source and target identifiers.
131    pub fn status_for_pair(
132        &self,
133        source_language: &str,
134        target_language: &str,
135    ) -> Result<LanguageAvailabilityStatus, TranslationError> {
136        let source_language = Language::from(source_language);
137        let target_language = Language::from(target_language);
138        self.status_for_languages(&source_language, Some(&target_language))
139    }
140
141    /// Returns pair availability for a `LanguagePair`.
142    pub fn status_for_language_pair(
143        &self,
144        pair: &LanguagePair,
145    ) -> Result<LanguageAvailabilityStatus, TranslationError> {
146        self.status_for_languages(pair.source(), pair.target())
147    }
148
149    /// Returns pair availability using `LanguageAvailability.status(from:to:)`.
150    pub fn status_for_languages(
151        &self,
152        source_language: &Language,
153        target_language: Option<&Language>,
154    ) -> Result<LanguageAvailabilityStatus, TranslationError> {
155        let source_language = to_cstring(source_language.identifier())?;
156        let target_language = target_language
157            .map(|language| to_cstring(language.identifier()))
158            .transpose()?;
159        let mut status_raw = 0;
160        let mut err_msg: *mut c_char = ptr::null_mut();
161        let status = unsafe {
162            ffi::trl_language_availability_status_from_to(
163                self.token,
164                source_language.as_ptr(),
165                target_language
166                    .as_ref()
167                    .map_or(ptr::null(), |language| language.as_ptr()),
168                &mut status_raw,
169                &mut err_msg,
170            )
171        };
172        if status == ffi::status::OK {
173            Ok(LanguageAvailabilityStatus::from_raw(status_raw))
174        } else {
175            Err(unsafe { error_from_status(status, err_msg) })
176        }
177    }
178
179    /// Returns translation availability for text and a target identifier.
180    pub fn status_for_text(
181        &self,
182        text: &str,
183        target_language: &str,
184    ) -> Result<LanguageAvailabilityStatus, TranslationError> {
185        let target_language = Language::from(target_language);
186        self.status_for_text_in_language(text, Some(&target_language))
187    }
188
189    /// Returns translation availability for text with an optional target language.
190    pub fn status_for_text_in_language(
191        &self,
192        text: &str,
193        target_language: Option<&Language>,
194    ) -> Result<LanguageAvailabilityStatus, TranslationError> {
195        let text = to_cstring(text)?;
196        let target_language = target_language
197            .map(|language| to_cstring(language.identifier()))
198            .transpose()?;
199        let mut status_raw = 0;
200        let mut err_msg: *mut c_char = ptr::null_mut();
201        let status = unsafe {
202            ffi::trl_language_availability_status_for_text(
203                self.token,
204                text.as_ptr(),
205                target_language
206                    .as_ref()
207                    .map_or(ptr::null(), |language| language.as_ptr()),
208                &mut status_raw,
209                &mut err_msg,
210            )
211        };
212        if status == ffi::status::OK {
213            Ok(LanguageAvailabilityStatus::from_raw(status_raw))
214        } else {
215            Err(unsafe { error_from_status(status, err_msg) })
216        }
217    }
218}