bracket_fluent/
lib.rs

1#![deny(missing_docs)]
2
3//! Helper for [fluent](https://www.projectfluent.org/) language lookup.
4//!
5//! The root data for the template must contain a `lang` field 
6//! with the identifier of the current language.
7//!
8//! Assuming a fluent language file in `locales/en/main.ftl` and the `lang` 
9//! field is set to `en`, using the contents:
10//!
11//! ```ignore
12//! welcome = Hello!
13//! greeting = Hello { $name }!
14//! block = { $var1 } { $var2 }
15//! ```
16//!
17//! We can resolve the message in a template using the fluent helper like this:
18//!
19//! ```ignore
20//! {{fluent "welcome"}}
21//! ```
22//!
23//! Pass variables using the helper hash parameters:
24//!
25//! ```ignore
26//! {{fluent "greeting" name="world"}}
27//! ```
28//!
29//! If you need to pass multi-line variables to a message use the `fluentparam` syntax 
30//! inside a block call:
31//!
32//! ```ignore
33//! {{#fluent "block"~}}
34//! {{#fluentparam "var1"~}}
35//! This is some multi-line content for 
36//! the first variable parameter named var1.
37//! {{/fluentparam}}
38//!
39//! {{#fluentparam "var2"}}
40//! Which is continued in another multi-line 
41//! paragraph using the variable named var2.
42//! {{~/fluentparam~}}
43//! {{/fluent}}
44//! ```
45//!
46use std::collections::HashMap;
47use std::sync::{Arc, RwLock};
48
49use bracket::{
50    error::HelperError,
51    helper::{Helper, HelperValue, LocalHelper},
52    parser::ast::Node,
53    render::{Context, Render, Type},
54};
55
56use serde_json::Value;
57
58use fluent_templates::fluent_bundle::FluentValue;
59use fluent_templates::LanguageIdentifier;
60use fluent_templates::Loader;
61
62static FLUENT_PARAM: &str = "fluentparam";
63
64/// Local helper for `{{#fluentparam}}` blocks.
65#[derive(Clone)]
66pub struct FluentParam {
67    parameters: Arc<RwLock<HashMap<String, String>>>,
68}
69
70impl Helper for FluentParam {
71    fn call<'render, 'call>(
72        &self,
73        rc: &mut Render<'render>,
74        ctx: &Context<'call>,
75        template: Option<&'render Node<'render>>,
76    ) -> HelperValue {
77        ctx.arity(1..1)?;
78        ctx.assert_block(template)?;
79
80        let param_name = ctx.try_get(0, &[Type::String])?.as_str().unwrap();
81
82        let node = template.unwrap();
83        let content = rc.buffer(node)?;
84        let mut writer = self.parameters.write().unwrap();
85        writer.insert(param_name.to_string(), content);
86
87        Ok(None)
88    }
89}
90
91impl LocalHelper for FluentParam {}
92
93/// Lookup a language string in the underlying loader.
94pub struct FluentHelper {
95    loader: Box<dyn Loader + Send + Sync>,
96    /// Escape messages, default is `true`.
97    pub escape: bool,
98}
99
100impl FluentHelper {
101    /// Create a new fluent helper.
102    ///
103    /// Messages are resolved using the underlying loader.
104    pub fn new(loader: Box<dyn Loader + Send + Sync>) -> Self {
105        Self {
106            loader,
107            escape: true,
108        }
109    }
110}
111
112impl Helper for FluentHelper {
113    fn call<'render, 'call>(
114        &self,
115        rc: &mut Render<'render>,
116        ctx: &Context<'call>,
117        template: Option<&'render Node<'render>>,
118    ) -> HelperValue {
119        ctx.arity(1..1)?;
120
121        let msg_id = ctx.try_get(0, &[Type::String])?.as_str().unwrap();
122
123        let lang = rc
124            .evaluate("@root.lang")?
125            .ok_or_else(|| {
126                HelperError::new(format!(
127                    "Helper '{}' requires a 'lang' variable in the template data",
128                    ctx.name()
129                ))
130            })?
131            .as_str()
132            .ok_or_else(|| {
133                HelperError::new(format!(
134                    "Type error in helper '{}' the 'lang' variable must be a string",
135                    ctx.name()
136                ))
137            })?;
138
139        let lang_id = lang
140            .parse::<LanguageIdentifier>()
141            .map_err(|e| HelperError::new(e.to_string()))?;
142
143        // Build arguments from hash parameters
144        let mut args: Option<HashMap<String, FluentValue>> =
145            if ctx.parameters().is_empty() {
146                None
147            } else {
148                let map = ctx
149                    .parameters()
150                    .iter()
151                    .filter_map(|(k, v)| {
152                        let val = match v {
153                            // `Number::as_f64` can't fail here because we haven't
154                            // enabled `arbitrary_precision` feature
155                            // in `serde_json`.
156                            Value::Number(n) => n.as_f64().unwrap().into(),
157                            Value::String(s) => s.to_owned().into(),
158                            _ => return None,
159                        };
160                        Some((k.to_string(), val))
161                    })
162                    .collect();
163                Some(map)
164            };
165
166        if let Some(node) = template {
167            let parameters: Arc<RwLock<HashMap<String, String>>> =
168                Arc::new(RwLock::new(HashMap::new()));
169            let local_helper = FluentParam {
170                parameters: Arc::clone(&parameters),
171            };
172            rc.register_local_helper(FLUENT_PARAM, Box::new(local_helper));
173            rc.template(node)?;
174            rc.unregister_local_helper(FLUENT_PARAM);
175
176            let lock = Arc::try_unwrap(parameters).expect(
177                "Fluent helper parameters lock still has multiple owners!",
178            );
179            let map = lock
180                .into_inner()
181                .expect("Fluent helper failed to get inner value from lock!");
182
183            let params = args.get_or_insert(HashMap::new());
184            for (k, v) in map {
185                params.insert(k, FluentValue::String(v.into()));
186            }
187        }
188
189        let message =
190            self.loader
191                .lookup_complete(&lang_id, &msg_id, args.as_ref());
192        if self.escape {
193            rc.write_escaped(&message)?;
194        } else {
195            rc.write(&message)?;
196        }
197
198        Ok(None)
199    }
200}