diff --git a/fluent-bundle/src/args.rs b/fluent-bundle/src/args.rs index 58a560e0..f301d378 100644 --- a/fluent-bundle/src/args.rs +++ b/fluent-bundle/src/args.rs @@ -52,7 +52,7 @@ use crate::types::FluentValue; /// "Hello, John. You have 5 messages." /// ); /// ``` -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct FluentArgs<'args>(Vec<(Cow<'args, str>, FluentValue<'args>)>); impl<'args> FluentArgs<'args> { diff --git a/fluent-bundle/src/bundle.rs b/fluent-bundle/src/bundle.rs index 41a00e24..66eb08bd 100644 --- a/fluent-bundle/src/bundle.rs +++ b/fluent-bundle/src/bundle.rs @@ -24,6 +24,7 @@ use crate::message::FluentMessage; use crate::resolver::{ResolveValue, Scope, WriteValue}; use crate::resource::FluentResource; use crate::types::FluentValue; +use crate::FluentFunctionObject; /// A collection of localization messages for a single locale, which are meant /// to be used together in a single view, widget or any other UI abstraction. @@ -535,6 +536,13 @@ impl FluentBundle { pub fn add_function(&mut self, id: &str, func: F) -> Result<(), FluentError> where F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static, + { + self.add_function_with_scope(id, func) + } + + pub fn add_function_with_scope(&mut self, id: &str, func: F) -> Result<(), FluentError> + where + F: FluentFunctionObject + Sync + Send + 'static, { match self.entries.entry(id.to_owned()) { HashEntry::Vacant(entry) => { diff --git a/fluent-bundle/src/entry.rs b/fluent-bundle/src/entry.rs index 37b3eccc..7deab1ca 100644 --- a/fluent-bundle/src/entry.rs +++ b/fluent-bundle/src/entry.rs @@ -9,9 +9,7 @@ use crate::args::FluentArgs; use crate::bundle::FluentBundle; use crate::resource::FluentResource; use crate::types::FluentValue; - -pub type FluentFunction = - Box Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Send + Sync>; +use crate::FluentMessage; type ResourceIdx = usize; type EntryIdx = usize; @@ -71,3 +69,40 @@ impl, M> GetEntry for FluentBundle { }) } } + +pub type FluentFunction = Box; + +pub trait FluentFunctionScope<'bundle> { + fn get_message(&self, id: &str) -> Option>; + + fn format_message( + &mut self, + pattern: &'bundle ast::Pattern<&'bundle str>, + args: Option>, + ) -> FluentValue<'bundle>; +} + +/// Implement custom function that retrieves execution scope information +pub trait FluentFunctionObject { + fn call<'bundle>( + &self, + scope: &mut dyn FluentFunctionScope<'bundle>, + positional: &[FluentValue<'bundle>], + named: &FluentArgs<'bundle>, + ) -> FluentValue<'bundle>; +} + +impl FluentFunctionObject for F +where + F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static, +{ + fn call<'bundle>( + &self, + scope: &mut dyn FluentFunctionScope, + positional: &[FluentValue<'bundle>], + named: &FluentArgs, + ) -> FluentValue<'bundle> { + let _ = scope; + self(positional, named) + } +} diff --git a/fluent-bundle/src/lib.rs b/fluent-bundle/src/lib.rs index 93d7ea53..5415315f 100644 --- a/fluent-bundle/src/lib.rs +++ b/fluent-bundle/src/lib.rs @@ -121,6 +121,7 @@ pub use args::FluentArgs; /// The concurrent specialization can be constructed with /// [`FluentBundle::new_concurrent`](crate::concurrent::FluentBundle::new_concurrent). pub type FluentBundle = bundle::FluentBundle; +pub use entry::{FluentFunctionObject, FluentFunctionScope}; pub use errors::FluentError; pub use message::{FluentAttribute, FluentMessage}; pub use resource::FluentResource; diff --git a/fluent-bundle/src/resolver/inline_expression.rs b/fluent-bundle/src/resolver/inline_expression.rs index 3f8c8d4f..da0a477d 100644 --- a/fluent-bundle/src/resolver/inline_expression.rs +++ b/fluent-bundle/src/resolver/inline_expression.rs @@ -3,6 +3,7 @@ use super::{ResolveValue, ResolverError, WriteValue}; use std::borrow::Borrow; use std::fmt; +use std::ops::Deref; use fluent_syntax::ast; use fluent_syntax::unicode::{unescape_unicode, unescape_unicode_to_string}; @@ -89,7 +90,11 @@ impl<'bundle> WriteValue<'bundle> for ast::InlineExpression<&'bundle str> { let func = scope.bundle.get_entry_function(id.name); if let Some(func) = func { - let result = func(resolved_positional_args.as_slice(), &resolved_named_args); + let result = func.call( + scope, + resolved_positional_args.as_slice(), + &resolved_named_args, + ); if let FluentValue::Error = result { self.write_error(w) } else { @@ -157,8 +162,12 @@ impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> { M: MemoizerKind, { match self { - Self::StringLiteral { value } => unescape_unicode_to_string(value).into(), - Self::NumberLiteral { value } => FluentValue::try_number(value), + Self::StringLiteral { value } => { + return unescape_unicode_to_string(value).into(); + } + Self::NumberLiteral { value } => { + return FluentValue::try_number(value); + } Self::VariableReference { id } => { if let Some(local_args) = &scope.local_args { if let Some(arg) = local_args.get(id.name) { @@ -171,7 +180,7 @@ impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> { if scope.local_args.is_none() { scope.add_error(self.into()); } - FluentValue::Error + return FluentValue::Error; } Self::FunctionReference { id, arguments } => { let (resolved_positional_args, resolved_named_args) = @@ -180,17 +189,27 @@ impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> { let func = scope.bundle.get_entry_function(id.name); if let Some(func) = func { - let result = func(resolved_positional_args.as_slice(), &resolved_named_args); - result + let result = func.call( + scope, + resolved_positional_args.as_slice(), + &resolved_named_args, + ); + return result; } else { - FluentValue::Error + return FluentValue::Error; } } - _ => { - let mut result = String::new(); - self.write(&mut result, scope).expect("Failed to write"); - result.into() + Self::Placeable { expression } => { + if let ast::Expression::Inline(expression) = expression.deref() { + return expression.resolve(scope); + } } - } + _ => {} + }; + + // Fallback to text serialization + let mut result = String::new(); + self.write(&mut result, scope).expect("Failed to write"); + result.into() } } diff --git a/fluent-bundle/src/resolver/scope.rs b/fluent-bundle/src/resolver/scope.rs index 1ddff1a4..f38cb410 100644 --- a/fluent-bundle/src/resolver/scope.rs +++ b/fluent-bundle/src/resolver/scope.rs @@ -2,7 +2,7 @@ use crate::bundle::FluentBundle; use crate::memoizer::MemoizerKind; use crate::resolver::{ResolveValue, ResolverError, WriteValue}; use crate::types::FluentValue; -use crate::{FluentArgs, FluentError, FluentResource}; +use crate::{FluentArgs, FluentError, FluentFunctionScope, FluentMessage, FluentResource}; use fluent_syntax::ast; use std::borrow::Borrow; use std::fmt; @@ -138,3 +138,30 @@ impl<'bundle, 'ast, 'args, 'errors, R, M> Scope<'bundle, 'ast, 'args, 'errors, R } } } + +impl<'bundle, 'ast, 'args, 'errors, R, M> FluentFunctionScope<'bundle> + for Scope<'bundle, 'ast, 'args, 'errors, R, M> +where + R: Borrow, + M: MemoizerKind, +{ + fn get_message(&self, id: &str) -> Option> { + self.bundle.get_message(id) + } + + fn format_message( + &mut self, + pattern: &'bundle ast::Pattern<&'bundle str>, + mut args: Option>, + ) -> FluentValue<'bundle> { + // Setup scope + std::mem::swap(&mut self.local_args, &mut args); + + let value = pattern.resolve(self); + + // Restore scope + std::mem::swap(&mut self.local_args, &mut args); + + value + } +} diff --git a/fluent-bundle/tests/function.rs b/fluent-bundle/tests/function.rs index 1d403e2f..a98c0dbb 100644 --- a/fluent-bundle/tests/function.rs +++ b/fluent-bundle/tests/function.rs @@ -102,3 +102,141 @@ liked-count2 = { NUMBER($num) -> let value = bundle.format_pattern(pattern, Some(&args), &mut errors); assert_eq!("One person liked your message", &value); } + +#[test] +fn test_extended_function() { + struct ManualMessageReference; + + impl fluent_bundle::FluentFunctionObject for ManualMessageReference { + fn call<'bundle>( + &self, + scope: &mut dyn fluent_bundle::FluentFunctionScope<'bundle>, + positional: &[FluentValue<'bundle>], + named: &FluentArgs<'bundle>, + ) -> FluentValue<'bundle> { + let Some(FluentValue::String(name)) = positional.first().cloned() else { + return FluentValue::Error; + }; + + let Some(msg) = scope.get_message(&name) else { + return FluentValue::Error; + }; + + let pattern = if let Some(FluentValue::String(attribute)) = positional.get(1) { + let Some(pattern) = msg.get_attribute(attribute) else { + return FluentValue::Error; + }; + Some(pattern.value()) + } else { + msg.value() + }; + + let Some(pattern) = pattern else { + return FluentValue::Error; + }; + + scope.format_message(pattern, Some(named.clone())) + } + } + + // Create bundle + let ftl_string = String::from( + r#" +hero-1 = Aurora + .gender = feminine + +hero-2 = Rick + .gender = masculine + +creature-horse = { $count -> + *[one] a horse + [other] { $count } horses +} + +creature-rabbit = { $count -> + *[one] a rabbit + [other] { $count } rabbits +} + +annotation = Beautiful! { MSGREF($creature, count: $count) } + +hero-owns-creature = + { MSGREF($hero) } arrived! + { MSGREF($hero, "gender") -> + [feminine] She owns + [masculine] He owns + *[other] They own + } + { MSGREF($creature, count: $count) } + +"#, + ); + + let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string."); + let mut bundle = FluentBundle::default(); + + bundle + .add_function("NUMBER", |positional, named| match positional.first() { + Some(FluentValue::Number(n)) => { + let mut num = n.clone(); + num.options.merge(named); + + FluentValue::Number(num) + } + _ => FluentValue::Error, + }) + .expect("Failed to add a function."); + + bundle + .add_function_with_scope("MSGREF", ManualMessageReference) + .expect("Failed to add a function"); + + bundle + .add_resource(res) + .expect("Failed to add FTL resources to the bundle."); + + // Examples with passing message reference to a function + let mut args = FluentArgs::new(); + args.set("creature", FluentValue::from("creature-horse")); + args.set("count", FluentValue::from(1)); + + let msg = bundle + .get_message("annotation") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value().expect("Message has no value."); + let value = bundle.format_pattern(pattern, Some(&args), &mut errors); + assert_eq!("Beautiful! \u{2068}a horse\u{2069}", &value); + + let mut args = FluentArgs::new(); + args.set("creature", FluentValue::from("creature-rabbit")); + args.set("count", FluentValue::from(5)); + + let msg = bundle + .get_message("annotation") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value().expect("Message has no value."); + let value = bundle.format_pattern(pattern, Some(&args), &mut errors); + assert_eq!( + "Beautiful! \u{2068}\u{2068}5\u{2069} rabbits\u{2069}", + &value + ); + + // Example with accessing message attributes + let mut args = FluentArgs::new(); + args.set("hero", FluentValue::from("hero-2")); + args.set("creature", FluentValue::from("creature-rabbit")); + args.set("count", FluentValue::from(3)); + + let msg = bundle + .get_message("hero-owns-creature") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value().expect("Message has no value."); + let value = bundle.format_pattern(pattern, Some(&args), &mut errors); + assert_eq!( + "\u{2068}Rick\u{2069} arrived! \n\u{2068}He owns\u{2069}\n\u{2068}\u{2068}3\u{2069} rabbits\u{2069}", + &value + ); +} diff --git a/fluent-bundle/tests/terms-references-with-arguments.rs b/fluent-bundle/tests/terms-references-with-arguments.rs new file mode 100644 index 00000000..6411e6f2 --- /dev/null +++ b/fluent-bundle/tests/terms-references-with-arguments.rs @@ -0,0 +1,73 @@ +use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue}; + +#[test] +fn test_term_argument_resolve() { + // 1. Create bundle + let ftl_string = String::from( + " +-liked-count = { $count -> + [0] No likes yet. + [one] One person liked your message + *[other] { $count } people liked your message +} + +annotation = Beautiful! { -liked-count(count: $num) } + ", + ); + + let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string."); + let mut bundle = FluentBundle::default(); + + bundle + .add_function("NUMBER", |positional, named| match positional.first() { + Some(FluentValue::Number(n)) => { + let mut num = n.clone(); + num.options.merge(named); + + FluentValue::Number(num) + } + _ => FluentValue::Error, + }) + .expect("Failed to add a function."); + + bundle + .add_resource(res) + .expect("Failed to add FTL resources to the bundle."); + + // 1. Example with passing custom argument to term + let mut args = FluentArgs::new(); + args.set("num", FluentValue::from(1)); + + let msg = bundle + .get_message("annotation") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value().expect("Message has no value."); + let value = bundle.format_pattern(pattern, Some(&args), &mut errors); + assert_eq!("Beautiful! One person liked your message", &value); + + let mut args = FluentArgs::new(); + args.set("num", FluentValue::from(5)); + + let msg = bundle + .get_message("annotation") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value().expect("Message has no value."); + let value = bundle.format_pattern(pattern, Some(&args), &mut errors); + assert_eq!( + "Beautiful! \u{2068}5\u{2069} people liked your message", + &value + ); + + let mut args = FluentArgs::new(); + args.set("num", FluentValue::from(0)); + + let msg = bundle + .get_message("annotation") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value().expect("Message has no value."); + let value = bundle.format_pattern(pattern, Some(&args), &mut errors); + assert_eq!("Beautiful! No likes yet.", &value); +} diff --git a/fluent-syntax/src/parser/expression.rs b/fluent-syntax/src/parser/expression.rs index c5ccb32b..ff6dfd9b 100644 --- a/fluent-syntax/src/parser/expression.rs +++ b/fluent-syntax/src/parser/expression.rs @@ -190,7 +190,7 @@ where } self.ptr += 1; self.skip_blank(); - let val = self.get_inline_expression(true)?; + let val = self.get_inline_expression(false)?; argument_names.push(id.name.clone()); named.push(ast::NamedArgument {