diff --git a/src/fluent.rs b/src/fluent.rs index 7cff72f..cbde448 100644 --- a/src/fluent.rs +++ b/src/fluent.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Debug, path::Path}; +use std::{collections::HashMap, error::Error, fmt::Debug, path::Path}; use fluent::{bundle::FluentBundle, types::FluentNumberOptions, FluentArgs, FluentResource}; use unic_langid::LanguageIdentifier; @@ -104,7 +104,7 @@ impl Localizer { /// Searches for a full locale match and returns it. /// If no full locale match, returns a language match if available pub fn get_locale(&self, locale: &LanguageIdentifier) -> Option<&Bundle> { - let full_locale_match = self.locales.get(&*locale); + let full_locale_match = self.locales.get(locale); // Try to match only on the language if full match not found match full_locale_match { @@ -122,31 +122,67 @@ impl Localizer { /// for details /// /// Fluent template errors are printed to stdout. - pub fn format_message<'a>( + pub fn format_message( &self, locale: &LanguageIdentifier, - key: &str, - args: Option<&'a FluentArgs>, + key: &(impl MessageKey + ?Sized), + args: Option<&FluentArgs>, ) -> Option { - let bundle = self.get_locale(locale)?; + self.format_message_result(locale, key, args).ok() + } - let message = bundle.get_message(key)?; + /// Format a FTL message into target locale if available.
+ /// See Fluent RS [FluentBundle::format_pattern documentation](https://docs.rs/fluent/latest/fluent/bundle/struct.FluentBundle.html#method.format_pattern) + /// for details + /// + /// Fluent template errors are printed to stdout. + pub fn format_message_result( + &self, + locale: &LanguageIdentifier, + key: &(impl MessageKey + ?Sized), + args: Option<&FluentArgs>, + ) -> Result> { + let bundle = self + .get_locale(locale) + .ok_or_else(|| format!("could not find locale {locale}"))?; - let pattern = message.value()?; + let message = bundle + .get_message(key.key()) + .ok_or_else(|| format!("could not find message with key={}", key.key()))?; let mut errors = Vec::new(); - let message = bundle - .format_pattern(pattern, args, &mut errors) - .to_string(); + let message = if let Some(attribute) = key.attribute() { + let attribute = message.get_attribute(attribute).ok_or_else(|| { + format!( + "could not find attribute={attribute} for message with key={}", + key.key() + ) + })?; - if errors.len() > 0 { - for err in errors { - println!("{}", err.to_string()); - } + bundle + .format_pattern(attribute.value(), args, &mut errors) + .to_string() + } else { + bundle + .format_pattern( + message.value().ok_or_else(|| { + format!( + "message with key={} does not have a standalone message", + key.key() + ) + })?, + args, + &mut errors, + ) + .to_string() + }; + + for err in errors { + println!("{err}"); } - Some(message) + Ok(message) } pub fn iter(&self) -> std::collections::hash_map::Iter { @@ -160,6 +196,36 @@ impl Localizer { } } +pub trait MessageKey { + fn key(&self) -> &str; + + fn attribute(&self) -> Option<&str> { + None + } +} + +impl + ?Sized> MessageKey for S { + fn key(&self) -> &str { + self.as_ref() + } +} + +#[derive(Debug, Clone, Copy)] +pub struct MessageAttribute<'key, 'attribute> { + pub key: &'key str, + pub attribute: &'attribute str, +} + +impl<'key, 'attribute> MessageKey for MessageAttribute<'key, 'attribute> { + fn key(&self) -> &str { + self.key + } + + fn attribute(&self) -> Option<&str> { + Some(self.attribute) + } +} + #[cfg(test)] mod tests { use super::*; @@ -199,6 +265,50 @@ mod tests { assert!(bundle.is_some()); } + #[test] + fn compiles_with_borrowed_string() { + let mut loc = Localizer::new(); + loc.add_bundle(ENGLISH, &[MAIN, SUB]).unwrap(); + + loc.format_message(&ENGLISH, &"test-key-a".to_owned(), None); + } + + #[test] + fn use_attributes() { + let mut loc = Localizer::new(); + loc.add_bundle(ENGLISH, &[MAIN, SUB]).unwrap(); + + let message = loc + .format_message( + &ENGLISH, + &MessageAttribute { + key: "attribute-test", + attribute: "attribute_a", + }, + None, + ) + .expect("formatting succeeds"); + + assert_eq!("Hello", message) + } + + #[test] + fn not_existing_attribute() { + let mut loc = Localizer::new(); + loc.add_bundle(ENGLISH, &[MAIN, SUB]).unwrap(); + + assert!(loc + .format_message( + &ENGLISH, + &MessageAttribute { + key: "attribute-test", + attribute: "does_not_exist", + }, + None, + ) + .is_none()); + } + #[test] fn can_format_pattern() { let mut loc = Localizer::new(); diff --git a/src/tera.rs b/src/tera.rs index 72ddf99..8517d4d 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::HashMap}; -use crate::Localizer; +use crate::{fluent::MessageAttribute, Localizer}; use fluent::{ types::{FluentNumber, FluentNumberOptions}, FluentArgs, FluentValue, @@ -21,19 +21,7 @@ impl tera::Function for Localizer { .and_then(|key| key.as_str()) .ok_or(tera::Error::msg("missing ftl key"))?; - let bundle = self.get_locale(&lang_arg).ok_or(tera::Error::msg(format!( - "locale not registered: {lang_arg}" - )))?; - - let msg = bundle - .get_message(ftl_key) - .ok_or(tera::Error::msg(&format!( - "FTL key not in locale: {}", - ftl_key - )))?; - let pattern = msg - .value() - .ok_or(tera::Error::msg("No value in fluent message"))?; + let ftl_attribute = args.get("attribute").and_then(|attr| attr.as_str()); let fluent_args: FluentArgs = args .iter() @@ -41,14 +29,21 @@ impl tera::Function for Localizer { .map(|(key, val)| (key, json_value_to_fluent_value(val, self.number_options()))) .collect(); - let mut errs = Vec::new(); - let res = bundle.format_pattern(pattern, Some(&fluent_args), &mut errs); - - if errs.len() > 0 { - dbg!(errs); + let message = if let Some(ftl_attribute) = ftl_attribute { + self.format_message_result( + &lang_arg, + &MessageAttribute { + key: ftl_key, + attribute: ftl_attribute, + }, + Some(&fluent_args), + ) + } else { + self.format_message_result(&lang_arg, ftl_key, Some(&fluent_args)) } + .map_err(|err| tera::Error::chain("failed to format message", err))?; - Ok(serde_json::Value::String(res.into())) + Ok(serde_json::Value::String(message)) } fn is_safe(&self) -> bool { diff --git a/test_data/main.ftl b/test_data/main.ftl index a2cc634..bf86008 100644 --- a/test_data/main.ftl +++ b/test_data/main.ftl @@ -1,2 +1,6 @@ test-key-a = Hello World test-name = Peg { $name } + +attribute-test = + .attribute_a = Hello + .attribute_b = there!