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