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 #[doc(no_inline)]
8 pub use icu_locale::LocaleFallbacker;
9 use icu_provider::prelude::*;
10 use icu_provider::DryDataProvider;
11 use icu_provider::DynamicDryDataProvider;
12
13 /// A data provider wrapper that performs locale fallback. This enables arbitrary locales to be
14 /// handled at runtime.
15 ///
16 /// # Examples
17 ///
18 /// ```
19 /// use icu_locale::langid;
20 /// use icu_provider::hello_world::*;
21 /// use icu_provider::prelude::*;
22 /// use icu_provider_adapters::fallback::LocaleFallbackProvider;
23 ///
24 /// let provider = HelloWorldProvider;
25 ///
26 /// let id = DataIdentifierCow::from_locale(langid!("ja-JP").into());
27 ///
28 /// // The provider does not have data for "ja-JP":
29 /// DataProvider::<HelloWorldV1>::load(
30 /// &provider,
31 /// DataRequest {
32 /// id: id.as_borrowed(),
33 /// ..Default::default()
34 /// },
35 /// )
36 /// .expect_err("No fallback");
37 ///
38 /// // But if we wrap the provider in a fallback provider...
39 /// let provider = LocaleFallbackProvider::new(
40 /// provider,
41 /// icu_locale::LocaleFallbacker::new().static_to_owned(),
42 /// );
43 ///
44 /// // ...then we can load "ja-JP" based on "ja" data
45 /// let response = DataProvider::<HelloWorldV1>::load(
46 /// &provider,
47 /// DataRequest {
48 /// id: id.as_borrowed(),
49 /// ..Default::default()
50 /// },
51 /// )
52 /// .expect("successful with vertical fallback");
53 ///
54 /// assert_eq!(response.metadata.locale.unwrap(), langid!("ja").into(),);
55 /// assert_eq!(response.payload.get().message, "こんにちは世界",);
56 /// ```
57 #[derive(Clone, Debug)]
58 pub struct LocaleFallbackProvider<P> {
59 inner: P,
60 fallbacker: LocaleFallbacker,
61 }
62
63 impl<P> LocaleFallbackProvider<P> {
64 /// Wraps a provider with a provider performing fallback under the given fallbacker.
65 ///
66 /// If the underlying provider contains deduplicated data, it is important to use the
67 /// same fallback data that `ExportDriver` used.
68 ///
69 /// # Examples
70 ///
71 /// ```
72 /// use icu_locale::langid;
73 /// use icu_locale::LocaleFallbacker;
74 /// use icu_provider::hello_world::*;
75 /// use icu_provider::prelude::*;
76 /// use icu_provider_adapters::fallback::LocaleFallbackProvider;
77 ///
78 /// let provider = HelloWorldProvider;
79 ///
80 /// let id = DataIdentifierCow::from_locale(langid!("de-CH").into());
81 ///
82 /// // There is no "de-CH" data in the `HelloWorldProvider`
83 /// DataProvider::<HelloWorldV1>::load(
84 /// &provider,
85 /// DataRequest {
86 /// id: id.as_borrowed(),
87 /// ..Default::default()
88 /// },
89 /// )
90 /// .expect_err("No data for de-CH");
91 ///
92 /// // `HelloWorldProvider` does not contain fallback data,
93 /// // but we can construct a fallbacker with `icu_locale`'s
94 /// // compiled data.
95 /// let provider = LocaleFallbackProvider::new(
96 /// provider,
97 /// LocaleFallbacker::new().static_to_owned(),
98 /// );
99 ///
100 /// // Now we can load the "de-CH" data via fallback to "de".
101 /// let german_hello_world: DataResponse<HelloWorldV1> = provider
102 /// .load(DataRequest {
103 /// id: id.as_borrowed(),
104 /// ..Default::default()
105 /// })
106 /// .expect("Loading should succeed");
107 ///
108 /// assert_eq!("Hallo Welt", german_hello_world.payload.get().message);
109 /// ```
new(provider: P, fallbacker: LocaleFallbacker) -> Self110 pub fn new(provider: P, fallbacker: LocaleFallbacker) -> Self {
111 Self {
112 inner: provider,
113 fallbacker,
114 }
115 }
116
117 /// Returns a reference to the inner provider, bypassing fallback.
inner(&self) -> &P118 pub fn inner(&self) -> &P {
119 &self.inner
120 }
121
122 /// Returns a mutable reference to the inner provider.
inner_mut(&mut self) -> &mut P123 pub fn inner_mut(&mut self) -> &mut P {
124 &mut self.inner
125 }
126
127 /// Returns ownership of the inner provider to the caller.
into_inner(self) -> P128 pub fn into_inner(self) -> P {
129 self.inner
130 }
131
132 /// Run the fallback algorithm with the data request using the inner data provider.
133 /// Internal function; external clients should use one of the trait impls below.
134 ///
135 /// Function arguments:
136 ///
137 /// - F1 should perform a data load for a single DataRequest and return the result of it
138 /// - F2 should map from the provider-specific response type to DataResponseMetadata
run_fallback<F1, F2, R>( &self, marker: DataMarkerInfo, mut base_req: DataRequest, mut f1: F1, mut f2: F2, ) -> Result<R, DataError> where F1: FnMut(DataRequest) -> Result<R, DataError>, F2: FnMut(&mut R) -> &mut DataResponseMetadata,139 fn run_fallback<F1, F2, R>(
140 &self,
141 marker: DataMarkerInfo,
142 mut base_req: DataRequest,
143 mut f1: F1,
144 mut f2: F2,
145 ) -> Result<R, DataError>
146 where
147 F1: FnMut(DataRequest) -> Result<R, DataError>,
148 F2: FnMut(&mut R) -> &mut DataResponseMetadata,
149 {
150 if marker.is_singleton {
151 return f1(base_req);
152 }
153 let mut fallback_iterator = self
154 .fallbacker
155 .for_config(marker.fallback_config)
156 .fallback_for(*base_req.id.locale);
157 let base_silent = core::mem::replace(&mut base_req.metadata.silent, true);
158 loop {
159 let result = f1(DataRequest {
160 id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
161 base_req.id.marker_attributes,
162 fallback_iterator.get(),
163 ),
164 ..base_req
165 });
166
167 match result.allow_identifier_not_found() {
168 Ok(Some(mut result)) => {
169 f2(&mut result).locale = Some(fallback_iterator.take());
170 return Ok(result);
171 }
172 Ok(None) => {
173 // If we just checked und, break out of the loop.
174 if fallback_iterator.get().is_default() {
175 break;
176 }
177 fallback_iterator.step();
178 }
179 Err(e) => {
180 // Log the original request rather than the fallback request
181 base_req.metadata.silent = base_silent;
182 return Err(e.with_req(marker, base_req));
183 }
184 };
185 }
186 base_req.metadata.silent = base_silent;
187 Err(DataErrorKind::IdentifierNotFound.with_req(marker, base_req))
188 }
189 }
190
191 impl<P, M> DynamicDataProvider<M> for LocaleFallbackProvider<P>
192 where
193 P: DynamicDataProvider<M>,
194 M: DynamicDataMarker,
195 {
load_data( &self, marker: DataMarkerInfo, req: DataRequest, ) -> Result<DataResponse<M>, DataError>196 fn load_data(
197 &self,
198 marker: DataMarkerInfo,
199 req: DataRequest,
200 ) -> Result<DataResponse<M>, DataError> {
201 self.run_fallback(
202 marker,
203 req,
204 |req| self.inner.load_data(marker, req),
205 |res| &mut res.metadata,
206 )
207 }
208 }
209
210 impl<P, M> DynamicDryDataProvider<M> for LocaleFallbackProvider<P>
211 where
212 P: DynamicDryDataProvider<M>,
213 M: DynamicDataMarker,
214 {
dry_load_data( &self, marker: DataMarkerInfo, req: DataRequest, ) -> Result<DataResponseMetadata, DataError>215 fn dry_load_data(
216 &self,
217 marker: DataMarkerInfo,
218 req: DataRequest,
219 ) -> Result<DataResponseMetadata, DataError> {
220 self.run_fallback(
221 marker,
222 req,
223 |req| self.inner.dry_load_data(marker, req),
224 |m| m,
225 )
226 }
227 }
228
229 impl<P, M> DataProvider<M> for LocaleFallbackProvider<P>
230 where
231 P: DataProvider<M>,
232 M: DataMarker,
233 {
load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError>234 fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError> {
235 self.run_fallback(
236 M::INFO,
237 req,
238 |req| self.inner.load(req),
239 |res| &mut res.metadata,
240 )
241 }
242 }
243
244 impl<P, M> DryDataProvider<M> for LocaleFallbackProvider<P>
245 where
246 P: DryDataProvider<M>,
247 M: DataMarker,
248 {
dry_load(&self, req: DataRequest) -> Result<DataResponseMetadata, DataError>249 fn dry_load(&self, req: DataRequest) -> Result<DataResponseMetadata, DataError> {
250 self.run_fallback(M::INFO, req, |req| self.inner.dry_load(req), |m| m)
251 }
252 }
253
254 #[test]
dry_test()255 fn dry_test() {
256 use icu_provider::hello_world::*;
257
258 struct TestProvider;
259 impl DataProvider<HelloWorldV1> for TestProvider {
260 fn load(&self, _: DataRequest) -> Result<DataResponse<HelloWorldV1>, DataError> {
261 panic!("pretend this is super expensive")
262 }
263 }
264
265 impl DryDataProvider<HelloWorldV1> for TestProvider {
266 fn dry_load(&self, req: DataRequest) -> Result<DataResponseMetadata, DataError> {
267 // We support all languages except English, and no regional variants. This is cheap to check.
268 if req.id.locale.region.is_some() || req.id.locale.language.as_str() == "en" {
269 Err(DataErrorKind::IdentifierNotFound.into_error())
270 } else {
271 Ok(Default::default())
272 }
273 }
274 }
275
276 let provider =
277 LocaleFallbackProvider::new(TestProvider, LocaleFallbacker::new().static_to_owned());
278
279 assert_eq!(
280 provider
281 .dry_load(DataRequest {
282 id: DataIdentifierBorrowed::for_locale(&"de-CH".parse().unwrap()),
283 ..Default::default()
284 })
285 .unwrap()
286 .locale,
287 "de".parse::<DataLocale>().ok()
288 );
289
290 assert_eq!(
291 provider
292 .dry_load(DataRequest {
293 id: DataIdentifierBorrowed::for_locale(&"en-GB".parse().unwrap()),
294 ..Default::default()
295 })
296 .unwrap()
297 .locale,
298 Some(DataLocale::default())
299 );
300 }
301