fluent_templates/loader/
tera.rs

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