Skip to content

Commit

Permalink
Merge 3faecfa into c91d9f4
Browse files Browse the repository at this point in the history
  • Loading branch information
gregtatum committed Oct 27, 2022
2 parents c91d9f4 + 3faecfa commit 795b821
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 25 deletions.
19 changes: 16 additions & 3 deletions fluent-bundle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ use std::iter::FromIterator;

use crate::types::FluentValue;

/// A map of arguments passed from the code to
/// the localization to be used for message
/// formatting.
/// Fluent messages can use arguments in order to programmatically add values to a
/// translated string. For instance, in a localized application you may wish to display
/// a user's email count. This could be done with the following message.
///
/// `msg-key = Hello, { $user }. You have { $emailCount } messages.`
///
/// Here `$user` and `$emailCount` are the arguments, which can be filled with values.
///
/// The [`FluentArgs`] struct is the map from the argument name (for example `$user`) to
/// the argument value (for example "John".) The logic to apply these to write these
/// to messages is elsewhere, this struct just stores the value.
///
/// # Example
///
Expand Down Expand Up @@ -48,14 +56,17 @@ use crate::types::FluentValue;
pub struct FluentArgs<'args>(Vec<(Cow<'args, str>, FluentValue<'args>)>);

impl<'args> FluentArgs<'args> {
/// Creates a new empty list.
pub fn new() -> Self {
Self::default()
}

/// Pre-allocates capacity for arguments.
pub fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}

/// Gets the [`FluentValue`] at the `key` if it exists.
pub fn get<K>(&self, key: K) -> Option<&FluentValue<'args>>
where
K: Into<Cow<'args, str>>,
Expand All @@ -68,6 +79,7 @@ impl<'args> FluentArgs<'args> {
}
}

/// Sets the key value pair.
pub fn set<K, V>(&mut self, key: K, value: V)
where
K: Into<Cow<'args, str>>,
Expand All @@ -81,6 +93,7 @@ impl<'args> FluentArgs<'args> {
self.0.insert(idx, (key, value.into()));
}

/// Iterate over a tuple of the key an [`FluentValue`].
pub fn iter(&self) -> impl Iterator<Item = (&str, &FluentValue)> {
self.0.iter().map(|(k, v)| (k.as_ref(), v))
}
Expand Down
29 changes: 20 additions & 9 deletions fluent-bundle/src/entry.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! `Entry` is used to store Messages, Terms and Functions in `FluentBundle` instances.
//! `Entry` is used to store the lookup information for Messages, Terms and Functions in
//! `FluentBundle` instances.

use std::borrow::Borrow;

Expand All @@ -12,24 +13,34 @@ use crate::types::FluentValue;
pub type FluentFunction =
Box<dyn for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Send + Sync>;

type ResourceIdx = usize;
type EntryIdx = usize;

/// The [`Entry`] stores indexes into the [`FluentBundle`]'s resources for Messages and Terms,
/// and owns the [`Box`] pointers to the [`FluentFunction`].
pub enum Entry {
Message((usize, usize)),
Term((usize, usize)),
Message((ResourceIdx, EntryIdx)),
Term((ResourceIdx, EntryIdx)),
Function(FluentFunction),
}

pub trait GetEntry {
/// Looks up a message by its string ID, and returns it if it exists.
fn get_entry_message(&self, id: &str) -> Option<&ast::Message<&str>>;

/// Looks up a term by its string ID, and returns it if it exists.
fn get_entry_term(&self, id: &str) -> Option<&ast::Term<&str>>;

/// Looks up a function by its string ID, and returns it if it exists.
fn get_entry_function(&self, id: &str) -> Option<&FluentFunction>;
}

