Skip to main content

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