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}