impl<'bundle, R: Borrow<FluentResource>, M> GetEntry for FluentBundle<R, M> {
fn get_entry_message(&self, id: &str) -> Option<&ast::Message<&str>> {
self.entries.get(id).and_then(|ref entry| match entry {
Entry::Message(pos) => {
let res = self.resources.get(pos.0)?.borrow();
if let ast::Entry::Message(ref msg) = res.get_entry(pos.1)? {
Entry::Message((resource_idx, entry_idx)) => {
let res = self.resources.get(*resource_idx)?.borrow();
if let ast::Entry::Message(ref msg) = res.get_entry(*entry_idx)? {
Some(msg)
} else {
None
Expand All @@ -41,9 +52,9 @@ impl<'bundle, R: Borrow<FluentResource>, M> GetEntry for FluentBundle<R, M> {

fn get_entry_term(&self, id: &str) -> Option<&ast::Term<&str>> {
self.entries.get(id).and_then(|ref entry| match entry {
Entry::Term(pos) => {
let res = self.resources.get(pos.0)?.borrow();
if let ast::Entry::Term(ref msg) = res.get_entry(pos.1)? {
Entry::Term((resource_idx, entry_idx)) => {
let res = self.resources.get(*resource_idx)?.borrow();
if let ast::Entry::Term(ref msg) = res.get_entry(*entry_idx)? {
Some(msg)
} else {
None
Expand Down
14 changes: 13 additions & 1 deletion fluent-bundle/src/memoizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
/// 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<I, R, U>(&self, args: I::Args, callback: U) -> Result<R, I::Error>
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>;
}
5 changes: 5 additions & 0 deletions fluent-bundle/src/resolver/errors.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use fluent_syntax::ast::InlineExpression;
use std::error::Error;

/// Maps an [`InlineExpression`] into the kind of reference, with owned strings
/// that identify the expression. This makes it so that the [`InlineExpression`] can
/// be used to generate an error string.
#[derive(Debug, PartialEq, Clone)]
pub enum ReferenceKind {
Function {
Expand Down Expand Up @@ -44,6 +47,8 @@ where
}
}

/// Errors generated during the process of resolving a fluent message into a string.
/// This process takes place in the `write` method of the `WriteValue` trait.
#[derive(Debug, PartialEq, Clone)]
pub enum ResolverError {
Reference(ReferenceKind),
Expand Down
12 changes: 11 additions & 1 deletion fluent-bundle/src/resolver/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
//! The `resolver` module contains the definitions and implementations for the internal
//! `ResolveValue` and `WriteValue` traits. The former converts AST nodes to a
//! [`FluentValue`], and the latter converts them to a string that is written to an
//! implementor of the [`std::fmt::Write`] trait.

pub mod errors;
mod expression;
mod inline_expression;
Expand All @@ -14,8 +19,9 @@ use crate::memoizer::MemoizerKind;
use crate::resource::FluentResource;
use crate::types::FluentValue;

// Converts an AST node to a `FluentValue`.
/// Resolves an AST node to a [`FluentValue`].
pub(crate) trait ResolveValue {
/// Resolves an AST node to a [`FluentValue`].
fn resolve<'source, 'errors, R, M>(
&'source self,
scope: &mut Scope<'source, 'errors, R, M>,
Expand All @@ -25,7 +31,9 @@ pub(crate) trait ResolveValue {
M: MemoizerKind;
}

/// Resolves and AST node to a string that is written to source `W`.
pub(crate) trait WriteValue {
/// Resolves and AST node to a string that is written to source `W`.
fn write<'source, 'errors, W, R, M>(
&'source self,
w: &mut W,
Expand All @@ -36,6 +44,8 @@ pub(crate) trait WriteValue {
R: Borrow<FluentResource>,
M: MemoizerKind;

/// Writes error information to `W`. This can be used to add FTL errors inline
/// to a message.
fn write_error<W>(&self, _w: &mut W) -> fmt::Result
where
W: fmt::Write;
Expand Down
11 changes: 5 additions & 6 deletions fluent-bundle/src/resolver/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,11 @@ impl<'scope, 'errors, R, M> Scope<'scope, 'errors, R, M> {
}
}

// This method allows us to lazily add Pattern on the stack,
// only if the Pattern::resolve has been called on an empty stack.
//
// This is the case when pattern is called from Bundle and it
// allows us to fast-path simple resolutions, and only use the stack
// for placeables.
/// This method allows us to lazily add Pattern on the stack, only if the
/// Pattern::resolve has been called on an empty stack.
///
/// This is the case when pattern is called from Bundle and it allows us to fast-path
/// simple resolutions, and only use the stack for placeables.
pub fn maybe_track<W>(
&mut self,
w: &mut W,
Expand Down
81 changes: 76 additions & 5 deletions fluent-bundle/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn FluentType + Send>;

/// 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,
Expand Down Expand Up @@ -101,15 +111,72 @@ impl<'s> Clone for FluentValue<'s> {
}

impl<'source> FluentValue<'source> {
pub fn try_number<S: ToString>(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<S: ToString>(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<FluentResource> = 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<R: Borrow<FluentResource>, M>(
&self,
other: &FluentValue,
Expand All @@ -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
Expand All @@ -144,6 +213,7 @@ impl<'source> FluentValue<'source> {
}
}

/// Write out a string version of the [`FluentValue`] to `W`.
pub fn write<W, R, M>(&self, w: &mut W, scope: &Scope<R, M>) -> fmt::Result
where
W: fmt::Write,
Expand All @@ -164,6 +234,7 @@ impl<'source> FluentValue<'source> {
}
}

/// Converts the [`FluentValue`] to a string.
pub fn as_string<R: Borrow<FluentResource>, M>(&self, scope: &Scope<R, M>) -> Cow<'source, str>
where
M: MemoizerKind,
Expand Down
13 changes: 13 additions & 0 deletions fluent-syntax/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1438,9 +1438,22 @@ pub enum InlineExpression<S> {
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum Expression<S> {
/// A select expression such as:
/// ```ftl
/// key = { $var ->
/// [key1] Value 1
/// *[other] Value 2
/// }
/// ```
Select {
selector: InlineExpression<S>,
variants: Vec<Variant<S>>,
},

/// An inline expression such as `${ username }`:
///
/// ```ftl
/// hello-user = Hello ${ username }
/// ```
Inline(InlineExpression<S>),
}

0 comments on commit 795b821

Please sign in to comment.