Skip to main content

fluent_templates/loader/
tera.rs

1use fluent_bundle::FluentValue;
2use std::borrow::Cow;
3use std::collections::HashMap;
4use unic_langid::LanguageIdentifier;
5
6use crate::Loader;
7
8const LANG_KEY: &str = "lang";
9const FLUENT_KEY: &str = "key";
10
11#[derive(Debug, thiserror::Error)]
12enum Error {
13    #[error("No `lang` argument provided.")]
14    NoLangArgument,
15    #[error("`lang` must be a valid unicode language identifier.")]
16    LangArgumentInvalid,
17    #[error("No `id` argument provided.")]
18    NoFluentArgument,
19    #[error("Couldn't convert JSON to Fluent value.")]
20    JsonToFluentFail,
21}
22
23impl From<Error> for tera::Error {
24    fn from(error: Error) -> Self {
25        tera::Error::message(error)
26    }
27}
28
29fn tera_to_fluent(json: tera::Value) -> crate::Result<FluentValue<'static>, Error> {
30    if let Some(n) = json.as_f64() {
31        return Ok(FluentValue::from(n));
32    }
33    if let Some(n) = json.as_u64() {
34        return Ok(FluentValue::from(n));
35    }
36    if let Some(s) = json.as_str() {
37        return Ok(FluentValue::from(s.to_string()));
38    }
39    return Err(Error::JsonToFluentFail);
40}
41
42fn parse_language(arg: &str) -> crate::Result<LanguageIdentifier, Error> {
43    arg.parse::<LanguageIdentifier>()
44        .ok()
45        .ok_or(Error::LangArgumentInvalid)
46}
47
48impl<L: Loader + Send + Sync + 'static> tera::Function<tera::TeraResult<tera::Value>>
49    for crate::FluentLoader<L>
50{
51    fn call(&self, kwargs: tera::Kwargs, _: &tera::State) -> tera::TeraResult<tera::Value> {
52        let lang_arg: Option<LanguageIdentifier> = kwargs
53            .get::<&str>(LANG_KEY)
54            .map_err(|_| Error::LangArgumentInvalid)?
55            .map(parse_language)
56            .transpose()?;
57        let lang = lang_arg
58            .as_ref()
59            .or(self.default_lang.as_ref())
60            .ok_or(Error::NoLangArgument)?;
61
62        let id = kwargs
63            .must_get::<&str>(FLUENT_KEY)
64            .map_err(|_| Error::NoFluentArgument)?;
65
66        /// Filters kwargs to exclude ones used by this function and tera.
67        fn is_not_tera_key((k, _): &(&tera::value::Key<'_>, &tera::Value)) -> bool {
68            match k.as_str() {
69                Some(LANG_KEY) | Some(FLUENT_KEY) | Some("__tera_one_off") => false,
70                _ => true,
71            }
72        }
73
74        let mut fluent_args = HashMap::new();
75
76        for (key, value) in kwargs.iter().filter(is_not_tera_key) {
77            if let Some(key) = key.as_str() {
78                fluent_args.insert(
79                    Cow::from(heck::ToKebabCase::to_kebab_case(key)),
80                    tera_to_fluent(value.clone())?,
81                );
82            }
83        }
84
85        let response = self.loader.lookup_with_args(lang, id, &fluent_args);
86        Ok(tera::Value::from(response))
87    }
88}