Skip to main content

icu_provider_adapters/fallback/
mod.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
5//! A data provider wrapper that performs locale fallback.
6
7#[doc(no_inline)]
8pub use icu_locale::LocaleFallbacker;
9use icu_provider::prelude::*;
10use icu_provider::DryDataProvider;
11use 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)]
58pub struct LocaleFallbackProvider<P> {
59    inner: P,
60    fallbacker: LocaleFallbacker,
61}
62
63impl<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    /// ```
110    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.
118    pub fn inner(&self) -> &P {
119        &self.inner
120    }
121
122    /// Returns a mutable reference to the inner provider.
123    pub fn inner_mut(&mut self) -> &mut P {
124        &mut self.inner
125    }
126
127    /// Returns ownership of the inner provider to the caller.
128    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
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_unknown() {
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
191impl<P, M> DynamicDataProvider<M> for LocaleFallbackProvider<P>
192where
193    P: DynamicDataProvider<M>,
194    M: DynamicDataMarker,
195{
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
210impl<P, M> DynamicDryDataProvider<M> for LocaleFallbackProvider<P>
211where
212    P: DynamicDryDataProvider<M>,
213    M: DynamicDataMarker,
214{
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
229impl<P, M> DataProvider<M> for LocaleFallbackProvider<P>
230where
231    P: DataProvider<M>,
232    M: DataMarker,
233{
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
244impl<P, M> DryDataProvider<M> for LocaleFallbackProvider<P>
245where
246    P: DryDataProvider<M>,
247    M: DataMarker,
248{
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]
255fn 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}