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 | |
5 | //! A data provider wrapper that performs locale fallback. |
6 | |
7 | use crate::helpers::result_is_err_missing_locale; |
8 | use icu_locid_transform::provider::*; |
9 | use icu_provider::prelude::*; |
10 | |
11 | #[doc (hidden)] // moved |
12 | pub use icu_locid_transform::fallback::{ |
13 | LocaleFallbackIterator, LocaleFallbacker, LocaleFallbackerWithConfig, |
14 | }; |
15 | #[doc (hidden)] // moved |
16 | pub use icu_provider::fallback::LocaleFallbackConfig; |
17 | |
18 | /// A data provider wrapper that performs locale fallback. This enables arbitrary locales to be |
19 | /// handled at runtime. |
20 | /// |
21 | /// # Examples |
22 | /// |
23 | /// ``` |
24 | /// use icu_locid::locale; |
25 | /// use icu_provider::prelude::*; |
26 | /// use icu_provider::hello_world::*; |
27 | /// use icu_provider_adapters::fallback::LocaleFallbackProvider; |
28 | /// |
29 | /// # let provider = icu_provider_blob::BlobDataProvider::try_new_from_static_blob(include_bytes!("../../tests/data/blob.postcard" )).unwrap(); |
30 | /// # let provider = provider.as_deserializing(); |
31 | /// |
32 | /// let req = DataRequest { |
33 | /// locale: &locale!("ja-JP" ).into(), |
34 | /// metadata: Default::default(), |
35 | /// }; |
36 | /// |
37 | /// // The provider does not have data for "ja-JP": |
38 | /// DataProvider::<HelloWorldV1Marker>::load(&provider, req).expect_err("No fallback" ); |
39 | /// |
40 | /// // But if we wrap the provider in a fallback provider... |
41 | /// let provider = LocaleFallbackProvider::try_new_unstable(provider) |
42 | /// .expect("Fallback data present" ); |
43 | /// |
44 | /// // ...then we can load "ja-JP" based on "ja" data |
45 | /// let response = |
46 | /// DataProvider::<HelloWorldV1Marker>::load(&provider, req).expect("successful with vertical fallback" ); |
47 | /// |
48 | /// assert_eq!( |
49 | /// response.metadata.locale.unwrap(), |
50 | /// locale!("ja" ).into(), |
51 | /// ); |
52 | /// assert_eq!( |
53 | /// response.payload.unwrap().get().message, |
54 | /// "こんにちは世界" , |
55 | /// ); |
56 | /// ``` |
57 | #[derive (Clone, Debug)] |
58 | pub struct LocaleFallbackProvider<P> { |
59 | inner: P, |
60 | fallbacker: LocaleFallbacker, |
61 | } |
62 | |
63 | impl<P> LocaleFallbackProvider<P> |
64 | where |
65 | P: DataProvider<LocaleFallbackLikelySubtagsV1Marker> |
66 | + DataProvider<LocaleFallbackParentsV1Marker> |
67 | + DataProvider<CollationFallbackSupplementV1Marker>, |
68 | { |
69 | /// Create a [`LocaleFallbackProvider`] by wrapping another data provider and then loading |
70 | /// fallback data from it. |
71 | /// |
72 | /// If the data provider being wrapped does not contain fallback data, use |
73 | /// [`LocaleFallbackProvider::new_with_fallbacker`]. |
74 | pub fn try_new_unstable(provider: P) -> Result<Self, DataError> { |
75 | let fallbacker: LocaleFallbacker = LocaleFallbacker::try_new_unstable(&provider)?; |
76 | Ok(Self { |
77 | inner: provider, |
78 | fallbacker, |
79 | }) |
80 | } |
81 | } |
82 | |
83 | impl<P> LocaleFallbackProvider<P> |
84 | where |
85 | P: AnyProvider, |
86 | { |
87 | /// Create a [`LocaleFallbackProvider`] by wrapping another data provider and then loading |
88 | /// fallback data from it. |
89 | /// |
90 | /// If the data provider being wrapped does not contain fallback data, use |
91 | /// [`LocaleFallbackProvider::new_with_fallbacker`]. |
92 | pub fn try_new_with_any_provider(provider: P) -> Result<Self, DataError> { |
93 | let fallbacker: LocaleFallbacker = LocaleFallbacker::try_new_with_any_provider(&provider)?; |
94 | Ok(Self { |
95 | inner: provider, |
96 | fallbacker, |
97 | }) |
98 | } |
99 | } |
100 | |
101 | #[cfg (feature = "serde" )] |
102 | impl<P> LocaleFallbackProvider<P> |
103 | where |
104 | P: BufferProvider, |
105 | { |
106 | /// Create a [`LocaleFallbackProvider`] by wrapping another data provider and then loading |
107 | /// fallback data from it. |
108 | /// |
109 | /// If the data provider being wrapped does not contain fallback data, use |
110 | /// [`LocaleFallbackProvider::new_with_fallbacker`]. |
111 | pub fn try_new_with_buffer_provider(provider: P) -> Result<Self, DataError> { |
112 | let fallbacker = LocaleFallbacker::try_new_with_buffer_provider(&provider)?; |
113 | Ok(Self { |
114 | inner: provider, |
115 | fallbacker, |
116 | }) |
117 | } |
118 | } |
119 | |
120 | impl<P> LocaleFallbackProvider<P> { |
121 | /// Wrap a provider with an arbitrary fallback engine. |
122 | /// |
123 | /// This relaxes the requirement that the wrapped provider contains its own fallback data. |
124 | /// |
125 | /// # Examples |
126 | /// |
127 | /// ``` |
128 | /// use icu_locid::locale; |
129 | /// use icu_locid_transform::LocaleFallbacker; |
130 | /// use icu_provider::hello_world::*; |
131 | /// use icu_provider::prelude::*; |
132 | /// use icu_provider_adapters::fallback::LocaleFallbackProvider; |
133 | /// |
134 | /// let provider = HelloWorldProvider; |
135 | /// |
136 | /// let req = DataRequest { |
137 | /// locale: &locale!("de-CH" ).into(), |
138 | /// metadata: Default::default(), |
139 | /// }; |
140 | /// |
141 | /// // There is no "de-CH" data in the `HelloWorldProvider` |
142 | /// DataProvider::<HelloWorldV1Marker>::load(&provider, req) |
143 | /// .expect_err("No data for de-CH" ); |
144 | /// |
145 | /// // `HelloWorldProvider` does not contain fallback data, |
146 | /// // but we can construct a fallbacker with `icu_locid_transform`'s |
147 | /// // compiled data. |
148 | /// let provider = LocaleFallbackProvider::new_with_fallbacker( |
149 | /// provider, |
150 | /// LocaleFallbacker::new().static_to_owned(), |
151 | /// ); |
152 | /// |
153 | /// // Now we can load the "de-CH" data via fallback to "de". |
154 | /// let german_hello_world: DataPayload<HelloWorldV1Marker> = provider |
155 | /// .load(req) |
156 | /// .expect("Loading should succeed" ) |
157 | /// .take_payload() |
158 | /// .expect("Data should be present" ); |
159 | /// |
160 | /// assert_eq!("Hallo Welt" , german_hello_world.get().message); |
161 | /// ``` |
162 | pub fn new_with_fallbacker(provider: P, fallbacker: LocaleFallbacker) -> Self { |
163 | Self { |
164 | inner: provider, |
165 | fallbacker, |
166 | } |
167 | } |
168 | |
169 | /// Returns a reference to the inner provider, bypassing fallback. |
170 | pub fn inner(&self) -> &P { |
171 | &self.inner |
172 | } |
173 | |
174 | /// Returns a mutable reference to the inner provider. |
175 | pub fn inner_mut(&mut self) -> &mut P { |
176 | &mut self.inner |
177 | } |
178 | |
179 | /// Returns ownership of the inner provider to the caller. |
180 | pub fn into_inner(self) -> P { |
181 | self.inner |
182 | } |
183 | |
184 | /// Run the fallback algorithm with the data request using the inner data provider. |
185 | /// Internal function; external clients should use one of the trait impls below. |
186 | /// |
187 | /// Function arguments: |
188 | /// |
189 | /// - F1 should perform a data load for a single DataRequest and return the result of it |
190 | /// - F2 should map from the provider-specific response type to DataResponseMetadata |
191 | fn run_fallback<F1, F2, R>( |
192 | &self, |
193 | key: DataKey, |
194 | mut base_req: DataRequest, |
195 | mut f1: F1, |
196 | mut f2: F2, |
197 | ) -> Result<R, DataError> |
198 | where |
199 | F1: FnMut(DataRequest) -> Result<R, DataError>, |
200 | F2: FnMut(&mut R) -> &mut DataResponseMetadata, |
201 | { |
202 | if key.metadata().singleton { |
203 | return f1(base_req); |
204 | } |
205 | let mut fallback_iterator = self |
206 | .fallbacker |
207 | .for_config(key.fallback_config()) |
208 | .fallback_for(base_req.locale.clone()); |
209 | let base_silent = core::mem::replace(&mut base_req.metadata.silent, true); |
210 | loop { |
211 | let result = f1(DataRequest { |
212 | locale: fallback_iterator.get(), |
213 | metadata: base_req.metadata, |
214 | }); |
215 | if !result_is_err_missing_locale(&result) { |
216 | return result |
217 | .map(|mut res| { |
218 | f2(&mut res).locale = Some(fallback_iterator.take()); |
219 | res |
220 | }) |
221 | // Log the original request rather than the fallback request |
222 | .map_err(|e| { |
223 | base_req.metadata.silent = base_silent; |
224 | e.with_req(key, base_req) |
225 | }); |
226 | } |
227 | // If we just checked und, break out of the loop. |
228 | if fallback_iterator.get().is_und() { |
229 | break; |
230 | } |
231 | fallback_iterator.step(); |
232 | } |
233 | base_req.metadata.silent = base_silent; |
234 | Err(DataErrorKind::MissingLocale.with_req(key, base_req)) |
235 | } |
236 | } |
237 | |
238 | impl<P> AnyProvider for LocaleFallbackProvider<P> |
239 | where |
240 | P: AnyProvider, |
241 | { |
242 | fn load_any(&self, key: DataKey, base_req: DataRequest) -> Result<AnyResponse, DataError> { |
243 | self.run_fallback( |
244 | key, |
245 | base_req, |
246 | |req| self.inner.load_any(key, req), |
247 | |res: &mut AnyResponse| &mut res.metadata, |
248 | ) |
249 | } |
250 | } |
251 | |
252 | impl<P> BufferProvider for LocaleFallbackProvider<P> |
253 | where |
254 | P: BufferProvider, |
255 | { |
256 | fn load_buffer( |
257 | &self, |
258 | key: DataKey, |
259 | base_req: DataRequest, |
260 | ) -> Result<DataResponse<BufferMarker>, DataError> { |
261 | self.run_fallback( |
262 | key, |
263 | base_req, |
264 | |req| self.inner.load_buffer(key, req), |
265 | |res: &mut DataResponse| &mut res.metadata, |
266 | ) |
267 | } |
268 | } |
269 | |
270 | impl<P, M> DynamicDataProvider<M> for LocaleFallbackProvider<P> |
271 | where |
272 | P: DynamicDataProvider<M>, |
273 | M: DataMarker, |
274 | { |
275 | fn load_data(&self, key: DataKey, base_req: DataRequest) -> Result<DataResponse<M>, DataError> { |
276 | self.run_fallback( |
277 | key, |
278 | base_req, |
279 | |req| self.inner.load_data(key, req), |
280 | |res: &mut DataResponse| &mut res.metadata, |
281 | ) |
282 | } |
283 | } |
284 | |
285 | impl<P, M> DataProvider<M> for LocaleFallbackProvider<P> |
286 | where |
287 | P: DataProvider<M>, |
288 | M: KeyedDataMarker, |
289 | { |
290 | fn load(&self, base_req: DataRequest) -> Result<DataResponse<M>, DataError> { |
291 | self.run_fallback( |
292 | M::KEY, |
293 | base_req, |
294 | |req| self.inner.load(req), |
295 | |res: &mut DataResponse| &mut res.metadata, |
296 | ) |
297 | } |
298 | } |
299 | |