icu_locale/
directionality.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use crate::provider::*;
6use crate::LocaleExpander;
7use icu_locale_core::subtags::Script;
8use icu_locale_core::LanguageIdentifier;
9use icu_provider::prelude::*;
10
11/// Represents the direction of a script.
12///
13/// [`LocaleDirectionality`] can be used to get this information.
14#[derive(Debug, PartialEq, Eq, Clone, Copy)]
15#[non_exhaustive]
16pub enum Direction {
17    /// The script is left-to-right.
18    LeftToRight,
19    /// The script is right-to-left.
20    RightToLeft,
21}
22
23/// Provides methods to determine the direction of a locale.
24///
25/// The `Expander` generic parameter wraps a [`LocaleExpander`].
26///
27/// # Examples
28///
29/// ```
30/// use icu::locale::{langid, Direction, LocaleDirectionality};
31///
32/// let ld = LocaleDirectionality::new_common();
33///
34/// assert_eq!(ld.get(&langid!("en")), Some(Direction::LeftToRight));
35/// ```
36#[derive(Debug)]
37pub struct LocaleDirectionality<Expander = LocaleExpander> {
38    script_direction: DataPayload<LocaleScriptDirectionV1>,
39    expander: Expander,
40}
41
42impl LocaleDirectionality<LocaleExpander> {
43    /// Creates a [`LocaleDirectionality`] from compiled data, using [`LocaleExpander`]
44    /// data for common locales.
45    ///
46    /// This includes limited likely subtags data, see [`LocaleExpander::new_common()`].
47    #[cfg(feature = "compiled_data")]
48    pub const fn new_common() -> Self {
49        Self::new_with_expander(LocaleExpander::new_common())
50    }
51
52    // Note: This is a custom impl because the bounds on `try_new_unstable` don't suffice
53    #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER, Self::new_common)]
54    #[cfg(feature = "serde")]
55    pub fn try_new_common_with_buffer_provider(
56        provider: &(impl BufferProvider + ?Sized),
57    ) -> Result<Self, DataError> {
58        let expander = LocaleExpander::try_new_common_with_buffer_provider(provider)?;
59        Self::try_new_with_expander_unstable(&provider.as_deserializing(), expander)
60    }
61    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_common)]
62    pub fn try_new_common_unstable<P>(provider: &P) -> Result<LocaleDirectionality, DataError>
63    where
64        P: DataProvider<LocaleScriptDirectionV1>
65            + DataProvider<LocaleLikelySubtagsLanguageV1>
66            + DataProvider<LocaleLikelySubtagsScriptRegionV1>
67            + ?Sized,
68    {
69        let expander = LocaleExpander::try_new_common_unstable(provider)?;
70        Self::try_new_with_expander_unstable(provider, expander)
71    }
72
73    /// Creates a [`LocaleDirectionality`] from compiled data, using [`LocaleExpander`]
74    /// data for all locales.
75    ///
76    /// This includes all likely subtags data, see [`LocaleExpander::new_extended()`].
77    #[cfg(feature = "compiled_data")]
78    pub const fn new_extended() -> Self {
79        Self::new_with_expander(LocaleExpander::new_extended())
80    }
81
82    // Note: This is a custom impl because the bounds on `try_new_unstable` don't suffice
83    #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER, Self::new_extended)]
84    #[cfg(feature = "serde")]
85    pub fn try_new_extended_with_buffer_provider(
86        provider: &(impl BufferProvider + ?Sized),
87    ) -> Result<Self, DataError> {
88        let expander = LocaleExpander::try_new_extended_with_buffer_provider(provider)?;
89        Self::try_new_with_expander_unstable(&provider.as_deserializing(), expander)
90    }
91    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_extended)]
92    pub fn try_new_extended_unstable<P>(provider: &P) -> Result<LocaleDirectionality, DataError>
93    where
94        P: DataProvider<LocaleScriptDirectionV1>
95            + DataProvider<LocaleLikelySubtagsLanguageV1>
96            + DataProvider<LocaleLikelySubtagsScriptRegionV1>
97            + DataProvider<LocaleLikelySubtagsExtendedV1>
98            + ?Sized,
99    {
100        let expander = LocaleExpander::try_new_extended_unstable(provider)?;
101        Self::try_new_with_expander_unstable(provider, expander)
102    }
103}
104
105impl<Expander: AsRef<LocaleExpander>> LocaleDirectionality<Expander> {
106    /// Creates a [`LocaleDirectionality`] with a custom [`LocaleExpander`] and compiled data.
107    ///
108    /// This allows using [`LocaleExpander::new_extended()`] with data for all locales.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use icu::locale::{
114    ///     langid, Direction, LocaleDirectionality, LocaleExpander,
115    /// };
116    ///
117    /// let ld_default = LocaleDirectionality::new_common();
118    ///
119    /// assert_eq!(ld_default.get(&langid!("jbn")), None);
120    ///
121    /// let expander = LocaleExpander::new_extended();
122    /// let ld_extended = LocaleDirectionality::new_with_expander(expander);
123    ///
124    /// assert_eq!(
125    ///     ld_extended.get(&langid!("jbn")),
126    ///     Some(Direction::RightToLeft)
127    /// );
128    /// ```
129    #[cfg(feature = "compiled_data")]
130    pub const fn new_with_expander(expander: Expander) -> Self {
131        LocaleDirectionality {
132            script_direction: DataPayload::from_static_ref(
133                crate::provider::Baked::SINGLETON_LOCALE_SCRIPT_DIRECTION_V1,
134            ),
135            expander,
136        }
137    }
138
139    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_with_expander)]
140    pub fn try_new_with_expander_unstable<P>(
141        provider: &P,
142        expander: Expander,
143    ) -> Result<Self, DataError>
144    where
145        P: DataProvider<LocaleScriptDirectionV1> + ?Sized,
146    {
147        let script_direction = provider.load(Default::default())?.payload;
148
149        Ok(LocaleDirectionality {
150            script_direction,
151            expander,
152        })
153    }
154
155    /// Returns the script direction of the given locale.
156    ///
157    /// Note that the direction is a property of the script of a locale, not of the language. As such,
158    /// when given a locale without an associated script tag (i.e., `locale!("en")` vs. `locale!("en-Latn")`),
159    /// this method first tries to infer the script using the language and region before returning its direction.
160    ///
161    /// If you already have a script struct and want to get its direction, you should use
162    /// `Locale::from(Some(my_script))` and call this method.
163    ///
164    /// This method will return `None` if either a locale's script cannot be determined, or there is no information
165    /// for the script.
166    ///
167    /// # Examples
168    ///
169    /// Using an existing locale:
170    ///
171    /// ```
172    /// use icu::locale::{langid, Direction, LocaleDirectionality};
173    ///
174    /// let ld = LocaleDirectionality::new_common();
175    ///
176    /// assert_eq!(ld.get(&langid!("en-US")), Some(Direction::LeftToRight));
177    ///
178    /// assert_eq!(ld.get(&langid!("ar")), Some(Direction::RightToLeft));
179    ///
180    /// assert_eq!(ld.get(&langid!("en-Arab")), Some(Direction::RightToLeft));
181    ///
182    /// assert_eq!(ld.get(&langid!("foo")), None);
183    /// ```
184    ///
185    /// Using a script directly:
186    ///
187    /// ```
188    /// use icu::locale::subtags::script;
189    /// use icu::locale::{Direction, LanguageIdentifier, LocaleDirectionality};
190    ///
191    /// let ld = LocaleDirectionality::new_common();
192    ///
193    /// assert_eq!(
194    ///     ld.get(&LanguageIdentifier::from(Some(script!("Latn")))),
195    ///     Some(Direction::LeftToRight)
196    /// );
197    /// ```
198    pub fn get(&self, langid: &LanguageIdentifier) -> Option<Direction> {
199        let script = self.expander.as_ref().get_likely_script(langid)?;
200
201        if self.script_in_ltr(script) {
202            Some(Direction::LeftToRight)
203        } else if self.script_in_rtl(script) {
204            Some(Direction::RightToLeft)
205        } else {
206            None
207        }
208    }
209
210    /// Returns whether the given locale is right-to-left.
211    ///
212    /// Note that if this method returns `false`, the locale is either left-to-right or
213    /// the [`LocaleDirectionality`] does not include data for the locale.
214    /// You should use [`LocaleDirectionality::get`] if you need to differentiate between these cases.
215    ///
216    /// See [`LocaleDirectionality::get`] for more information.
217    pub fn is_right_to_left(&self, langid: &LanguageIdentifier) -> bool {
218        self.expander
219            .as_ref()
220            .get_likely_script(langid)
221            .map(|s| self.script_in_rtl(s))
222            .unwrap_or(false)
223    }
224
225    /// Returns whether the given locale is left-to-right.
226    ///
227    /// Note that if this method returns `false`, the locale is either right-to-left or
228    /// the [`LocaleDirectionality`] does not include data for the locale.
229    /// You should use [`LocaleDirectionality::get`] if you need to differentiate between these cases.
230    ///
231    /// See [`LocaleDirectionality::get`] for more information.
232    pub fn is_left_to_right(&self, langid: &LanguageIdentifier) -> bool {
233        self.expander
234            .as_ref()
235            .get_likely_script(langid)
236            .map(|s| self.script_in_ltr(s))
237            .unwrap_or(false)
238    }
239
240    fn script_in_rtl(&self, script: Script) -> bool {
241        self.script_direction
242            .get()
243            .rtl
244            .binary_search(&script.to_tinystr().to_unvalidated())
245            .is_ok()
246    }
247
248    fn script_in_ltr(&self, script: Script) -> bool {
249        self.script_direction
250            .get()
251            .ltr
252            .binary_search(&script.to_tinystr().to_unvalidated())
253            .is_ok()
254    }
255}