fluent_template_helper/loader/
arc_loader.rs

1use std::collections::HashMap;
2use std::fs::read_dir;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use fluent_bundle::concurrent::FluentBundle;
7use fluent_bundle::{FluentResource, FluentValue};
8
9pub use unic_langid::{langid, langids, LanguageIdentifier};
10
11pub struct ArcLoaderBuilder<'a, 'b> {
12    location: &'a Path,
13    fallback: LanguageIdentifier,
14    shared: Option<&'b [PathBuf]>,
15    customize: Option<fn(&mut FluentBundle<Arc<FluentResource>>)>,
16}
17
18impl<'a, 'b> ArcLoaderBuilder<'a, 'b> {
19
20    /// Adds Fluent resources that are shared across all localizations.
21    pub fn shared_resources(mut self, shared: Option<&'b [PathBuf]>) -> Self {
22        self.shared = shared;
23        self
24    }
25
26    /// Allows you to customise each `FluentBundle`.
27    pub fn customize(mut self, customize: fn(&mut FluentBundle<Arc<FluentResource>>)) -> Self {
28        self.customize = Some(customize);
29        self
30    }
31
32    /// Constructs an `ArcLoader` from the settings provided.
33    pub fn build(self) -> Result<ArcLoader, Box<dyn std::error::Error>> {
34        let mut resources = HashMap::new();
35
36        for entry in read_dir(self.location)? {
37            let entry = entry?;
38            if entry.file_type()?.is_dir() {
39                if let Ok(lang) = entry.file_name().into_string() {
40                    let lang_resources = super::read_from_dir(entry.path())?
41                        .into_iter()
42                        .map(Arc::new)
43                        .collect::<Vec<_>>();
44                    resources.insert(lang.parse::<LanguageIdentifier>()?, lang_resources);
45                }
46            }
47        }
48
49        let mut bundles = HashMap::new();
50        for (lang, v) in resources.iter() {
51            let mut bundle = FluentBundle::new(&[lang.clone()][..]);
52
53            for shared_resource in self.shared.as_deref().unwrap_or(&[]) {
54                bundle.add_resource(Arc::new(super::read_from_file(shared_resource)?))
55                    .expect("Failed to add core FTL resources to the bundle.");
56            }
57
58            for res in v {
59                bundle
60                    .add_resource(res.clone())
61                    .expect("Failed to add FTL resources to the bundle.");
62            }
63
64            if let Some(customize) = self.customize {
65                (customize)(&mut bundle);
66            }
67
68            bundles.insert(lang.clone(), bundle);
69        }
70
71        let fallbacks = super::build_fallbacks(&*resources.keys().cloned().collect::<Vec<_>>());
72
73        Ok(ArcLoader {
74            resources,
75            bundles,
76            fallbacks,
77            fallback: self.fallback,
78        })
79    }
80}
81
82/// A loader that uses `Arc<FluentResource>` as its backing storage. This is
83/// mainly useful for when you need to load fluent at run time. You can
84/// configure the initialisation with `ArcLoaderBuilder`.
85/// ```no_run
86/// use fluent_template_helper::{ArcLoader, FluentHelper};
87///
88/// let mut handlebars = handlebars::Handlebars::new();
89/// let loader = ArcLoader::new("locales/", unic_langid::langid!("en-US"))
90///     .shared_resources(Some(&["locales/core.ftl".into()]))
91///     .customize(|bundle| bundle.set_use_isolating(false))
92///     .build()
93///     .unwrap();
94/// handlebars.register_helper("fluent", Box::new(FluentHelper::new(loader)));
95/// ```
96pub struct ArcLoader {
97    bundles: HashMap<LanguageIdentifier, FluentBundle<Arc<FluentResource>>>,
98    fallback: LanguageIdentifier,
99    fallbacks: HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
100    resources: HashMap<LanguageIdentifier, Vec<Arc<FluentResource>>>,
101}
102
103impl super::Loader for ArcLoader {
104    // Traverse the fallback chain,
105    fn lookup(
106        &self,
107        lang: &LanguageIdentifier,
108        text_id: &str,
109        args: Option<&HashMap<&str, FluentValue>>,
110    ) -> String {
111        for l in self.fallbacks.get(lang).expect("language not found") {
112            if let Some(val) = self.lookup_single_language(l, text_id, args) {
113                return val;
114            }
115        }
116        if *lang != self.fallback {
117            if let Some(val) = self.lookup_single_language(&self.fallback, text_id, args) {
118                return val;
119            }
120        }
121        format!("Unknown localization {}", text_id)
122    }
123}
124
125impl ArcLoader {
126    /// Creates a new `ArcLoaderBuilder`
127    pub fn new<P: AsRef<Path> + ?Sized>(location: &P, fallback: LanguageIdentifier) -> ArcLoaderBuilder {
128        ArcLoaderBuilder { location: location.as_ref(), fallback, shared: None, customize: None }
129    }
130
131    /// Returns a Vec over the locales that were detected.
132    pub fn locales(&self) -> Vec<LanguageIdentifier> {
133        self.resources.keys().cloned().collect()
134    }
135
136    /// Convenience function to look up a string for a single language
137    pub fn lookup_single_language(
138        &self,
139        lang: &LanguageIdentifier,
140        text_id: &str,
141        args: Option<&HashMap<&str, FluentValue>>,
142    ) -> Option<String> {
143        if let Some(bundle) = self.bundles.get(lang) {
144            if let Some(message) = bundle.get_message(text_id).and_then(|m| m.value) {
145                let mut errors = Vec::new();
146
147                let value = bundle.format_pattern(&message, args, &mut errors);
148
149                if errors.is_empty() {
150                    Some(value.into())
151                } else {
152                    panic!(
153                        "Failed to format a message for locale {} and id {}.\nErrors\n{:?}",
154                        lang, text_id, errors
155                    )
156                }
157            } else {
158                None
159            }
160        } else {
161            panic!("Unknown language {}", lang)
162        }
163    }
164}
165
166