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}