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