fluent_template_helper/loader/
static_loader.rs

1use std::collections::HashMap;
2
3use fluent_bundle::concurrent::FluentBundle;
4use fluent_bundle::{FluentResource, FluentValue};
5
6pub use unic_langid::{langid, langids, LanguageIdentifier};
7
8/// Loads Fluent data at runtime via `lazy_static` to produce a loader.
9///
10/// Usage:
11///
12/// ```rust
13/// use fluent_template_helper::*;
14///
15/// static_loader!(create_loader, "./tests/locales/", "en-US");
16///
17/// fn init() {
18///     let loader = create_loader();
19///     let helper = FluentHelper::new(loader);
20/// }
21/// ```
22///
23/// `$constructor` is the name of the constructor function for the loader, `$location` is
24/// the location of a folder containing individual locale folders, `$fallback` is the language to use
25/// for fallback strings.
26///
27/// Some Fluent users have a share "core.ftl" file that contains strings used by all locales,
28/// for example branding information. They also may want to define custom functions on the bundle.
29///
30/// This can be done with an extended invocation:
31///
32/// ```rust
33/// use fluent_template_helper::*;
34///
35/// static_loader!(create_loader, "./tests/locales/", "en-US", core: "./tests/core.ftl",
36///                customizer: |bundle| {bundle.add_function("FOOBAR", |_values, _named| {unimplemented!()}); });
37///
38/// fn init() {
39///     let loader = create_loader();
40///     let helper = FluentHelper::new(loader);
41/// }
42/// ```
43///
44/// The constructor function is cheap to call multiple times since all the heavy duty stuff is stored in shared statics.
45///
46#[macro_export]
47macro_rules! static_loader {
48    ($constructor:ident, $location:expr, $fallback:expr) => {
49        $crate::lazy_static::lazy_static! {
50            static ref RESOURCES: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::fluent_bundle::FluentResource>> = $crate::loader::build_resources($location);
51            static ref BUNDLES: std::collections::HashMap<$crate::loader::LanguageIdentifier, $crate::fluent_bundle::concurrent::FluentBundle<&'static $crate::fluent_bundle::FluentResource>> = $crate::loader::build_bundles(&&RESOURCES, None, |_bundle| {});
52            static ref LOCALES: Vec<$crate::loader::LanguageIdentifier> = RESOURCES.keys().cloned().collect();
53            static ref FALLBACKS: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::loader::LanguageIdentifier>> = $crate::loader::build_fallbacks(&*LOCALES);
54        }
55
56        pub fn $constructor() -> $crate::loader::StaticLoader {
57            $crate::loader::StaticLoader::new(&*BUNDLES, &*FALLBACKS, $fallback.parse().expect("fallback language not valid"))
58        }
59    };
60    ($constructor:ident, $location:expr, $fallback:expr, core: $core:expr, customizer: $custom:expr) => {
61        $crate::lazy_static::lazy_static! {
62            static ref CORE_RESOURCE: $crate::fluent_bundle::FluentResource = $crate::loader::load_core_resource($core);
63            static ref RESOURCES: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::fluent_bundle::FluentResource>> = $crate::loader::build_resources($location);
64            static ref BUNDLES: std::collections::HashMap<$crate::loader::LanguageIdentifier, $crate::fluent_bundle::concurrent::FluentBundle<&'static $crate::fluent_bundle::FluentResource>> = $crate::loader::build_bundles(&*RESOURCES, Some(&CORE_RESOURCE), $custom);
65            static ref LOCALES: Vec<$crate::loader::LanguageIdentifier> = RESOURCES.keys().cloned().collect();
66            static ref FALLBACKS: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::loader::LanguageIdentifier>> = $crate::loader::build_fallbacks(&*LOCALES);
67        }
68
69        pub fn $constructor() -> $crate::loader::StaticLoader {
70            $crate::loader::StaticLoader::new(&*BUNDLES, &*FALLBACKS, $fallback.parse().expect("fallback language not valid"))
71        }
72    };
73}
74
75/// A simple Loader implementation, with statically-loaded fluent data.
76/// Typically created with the [`static_loader!()`] macro
77pub struct StaticLoader {
78    bundles: &'static HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>>,
79    fallbacks: &'static HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
80    fallback: LanguageIdentifier,
81}
82
83impl StaticLoader {
84    /// Construct a StaticLoader
85    ///
86    /// You should probably be using the constructor from `static_loader!()`
87    pub fn new(
88        bundles: &'static HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>>,
89        fallbacks: &'static HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
90        fallback: LanguageIdentifier,
91    ) -> Self {
92        Self {
93            bundles,
94            fallbacks,
95            fallback,
96        }
97    }
98
99    /// Convenience function to look up a string for a single language
100    pub fn lookup_single_language(
101        &self,
102        lang: &LanguageIdentifier,
103        text_id: &str,
104        args: Option<&HashMap<&str, FluentValue>>,
105    ) -> Option<String> {
106        if let Some(bundle) = self.bundles.get(lang) {
107            if let Some(message) = bundle.get_message(text_id).and_then(|m| m.value) {
108                let mut errors = Vec::new();
109
110                let value = bundle.format_pattern(&message, dbg!(args), &mut errors);
111
112                if errors.is_empty() {
113                    Some(value.into())
114                } else {
115                    panic!(
116                        "Failed to format a message for locale {} and id {}.\nErrors\n{:?}",
117                        lang, text_id, errors
118                    )
119                }
120            } else {
121                None
122            }
123        } else {
124            panic!("Unknown language {}", lang)
125        }
126    }
127
128    /// Convenience function to look up a string without falling back to the default fallback language
129    pub fn lookup_no_default_fallback(
130        &self,
131        lang: &LanguageIdentifier,
132        text_id: &str,
133        args: Option<&HashMap<&str, FluentValue>>,
134    ) -> Option<String> {
135        for l in self.fallbacks.get(lang).expect("language not found") {
136            if let Some(val) = self.lookup_single_language(l, text_id, args) {
137                return Some(val);
138            }
139        }
140
141        None
142    }
143}
144
145impl super::Loader for StaticLoader {
146    // Traverse the fallback chain,
147    fn lookup(
148        &self,
149        lang: &LanguageIdentifier,
150        text_id: &str,
151        args: Option<&HashMap<&str, FluentValue>>,
152    ) -> String {
153        for l in self.fallbacks.get(lang).expect("language not found") {
154            if let Some(val) = self.lookup_single_language(l, text_id, args) {
155                return val;
156            }
157        }
158        if *lang != self.fallback {
159            if let Some(val) = self.lookup_single_language(&self.fallback, text_id, args) {
160                return val;
161            }
162        }
163        format!("Unknown localization {}", text_id)
164    }
165}
166
167