fluent_templates/loader/
multi_loader.rs

1use crate::Loader;
2use fluent_bundle::FluentValue;
3use std::borrow::Cow;
4use std::collections::{HashMap, VecDeque};
5
6pub use unic_langid::LanguageIdentifier;
7
8/// A loader comprised of other loaders.
9///
10/// This loader allows for loaders with multiple sources to be used
11/// from a single one, instead of a multiple of them.
12///
13/// The idea behind this loader is to allow for the scenario where you depend
14/// on crates that have their own loader (think of protocol crates which
15/// are dependencies of a frontend -> the frontend needs to know each of
16/// the protocol's messages and be able to display them). Using a multiloader
17/// allows you to query multiple localization sources from one single source.
18///
19/// Note that a [`MultiLoader`] is most useful where each of your fluent modules
20/// is specially namespaced to avoid name collisions.
21///
22/// # Usage
23/// ```rust
24/// use fluent_templates::{ArcLoader, StaticLoader, MultiLoader, Loader};
25/// use unic_langid::{LanguageIdentifier, langid};
26///
27/// const US_ENGLISH: LanguageIdentifier = langid!("en-US");
28/// const CHINESE: LanguageIdentifier = langid!("zh-CN");
29///
30/// fluent_templates::static_loader! {
31///     static LOCALES = {
32///         locales: "./tests/locales",
33///         fallback_language: "en-US",
34///         // Removes unicode isolating marks around arguments, you typically
35///         // should only set to false when testing.
36///         customise: |bundle| bundle.set_use_isolating(false),
37///     };
38/// }
39///
40/// fn main() {
41///     let cn_loader = ArcLoader::builder("./tests/locales", CHINESE)
42///         .customize(|bundle| bundle.set_use_isolating(false))
43///         .build()
44///         .unwrap();
45///
46///     let mut multiloader = MultiLoader::from_iter([
47///         Box::new(&*LOCALES) as Box<dyn Loader>,
48///     ]);
49///     multiloader.push_back(Box::new(cn_loader) as Box<dyn Loader>);
50///     assert_eq!("Hello World!", multiloader.lookup(&US_ENGLISH, "hello-world"));
51///     assert_eq!("儿", multiloader.lookup(&CHINESE, "exists"));
52/// }
53/// ```
54///
55/// # Order of search
56/// The one that is inserted first is also the one searched first.
57pub struct MultiLoader<T = Box<dyn Loader + Sync + Send>> {
58    loaders: VecDeque<T>,
59}
60
61impl<T> MultiLoader<T> {
62    /// Creates a [`MultiLoader`] without any loaders.
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    /// Creates a [`MultiLoader`] from an iterator of loaders.
68    pub fn from_iter(iter: impl IntoIterator<Item = T>) -> Self {
69        Self {
70            loaders: iter.into_iter().collect(),
71        }
72    }
73
74    /// Pushes a loader in front of all the others in terms of precedence.
75    pub fn push_front(&mut self, loader: T) {
76        self.loaders.push_front(loader);
77    }
78
79    /// Pushes a loader at the back in terms of precedence.
80    pub fn push_back(&mut self, loader: T) {
81        self.loaders.push_back(loader);
82    }
83
84    /// Pushes a loader at the back in terms of precedence.
85    pub fn remove(&mut self, idx: usize) -> Option<T> {
86        self.loaders.remove(idx)
87    }
88}
89
90impl<T> Default for MultiLoader<T> {
91    fn default() -> Self {
92        Self {
93            loaders: VecDeque::default(),
94        }
95    }
96}
97
98impl<T: Loader> crate::Loader for MultiLoader<T> {
99    fn lookup_complete(
100        &self,
101        lang: &unic_langid::LanguageIdentifier,
102        text_id: &str,
103        args: Option<&std::collections::HashMap<Cow<'static, str>, fluent_bundle::FluentValue>>,
104    ) -> String {
105        for loader in self.loaders.iter() {
106            if let Some(text) = loader.try_lookup_complete(lang, text_id, args) {
107                return text;
108            }
109        }
110        format!("Unknown localization key: {text_id:?}")
111    }
112
113    fn try_lookup_complete(
114        &self,
115        lang: &LanguageIdentifier,
116        text_id: &str,
117        args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
118    ) -> Option<String> {
119        for loader in self.loaders.iter() {
120            if let Some(text) = loader.try_lookup_complete(lang, text_id, args) {
121                return Some(text);
122            }
123        }
124        None
125    }
126
127    fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
128        Box::new(self.loaders.iter().flat_map(|loader| loader.locales()))
129    }
130}