diff --git a/fluent-bundle/src/memoizer.rs b/fluent-bundle/src/memoizer.rs index c738a857..1d6d4918 100644 --- a/fluent-bundle/src/memoizer.rs +++ b/fluent-bundle/src/memoizer.rs @@ -2,17 +2,29 @@ use crate::types::FluentType; use intl_memoizer::Memoizable; use unic_langid::LanguageIdentifier; +/// This trait contains thread-safe methods which extend [intl_memoizer::IntlLangMemoizer]. +/// It is used as the generic bound in this crate when a memoizer is needed. pub trait MemoizerKind: 'static { fn new(lang: LanguageIdentifier) -> Self where Self: Sized; - fn with_try_get_threadsafe(&self, args: I::Args, cb: U) -> Result + /// A threadsafe variant of `with_try_get` from [intl_memoizer::IntlLangMemoizer]. + /// The generics enforce that `Self` and its arguments are actually threadsafe. + /// + /// `I` - The [Memoizable](intl_memoizer::Memoizable) internationalization formatter. + /// + /// `R` - The result from the format operation. + /// + /// `U` - The callback that accepts the instance of the intl formatter, and generates + /// some kind of results `R`. + fn with_try_get_threadsafe(&self, args: I::Args, callback: U) -> Result where Self: Sized, I: Memoizable + Send + Sync + 'static, I::Args: Send + Sync + 'static, U: FnOnce(&I) -> R; + /// Wires up the `as_string` or `as_string_threadsafe` variants for [`FluentType`]. fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str>; } diff --git a/fluent-bundle/src/types/mod.rs b/fluent-bundle/src/types/mod.rs index 6729a8b9..8d011c93 100644 --- a/fluent-bundle/src/types/mod.rs +++ b/fluent-bundle/src/types/mod.rs @@ -28,9 +28,19 @@ use crate::memoizer::MemoizerKind; use crate::resolver::Scope; use crate::resource::FluentResource; +/// Custom types can implement the [`FluentType`] trait in order to generate a string +/// value for use in the message generation process. pub trait FluentType: fmt::Debug + AnyEq + 'static { + /// Create a clone of the underlying type. fn duplicate(&self) -> Box; + + /// Convert the custom type into a string value, for instance a custom DateTime + /// type could return "Oct. 27, 2022". fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str>; + + /// Convert the custom type into a string value, for instance a custom DateTime + /// type could return "Oct. 27, 2022". This operation is provided the threadsafe + /// [IntlLangMemoizer](intl_memoizer::concurrent::IntlLangMemoizer). fn as_string_threadsafe( &self, intls: &intl_memoizer::concurrent::IntlLangMemoizer, @@ -101,15 +111,72 @@ impl<'s> Clone for FluentValue<'s> { } impl<'source> FluentValue<'source> { - pub fn try_number(v: S) -> Self { - let s = v.to_string(); - if let Ok(num) = FluentNumber::from_str(&s) { - num.into() + /// Attempts to parse the string representation of a `value` that supports + /// [`ToString`] into a [`FluentValue::Number`]. If it fails, it will instead + /// convert it to a [`FluentValue::String`]. + /// + /// ``` + /// use fluent_bundle::types::{FluentNumber, FluentNumberOptions, FluentValue}; + /// + /// // "2" parses into a `FluentNumber` + /// assert_eq!( + /// FluentValue::try_number("2"), + /// FluentValue::Number(FluentNumber::new(2.0, FluentNumberOptions::default())) + /// ); + /// + /// // Floats can be parsed as well. + /// assert_eq!( + /// FluentValue::try_number("3.141569"), + /// FluentValue::Number(FluentNumber::new( + /// 3.141569, + /// FluentNumberOptions { + /// minimum_fraction_digits: Some(6), + /// ..Default::default() + /// } + /// )) + /// ); + /// + /// // When a value is not a valid number, it falls back to a `FluentValue::String` + /// assert_eq!( + /// FluentValue::try_number("A string"), + /// FluentValue::String("A string".into()) + /// ); + /// ``` + pub fn try_number(value: S) -> Self { + let string = value.to_string(); + if let Ok(number) = FluentNumber::from_str(&string) { + number.into() } else { - s.into() + string.into() } } + /// Checks to see if two [`FluentValues`](FluentValue) match each other by having the + /// same type and contents. The special exception is in the case of a string being + /// compared to a number. Here attempt to check that the plural rule category matches. + /// + /// ``` + /// use fluent_bundle::resolver::Scope; + /// use fluent_bundle::{types::FluentValue, FluentBundle, FluentResource}; + /// use unic_langid::langid; + /// + /// let langid_ars = langid!("en"); + /// let bundle: FluentBundle = FluentBundle::new(vec![langid_ars]); + /// let scope = Scope::new(&bundle, None, None); + /// + /// // Matching examples: + /// assert!(FluentValue::try_number("2").matches(&FluentValue::try_number("2"), &scope)); + /// assert!(FluentValue::from("fluent").matches(&FluentValue::from("fluent"), &scope)); + /// assert!( + /// FluentValue::from("one").matches(&FluentValue::try_number("1"), &scope), + /// "Plural rules are matched." + /// ); + /// + /// // Non-matching examples: + /// assert!(!FluentValue::try_number("2").matches(&FluentValue::try_number("3"), &scope)); + /// assert!(!FluentValue::from("fluent").matches(&FluentValue::from("not fluent"), &scope)); + /// assert!(!FluentValue::from("two").matches(&FluentValue::try_number("100"), &scope),); + /// ``` pub fn matches, M>( &self, other: &FluentValue, @@ -131,6 +198,8 @@ impl<'source> FluentValue<'source> { "other" => PluralCategory::OTHER, _ => return false, }; + // This string matches a plural rule keyword. Check if the number + // matches the plural rule category. scope .bundle .intls @@ -144,6 +213,7 @@ impl<'source> FluentValue<'source> { } } + /// Write out a string version of the [`FluentValue`] to `W`. pub fn write(&self, w: &mut W, scope: &Scope) -> fmt::Result where W: fmt::Write, @@ -164,6 +234,7 @@ impl<'source> FluentValue<'source> { } } + /// Converts the [`FluentValue`] to a string. pub fn as_string, M>(&self, scope: &Scope) -> Cow<'source, str> where M: MemoizerKind, diff --git a/fluent-syntax/src/ast/mod.rs b/fluent-syntax/src/ast/mod.rs index 5b79bb3e..fa17c1bf 100644 --- a/fluent-syntax/src/ast/mod.rs +++ b/fluent-syntax/src/ast/mod.rs @@ -1438,9 +1438,22 @@ pub enum InlineExpression { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] pub enum Expression { + /// A select expression such as: + /// ```ftl + /// key = { $var -> + /// [key1] Value 1 + /// *[other] Value 2 + /// } + /// ``` Select { selector: InlineExpression, variants: Vec>, }, + + /// An inline expression such as `${ username }`: + /// + /// ```ftl + /// hello-user = Hello ${ username } + /// ``` Inline(InlineExpression), }