diff --git a/fluent-bundle/src/builtins.rs b/fluent-bundle/src/builtins.rs new file mode 100644 index 00000000..5a248f80 --- /dev/null +++ b/fluent-bundle/src/builtins.rs @@ -0,0 +1,14 @@ +use crate::{FluentArgs, FluentValue}; + +#[allow(non_snake_case)] +pub fn NUMBER<'a>(positional: &[FluentValue<'a>], named: &FluentArgs) -> FluentValue<'a> { + let Some(FluentValue::Number(n)) = positional.first() else { + return FluentValue::Error; + }; + + let mut n = n.clone(); + n.options.merge(named); + println!("{named:?} => {n:?}"); + + FluentValue::Number(n) +} diff --git a/fluent-bundle/src/bundle.rs b/fluent-bundle/src/bundle.rs index d198004d..41a00e24 100644 --- a/fluent-bundle/src/bundle.rs +++ b/fluent-bundle/src/bundle.rs @@ -547,6 +547,61 @@ impl FluentBundle { }), } } + + /// Adds the builtin functions described in the [FTL syntax guide] to the bundle, making them + /// available in messages. + /// + /// # Examples + /// + /// ``` + /// use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue}; + /// use unic_langid::langid; + /// + /// let ftl_string = String::from(r#"rank = { NUMBER($n, type: "ordinal") -> + /// [1] first + /// [2] second + /// [3] third + /// [one] {$n}st + /// [two] {$n}nd + /// [few] {$n}rd + /// *[other] {$n}th + /// }"#); + /// let resource = FluentResource::try_new(ftl_string) + /// .expect("Could not parse an FTL string."); + /// let langid_en = langid!("en-US"); + /// let mut bundle = FluentBundle::new(vec![langid_en]); + /// bundle.add_resource(&resource) + /// .expect("Failed to add FTL resources to the bundle."); + /// + /// // Register the builtin functions (including NUMBER()) + /// bundle.add_builtins().expect("Failed to add builtins to the bundle."); + /// + /// let msg = bundle.get_message("rank").expect("Message doesn't exist."); + /// let mut errors = vec![]; + /// let pattern = msg.value().expect("Message has no value."); + /// + /// let mut args = FluentArgs::new(); + /// + /// args.set("n", 5); + /// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + /// assert_eq!(&value, "\u{2068}5\u{2069}th"); + /// + /// args.set("n", 12); + /// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + /// assert_eq!(&value, "\u{2068}12\u{2069}th"); + /// + /// args.set("n", 22); + /// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + /// assert_eq!(&value, "\u{2068}22\u{2069}nd"); + /// ``` + /// + /// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html + pub fn add_builtins(&mut self) -> Result<(), FluentError> { + self.add_function("NUMBER", crate::builtins::NUMBER)?; + // TODO: DATETIME() + + Ok(()) + } } impl Default for FluentBundle { diff --git a/fluent-bundle/src/lib.rs b/fluent-bundle/src/lib.rs index 4e180aec..93d7ea53 100644 --- a/fluent-bundle/src/lib.rs +++ b/fluent-bundle/src/lib.rs @@ -99,6 +99,7 @@ //! the `fluent-bundle` crate directly, while the ecosystem //! matures and higher level APIs are being developed. mod args; +pub mod builtins; pub mod bundle; pub mod concurrent; mod entry; diff --git a/fluent-bundle/src/types/mod.rs b/fluent-bundle/src/types/mod.rs index 24ec8c08..585e90b6 100644 --- a/fluent-bundle/src/types/mod.rs +++ b/fluent-bundle/src/types/mod.rs @@ -199,13 +199,16 @@ impl<'source> FluentValue<'source> { }; // This string matches a plural rule keyword. Check if the number // matches the plural rule category. + let r#type = match b.options.r#type { + FluentNumberType::Cardinal => PluralRuleType::CARDINAL, + FluentNumberType::Ordinal => PluralRuleType::ORDINAL, + }; scope .bundle .intls - .with_try_get_threadsafe::( - (PluralRuleType::CARDINAL,), - |pr| pr.0.select(b) == Ok(cat), - ) + .with_try_get_threadsafe::((r#type,), |pr| { + pr.0.select(b) == Ok(cat) + }) .unwrap() } _ => false, diff --git a/fluent-bundle/src/types/number.rs b/fluent-bundle/src/types/number.rs index b9699279..b9c3b2de 100644 --- a/fluent-bundle/src/types/number.rs +++ b/fluent-bundle/src/types/number.rs @@ -8,6 +8,23 @@ use intl_pluralrules::operands::PluralOperands; use crate::args::FluentArgs; use crate::types::FluentValue; +#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)] +pub enum FluentNumberType { + #[default] + Cardinal, + Ordinal, +} + +impl From<&str> for FluentNumberType { + fn from(input: &str) -> Self { + match input { + "cardinal" => Self::Cardinal, + "ordinal" => Self::Ordinal, + _ => Self::default(), + } + } +} + #[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq)] pub enum FluentNumberStyle { #[default] @@ -48,6 +65,7 @@ impl From<&str> for FluentNumberCurrencyDisplayStyle { #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct FluentNumberOptions { + pub r#type: FluentNumberType, pub style: FluentNumberStyle, pub currency: Option, pub currency_display: FluentNumberCurrencyDisplayStyle, @@ -62,6 +80,7 @@ pub struct FluentNumberOptions { impl Default for FluentNumberOptions { fn default() -> Self { Self { + r#type: Default::default(), style: Default::default(), currency: None, currency_display: Default::default(), @@ -79,6 +98,9 @@ impl FluentNumberOptions { pub fn merge(&mut self, opts: &FluentArgs) { for (key, value) in opts.iter() { match (key, value) { + ("type", FluentValue::String(n)) => { + self.r#type = n.as_ref().into(); + } ("style", FluentValue::String(n)) => { self.style = n.as_ref().into(); } diff --git a/fluent-bundle/tests/builtins.rs b/fluent-bundle/tests/builtins.rs new file mode 100644 index 00000000..092c94ac --- /dev/null +++ b/fluent-bundle/tests/builtins.rs @@ -0,0 +1,67 @@ +use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue}; +use fluent_syntax::ast::Pattern; + +#[test] +fn test_builtin_number() { + // 1. Create bundle + let ftl_string = String::from( + r#" +count = { NUMBER($num, type: "cardinal") -> + *[other] A + [one] B +} +order = { NUMBER($num, type: "ordinal") -> + *[other] {$num}th + [one] {$num}st + [two] {$num}nd + [few] {$num}rd +} + "#, + ); + + let mut bundle = FluentBundle::default(); + bundle + .add_resource(FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.")) + .expect("Failed to add FTL resources to the bundle."); + bundle + .add_builtins() + .expect("Failed to add builtin functions to the bundle."); + + let get_val = |pattern: &Pattern<&'_ str>, num: isize| { + let mut args = FluentArgs::new(); + args.set("num", FluentValue::from(num)); + let mut errors = vec![]; + let val = bundle.format_pattern(pattern, Some(&args), &mut errors); + if errors.is_empty() { + Ok(val.into_owned()) + } else { + Err(errors) + } + }; + + let count = bundle + .get_message("count") + .expect("Message doesn't exist") + .value() + .expect("Message has no value"); + + assert_eq!(get_val(count, 0).unwrap(), "A"); + assert_eq!(get_val(count, 1).unwrap(), "B"); + assert_eq!(get_val(count, 2).unwrap(), "A"); + assert_eq!(get_val(count, 12).unwrap(), "A"); + assert_eq!(get_val(count, 15).unwrap(), "A"); + assert_eq!(get_val(count, 123).unwrap(), "A"); + + let order = bundle + .get_message("order") + .expect("Message doesn't exist") + .value() + .expect("Message has no value"); + + assert_eq!(get_val(order, 0).unwrap(), "\u{2068}0\u{2069}th"); + assert_eq!(get_val(order, 1).unwrap(), "\u{2068}1\u{2069}st"); + assert_eq!(get_val(order, 2).unwrap(), "\u{2068}2\u{2069}nd"); + assert_eq!(get_val(order, 12).unwrap(), "\u{2068}12\u{2069}th"); + assert_eq!(get_val(order, 15).unwrap(), "\u{2068}15\u{2069}th"); + assert_eq!(get_val(order, 123).unwrap(), "\u{2068}123\u{2069}rd"); +}