diff --git a/i18n-embed-fl/i18n/en-US/i18n_embed_fl.ftl b/i18n-embed-fl/i18n/en-US/i18n_embed_fl.ftl index 00c0c94..b6aede5 100644 --- a/i18n-embed-fl/i18n/en-US/i18n_embed_fl.ftl +++ b/i18n-embed-fl/i18n/en-US/i18n_embed_fl.ftl @@ -3,4 +3,13 @@ hello-arg = Hello {$name}! .attr = Hello {$name}'s attribute! hello-arg-2 = Hello {$name1} and {$name2}! hello-attr = Uninspiring. - .text = Hello, attribute! \ No newline at end of file + .text = Hello, attribute! +hello-recursive = Hello { hello-recursive-descent } + .attr = Why hello { hello-recursive-descent } + .again = Why hello { hello-recursive-descent.attr } +hello-recursive-descent = to you, {$name}! + .attr = again, {$name}! +hello-select = { $attr -> + *[no] { hello-recursive } + [yes] { hello-recursive.attr } +} diff --git a/i18n-embed-fl/src/lib.rs b/i18n-embed-fl/src/lib.rs index e68c915..e95f8f9 100644 --- a/i18n-embed-fl/src/lib.rs +++ b/i18n-embed-fl/src/lib.rs @@ -1,4 +1,5 @@ -use fluent::{FluentAttribute, FluentMessage}; +use fluent::concurrent::FluentBundle; +use fluent::{FluentAttribute, FluentMessage, FluentResource}; use fluent_syntax::ast::{CallArguments, Expression, InlineExpression, Pattern, PatternElement}; use i18n_embed::{fluent::FluentLanguageLoader, FileSystemAssets, LanguageLoader}; use proc_macro::TokenStream; @@ -504,8 +505,8 @@ pub fn fl(input: TokenStream) -> TokenStream { if let Some(message_id_str) = &message_id_string { checked_loader_has_message = domain_data .loader - .with_fluent_message(message_id_str, |message: FluentMessage<'_>| { - check_message_args(message, &specified_args); + .with_fluent_message_and_bundle(message_id_str, |message, bundle| { + check_message_args(message, bundle, &specified_args); }) .is_some(); } @@ -524,11 +525,11 @@ pub fn fl(input: TokenStream) -> TokenStream { } else { if let Some(message_id_str) = &message_id_string { if let Some(attr_id_str) = &attr_str { - let attr_res = domain_data.loader.with_fluent_message( + let attr_res = domain_data.loader.with_fluent_message_and_bundle( message_id_str, - |message: FluentMessage<'_>| match message.get_attribute(attr_id_str) { + |message, bundle| match message.get_attribute(attr_id_str) { Some(attr) => { - check_attribute_args(attr, &specified_args); + check_attribute_args(attr, bundle, &specified_args); true } None => false, @@ -668,13 +669,16 @@ fn fuzzy_attribute_suggestions( .collect() } -fn check_message_args( +fn check_message_args( message: FluentMessage<'_>, + bundle: &FluentBundle, specified_args: &HashMap>, -) { +) where + R: std::borrow::Borrow, +{ if let Some(pattern) = message.value() { let mut args = Vec::new(); - args_from_pattern(pattern, &mut args); + args_from_pattern(pattern, bundle, &mut args); let args_set: HashSet<&str> = args.into_iter().collect(); @@ -735,13 +739,16 @@ fn check_message_args( } } -fn check_attribute_args( +fn check_attribute_args( attr: FluentAttribute<'_>, + bundle: &FluentBundle, specified_args: &HashMap>, -) { +) where + R: std::borrow::Borrow, +{ let pattern = attr.value(); let mut args = Vec::new(); - args_from_pattern(pattern, &mut args); + args_from_pattern(pattern, bundle, &mut args); let args_set: HashSet<&str> = args.into_iter().collect(); @@ -801,56 +808,101 @@ fn check_attribute_args( } } -fn args_from_pattern(pattern: &Pattern, args: &mut Vec) { +fn args_from_pattern<'m, R>( + pattern: &Pattern<&'m str>, + bundle: &'m FluentBundle, + args: &mut Vec<&'m str>, +) where + R: std::borrow::Borrow, +{ pattern.elements.iter().for_each(|element| { if let PatternElement::Placeable { expression } = element { - args_from_expression(expression, args) + args_from_expression(expression, bundle, args) } }); } -fn args_from_expression(expr: &Expression, args: &mut Vec) { +fn args_from_expression<'m, R>( + expr: &Expression<&'m str>, + bundle: &'m FluentBundle, + args: &mut Vec<&'m str>, +) where + R: std::borrow::Borrow, +{ match expr { Expression::Inline(inline_expr) => { - args_from_inline_expression(inline_expr, args); + args_from_inline_expression(inline_expr, bundle, args); } Expression::Select { selector, variants } => { - args_from_inline_expression(selector, args); + args_from_inline_expression(selector, bundle, args); variants.iter().for_each(|variant| { - args_from_pattern(&variant.value, args); + args_from_pattern(&variant.value, bundle, args); }) } } } -fn args_from_inline_expression(inline_expr: &InlineExpression, args: &mut Vec) { +fn args_from_inline_expression<'m, R>( + inline_expr: &InlineExpression<&'m str>, + bundle: &'m FluentBundle, + args: &mut Vec<&'m str>, +) where + R: std::borrow::Borrow, +{ match inline_expr { InlineExpression::FunctionReference { id: _, arguments: call_args, } => { - args_from_call_arguments(call_args, args); + args_from_call_arguments(call_args, bundle, args); } InlineExpression::TermReference { id: _, attribute: _, arguments: Some(call_args), } => { - args_from_call_arguments(call_args, args); + args_from_call_arguments(call_args, bundle, args); } InlineExpression::VariableReference { id } => args.push(id.name), - InlineExpression::Placeable { expression } => args_from_expression(expression, args), + InlineExpression::Placeable { expression } => { + args_from_expression(expression, bundle, args) + } + InlineExpression::MessageReference { + id, + attribute: None, + } => { + bundle + .get_message(&id.name) + .and_then(|m| m.value()) + .map(|p| args_from_pattern(p, bundle, args)); + } + InlineExpression::MessageReference { + id, + attribute: Some(attribute), + } => { + bundle + .get_message(&id.name) + .and_then(|m| m.get_attribute(&attribute.name)) + .map(|m| m.value()) + .map(|p| args_from_pattern(p, bundle, args)); + } _ => {} } } -fn args_from_call_arguments(call_args: &CallArguments, args: &mut Vec) { +fn args_from_call_arguments<'m, R>( + call_args: &CallArguments<&'m str>, + bundle: &'m FluentBundle, + args: &mut Vec<&'m str>, +) where + R: std::borrow::Borrow, +{ call_args.positional.iter().for_each(|expr| { - args_from_inline_expression(expr, args); + args_from_inline_expression(expr, bundle, args); }); call_args.named.iter().for_each(|named_arg| { - args_from_inline_expression(&named_arg.value, args); + args_from_inline_expression(&named_arg.value, bundle, args); }) } diff --git a/i18n-embed-fl/tests/fl_macro.rs b/i18n-embed-fl/tests/fl_macro.rs index 41a59ed..1afee66 100644 --- a/i18n-embed-fl/tests/fl_macro.rs +++ b/i18n-embed-fl/tests/fl_macro.rs @@ -90,3 +90,60 @@ fn with_attr_and_args() { fl!(loader, "hello-arg", "attr", name = "Bob") ); } + +#[test] +fn with_args_in_messagereference() { + let loader: FluentLanguageLoader = fluent_language_loader!(); + loader + .load_languages(&Localizations, &[loader.fallback_language().clone()]) + .unwrap(); + + pretty_assertions::assert_eq!( + "Hello to you, \u{2068}Bob\u{2069}!", + fl!(loader, "hello-recursive", name = "Bob") + ); +} + +#[test] +fn with_args_in_messagereference_attr() { + let loader: FluentLanguageLoader = fluent_language_loader!(); + loader + .load_languages(&Localizations, &[loader.fallback_language().clone()]) + .unwrap(); + + pretty_assertions::assert_eq!( + "Why hello to you, \u{2068}Bob\u{2069}!", + fl!(loader, "hello-recursive", "attr", name = "Bob") + ); +} + +#[test] +fn with_args_in_messagereference_attr_to_attr() { + let loader: FluentLanguageLoader = fluent_language_loader!(); + loader + .load_languages(&Localizations, &[loader.fallback_language().clone()]) + .unwrap(); + + pretty_assertions::assert_eq!( + "Why hello again, \u{2068}Bob\u{2069}!", + fl!(loader, "hello-recursive", "again", name = "Bob") + ); +} + +#[test] +fn with_args_in_select_messagereference() { + let loader: FluentLanguageLoader = fluent_language_loader!(); + loader + .load_languages(&Localizations, &[loader.fallback_language().clone()]) + .unwrap(); + + pretty_assertions::assert_eq!( + "Hello to you, \u{2068}Bob\u{2069}!", + fl!(loader, "hello-select", attr = "", name = "Bob") + ); + + pretty_assertions::assert_eq!( + "Why hello to you, \u{2068}Bob\u{2069}!", + fl!(loader, "hello-select", attr = "yes", name = "Bob") + ); +} diff --git a/i18n-embed/src/fluent.rs b/i18n-embed/src/fluent.rs index ac1f2fb..4a28028 100644 --- a/i18n-embed/src/fluent.rs +++ b/i18n-embed/src/fluent.rs @@ -342,6 +342,40 @@ impl FluentLanguageLoader { .map(closure) } + /// Searches for a message named `message_id` in all languages that + /// are currently loaded, including the fallback language. If the + /// message is found, invokes the `closure` with the: + /// + /// 0. [message](FluentMessage) + /// 1. the language-specific [bundle](FluentBundle) + /// that owns it. + /// + /// Returns `Some` of whatever the closure returns, or `None` if no + /// messages were found matching the `message_id`. + pub fn with_fluent_message_and_bundle( + &self, + message_id: &str, + closure: C, + ) -> Option + where + C: Fn(FluentMessage<'_>, &FluentBundle, IntlLangMemoizer>) -> OUT, + { + self.inner + .load() + .language_config + .read() + .language_bundles + .iter() + .flat_map(|language_bundles| language_bundles.iter()) + .find_map(|language_bundle| { + Some(( + language_bundle.bundle.get_message(message_id)?, + &language_bundle.bundle, + )) + }) + .map(|(msg, bundle)| closure(msg, bundle)) + } + /// Runs the provided `closure` with an iterator over the messages /// available for the specified `language`. There may be duplicate /// messages when they are duplicated in resources applicable to