Skip to main content

fluent_templates/loader/
arc_loader.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fs::read_dir;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use crate::languages::negotiate_languages;
8use crate::FluentBundle;
9use fluent_bundle::{FluentResource, FluentValue};
10
11use crate::error::{LoaderError, LookupError};
12
13pub use unic_langid::LanguageIdentifier;
14
15type Customize = Option<Box<dyn FnMut(&mut FluentBundle<Arc<FluentResource>>)>>;
16
17/// A builder pattern struct for constructing `ArcLoader`s.
18pub struct ArcLoaderBuilder<'a, 'b> {
19    location: &'a Path,
20    fallback: LanguageIdentifier,
21    shared: Option<&'b [PathBuf]>,
22    customize: Customize,
23}
24
25impl<'a, 'b> ArcLoaderBuilder<'a, 'b> {
26    /// Adds Fluent resources that are shared across all localizations.
27    pub fn shared_resources<'b2>(
28        self,
29        shared: Option<&'b2 [PathBuf]>,
30    ) -> ArcLoaderBuilder<'a, 'b2> {
31        ArcLoaderBuilder {
32            location: self.location,
33            fallback: self.fallback,
34            shared,
35            customize: self.customize,
36        }
37    }
38
39    /// Allows you to customise each `FluentBundle`.
40    pub fn customize(
41        mut self,
42        customize: impl FnMut(&mut FluentBundle<Arc<FluentResource>>) + 'static,
43    ) -> Self {
44        self.customize = Some(Box::new(customize));
45        self
46    }
47
48    /// Constructs an `ArcLoader` from the settings provided.
49    pub fn build(mut self) -> Result<ArcLoader, LoaderError> {
50        let mut resources: HashMap<LanguageIdentifier, Vec<Arc<FluentResource>>> = HashMap::new();
51        let entries = read_dir(self.location).map_err(|source| LoaderError::Fs {
52            path: self.location.to_owned(),
53            source,
54        })?;
55
56        for entry in entries {
57            let entry = entry.map_err(|source| LoaderError::Fs {
58                path: self.location.to_owned(),
59                source,
60            })?;
61            let file_type = entry.file_type().map_err(|source| LoaderError::Fs {
62                path: entry.path(),
63                source,
64            })?;
65            if file_type.is_dir() {
66                if let Ok(lang) = entry.file_name().into_string() {
67                    let lang = lang.parse::<LanguageIdentifier>()?;
68                    let lang_resources = crate::fs::read_from_dir(entry.path())?
69                        .into_iter()
70                        .map(Arc::new)
71                        .collect::<Vec<_>>();
72                    resources.entry(lang).or_default().extend(lang_resources);
73                }
74            } else if file_type.is_file() && entry.path().extension().is_some_and(|e| e == "ftl") {
75                if let Some(stem) = entry.path().file_stem().and_then(|s| s.to_str()) {
76                    if let Ok(lang) = stem.parse::<LanguageIdentifier>() {
77                        let res = Arc::new(crate::fs::read_from_file(entry.path())?);
78                        resources.entry(lang).or_default().push(res);
79                    }
80                }
81            }
82        }
83
84        let mut bundles = HashMap::new();
85        for (lang, v) in resources.iter() {
86            let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
87
88            for shared_resource in self.shared.unwrap_or(&[]) {
89                bundle
90                    .add_resource(Arc::new(crate::fs::read_from_file(shared_resource)?))
91                    .map_err(|errors| LoaderError::FluentBundle { errors })?;
92            }
93
94            for res in v {
95                bundle
96                    .add_resource(res.clone())
97                    .map_err(|errors| LoaderError::FluentBundle { errors })?;
98            }
99
100            if let Some(customize) = self.customize.as_mut() {
101                (customize)(&mut bundle);
102            }
103
104            bundles.insert(lang.clone(), bundle);
105        }
106
107        let fallbacks = super::build_fallbacks(&resources.keys().cloned().collect::<Vec<_>>());
108
109        Ok(ArcLoader {
110            bundles,
111            fallbacks,
112            fallback: self.fallback,
113        })
114    }
115}
116
117/// A loader that uses `Arc<FluentResource>` as its backing storage. This is
118/// mainly useful for when you need to load fluent at run time. You can
119/// configure the initialisation with `ArcLoaderBuilder`.
120/// ```no_run
121/// use fluent_templates::ArcLoader;
122///
123/// let loader = ArcLoader::builder("locales/", unic_langid::langid!("en-US"))
124///     .shared_resources(Some(&["locales/core.ftl".into()]))
125///     .customize(|bundle| bundle.set_use_isolating(false))
126///     .build()
127///     .unwrap();
128/// ```
129pub struct ArcLoader {
130    bundles: HashMap<LanguageIdentifier, FluentBundle<Arc<FluentResource>>>,
131    fallback: LanguageIdentifier,
132    fallbacks: HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
133}
134
135impl super::Loader for ArcLoader {
136    // Traverse the fallback chain,
137    fn lookup_complete(
138        &self,
139        lang: &LanguageIdentifier,
140        text_id: &str,
141        args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
142    ) -> String {
143        for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
144            if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
145                return val;
146            }
147        }
148        if *lang != self.fallback {
149            if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
150                return val;
151            }
152        }
153        format!("Unknown localization key: {text_id:?}")
154    }
155
156    // Traverse the fallback chain,
157    fn try_lookup_complete(
158        &self,
159        lang: &LanguageIdentifier,
160        text_id: &str,
161        args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
162    ) -> Option<String> {
163        for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
164            if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
165                return Some(val);
166            }
167        }
168        if *lang != self.fallback {
169            if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
170                return Some(val);
171            }
172        }
173        None
174    }
175
176    fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
177        Box::new(self.fallbacks.keys())
178    }
179}
180
181impl ArcLoader {
182    /// Creates a new `ArcLoaderBuilder`
183    pub fn builder<'a, P: AsRef<Path> + ?Sized>(
184        location: &'a P,
185        fallback: LanguageIdentifier,
186    ) -> ArcLoaderBuilder<'a, 'static> {
187        ArcLoaderBuilder {
188            location: location.as_ref(),
189            fallback,
190            shared: None,
191            customize: None,
192        }
193    }
194
195    /// Convenience function to look up a string for a single language
196    pub fn lookup_single_language<T: AsRef<str>>(
197        &self,
198        lang: &LanguageIdentifier,
199        text_id: &str,
200        args: Option<&HashMap<T, FluentValue>>,
201    ) -> Result<String, LookupError> {
202        super::shared::lookup_single_language(&self.bundles, lang, text_id, args)
203    }
204
205    /// Convenience function to look up a string without falling back to the
206    /// default fallback language
207    pub fn lookup_no_default_fallback<S: AsRef<str>>(
208        &self,
209        lang: &LanguageIdentifier,
210        text_id: &str,
211        args: Option<&HashMap<S, FluentValue>>,
212    ) -> Option<String> {
213        super::shared::lookup_no_default_fallback(
214            &self.bundles,
215            &self.fallbacks,
216            lang,
217            text_id,
218            args,
219        )
220    }
221
222    /// Return the fallback language
223    pub fn fallback(&self) -> &LanguageIdentifier {
224        &self.fallback
225    }
226}