Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
use std::borrow::Cow;
use std::iter::FromIterator;
use crate::types::FluentValue;
/// 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
///
/// ```
/// use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
///
/// let mut args = FluentArgs::new();
/// args.set("user", "John");
/// args.set("emailCount", 5);
///
/// let res = FluentResource::try_new(r#"
///
/// msg-key = Hello, { $user }. You have { $emailCount } messages.
///
/// "#.to_string())
/// .expect("Failed to parse FTL.");
///
/// let mut bundle = FluentBundle::default();
///
/// // For this example, we'll turn on BiDi support.
/// // Please, be careful when doing it, it's a risky move.
/// bundle.set_use_isolating(false);
///
/// bundle.add_resource(res)
/// .expect("Failed to add a resource.");
///
/// let mut err = vec![];
///
/// let msg = bundle.get_message("msg-key")
/// .expect("Failed to retrieve a message.");
/// let value = msg.value()
/// .expect("Failed to retrieve a value.");
///
/// assert_eq!(
/// bundle.format_pattern(value, Some(&args), &mut err),
/// "Hello, John. You have 5 messages."
/// );
/// ```
#[derive(Debug, Default)]
pub struct FluentArgs<'args>(Vec<(Cow<'args, str>, FluentValue<'args>)>);
impl<'args> FluentArgs<'args> {
/// Creates a new empty argument map.
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>>,
{
let key = key.into();
if let Ok(idx) = self.0.binary_search_by_key(&&key, |(k, _)| k) {
Some(&self.0[idx].1)
} else {
None
}
}
/// Sets the key value pair.
pub fn set<K, V>(&mut self, key: K, value: V)
where
K: Into<Cow<'args, str>>,
V: Into<FluentValue<'args>>,
{
let key = key.into();
match self.0.binary_search_by_key(&&key, |(k, _)| k) {
Ok(idx) => self.0[idx] = (key, value.into()),
Err(idx) => 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))
}
}
impl<'args, K, V> FromIterator<(K, V)> for FluentArgs<'args>
where
K: Into<Cow<'args, str>>,
V: Into<FluentValue<'args>>,
{
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
{
let iter = iter.into_iter();
let mut args = if let Some(size) = iter.size_hint().1 {
FluentArgs::with_capacity(size)
} else {
FluentArgs::new()
};
for (k, v) in iter {
args.set(k, v);
}
args
}
}
impl<'args> IntoIterator for FluentArgs<'args> {
type Item = (Cow<'args, str>, FluentValue<'args>);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replace_existing_arguments() {
let mut args = FluentArgs::new();
args.set("name", "John");
args.set("emailCount", 5);
assert_eq!(args.0.len(), 2);
assert_eq!(
args.get("name"),
Some(&FluentValue::String(Cow::Borrowed("John")))
);
assert_eq!(args.get("emailCount"), Some(&FluentValue::try_number("5")));
args.set("name", "Jane");
args.set("emailCount", 7);
assert_eq!(args.0.len(), 2);
assert_eq!(
args.get("name"),
Some(&FluentValue::String(Cow::Borrowed("Jane")))
);
assert_eq!(args.get("emailCount"), Some(&FluentValue::try_number("7")));
}
}
+13
View File
@@ -0,0 +1,13 @@
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);
FluentValue::Number(n)
}
+672
View File
@@ -0,0 +1,672 @@
//! `FluentBundle` is a collection of localization messages in Fluent.
//!
//! It stores a list of messages in a single locale which can reference one another, use the same
//! internationalization formatters, functions, scopeironmental variables and are expected to be used
//! together.
use rustc_hash::FxHashMap;
use std::borrow::Borrow;
use std::borrow::Cow;
use std::collections::hash_map::Entry as HashEntry;
use std::default::Default;
use std::fmt;
use fluent_syntax::ast;
use intl_memoizer::IntlLangMemoizer;
use unic_langid::LanguageIdentifier;
use crate::args::FluentArgs;
use crate::entry::Entry;
use crate::entry::GetEntry;
use crate::errors::{EntryKind, FluentError};
use crate::memoizer::MemoizerKind;
use crate::message::FluentMessage;
use crate::resolver::{ResolveValue, Scope, WriteValue};
use crate::resource::FluentResource;
use crate::types::FluentValue;
/// 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.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource, FluentValue, FluentArgs};
/// use unic_langid::langid;
///
/// // 1. Create a FluentResource
///
/// let ftl_string = String::from("intro = Welcome, { $name }.");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
///
///
/// // 2. Create a FluentBundle
///
/// let langid_en = langid!("en-US");
/// let mut bundle = FluentBundle::new(vec![langid_en]);
///
///
/// // 3. Add the resource to the bundle
///
/// bundle.add_resource(&resource)
/// .expect("Failed to add FTL resources to the bundle.");
///
///
/// // 4. Retrieve a FluentMessage from the bundle
///
/// let msg = bundle.get_message("intro")
/// .expect("Message doesn't exist.");
///
/// let mut args = FluentArgs::new();
/// args.set("name", "Rustacean");
///
///
/// // 5. Format the value of the message
///
/// let mut errors = vec![];
///
/// let pattern = msg.value()
/// .expect("Message has no value.");
///
/// assert_eq!(
/// bundle.format_pattern(&pattern, Some(&args), &mut errors),
/// // The placeholder is wrapper in Unicode Directionality Marks
/// // to indicate that the placeholder may be of different direction
/// // than surrounding string.
/// "Welcome, \u{2068}Rustacean\u{2069}."
/// );
///
/// ```
///
/// # `FluentBundle` Life Cycle
///
/// ## Create a bundle
///
/// To create a bundle, call [`FluentBundle::new`] with a locale list that represents the best
/// possible fallback chain for a given locale. The simplest case is a one-locale list.
///
/// Fluent uses [`LanguageIdentifier`] which can be created using `langid!` macro.
///
/// ## Add Resources
///
/// Next, call [`add_resource`](FluentBundle::add_resource) one or more times, supplying translations in the FTL syntax.
///
/// Since [`FluentBundle`] is generic over anything that can borrow a [`FluentResource`],
/// one can use [`FluentBundle`] to own its resources, store references to them,
/// or even [`Rc<FluentResource>`](std::rc::Rc) or [`Arc<FluentResource>`](std::sync::Arc).
///
/// The [`FluentBundle`] instance is now ready to be used for localization.
///
/// ## Format
///
/// To format a translation, call [`get_message`](FluentBundle::get_message) to retrieve a [`FluentMessage`],
/// and then call [`format_pattern`](FluentBundle::format_pattern) on the message value or attribute in order to
/// retrieve the translated string.
///
/// The result of [`format_pattern`](FluentBundle::format_pattern) is an
/// [`Cow<str>`](std::borrow::Cow). It is
/// recommended to treat the result as opaque from the perspective of the program and use it only
/// to display localized messages. Do not examine it or alter in any way before displaying. This
/// is a general good practice as far as all internationalization operations are concerned.
///
/// If errors were encountered during formatting, they will be
/// accumulated in the [`Vec<FluentError>`](FluentError) passed as the third argument.
///
/// While they are not fatal, they usually indicate problems with the translation,
/// and should be logged or reported in a way that allows the developer to notice
/// and fix them.
///
///
/// # Locale Fallback Chain
///
/// [`FluentBundle`] stores messages in a single locale, but keeps a locale fallback chain for the
/// purpose of language negotiation with i18n formatters. For instance, if date and time formatting
/// are not available in the first locale, [`FluentBundle`] will use its `locales` fallback chain
/// to negotiate a sensible fallback for date and time formatting.
///
/// # Concurrency
///
/// As you may have noticed, [`fluent_bundle::FluentBundle`](crate::FluentBundle) is a specialization of [`fluent_bundle::bundle::FluentBundle`](crate::bundle::FluentBundle)
/// which works with an [`IntlLangMemoizer`] over [`RefCell`](std::cell::RefCell).
/// In scenarios where the memoizer must work concurrently, there's an implementation of
/// [`IntlLangMemoizer`][concurrent::IntlLangMemoizer] that uses [`Mutex`](std::sync::Mutex) and there's [`FluentBundle::new_concurrent`] which works with that.
///
/// [concurrent::IntlLangMemoizer]: https://docs.rs/intl-memoizer/latest/intl_memoizer/concurrent/struct.IntlLangMemoizer.html
pub struct FluentBundle<R, M> {
pub locales: Vec<LanguageIdentifier>,
pub(crate) resources: Vec<R>,
pub(crate) entries: FxHashMap<String, Entry>,
pub(crate) intls: M,
pub(crate) use_isolating: bool,
pub(crate) transform: Option<fn(&str) -> Cow<str>>,
pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
}
impl<R, M> FluentBundle<R, M> {
/// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
///
/// If any entry in the resource uses the same identifier as an already
/// existing key in the bundle, the new entry will be ignored and a
/// `FluentError::Overriding` will be added to the result.
///
/// The method can take any type that can be borrowed to [`FluentResource`]:
/// - `FluentResource`
/// - `&FluentResource`
/// - `Rc<FluentResource>`
/// - `Arc<FluentResource>`
///
/// This allows the user to introduce custom resource management and share
/// resources between instances of `FluentBundle`.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("
/// hello = Hi!
/// goodbye = Bye!
/// ");
/// 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.");
/// assert_eq!(true, bundle.has_message("hello"));
/// ```
///
/// # Whitespace
///
/// Message ids must have no leading whitespace. Message values that span
/// multiple lines must have leading whitespace on all but the first line. These
/// are standard FTL syntax rules that may prove a bit troublesome in source
/// code formatting. The [`indoc!`] crate can help with stripping extra indentation
/// if you wish to indent your entire message.
///
/// [FTL syntax]: https://projectfluent.org/fluent/guide/
/// [`indoc!`]: https://github.com/dtolnay/indoc
/// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
pub fn add_resource(&mut self, r: R) -> Result<(), Vec<FluentError>>
where
R: Borrow<FluentResource>,
{
let mut errors = vec![];
let res = r.borrow();
let res_pos = self.resources.len();
for (entry_pos, entry) in res.entries().enumerate() {
let (id, entry) = match entry {
ast::Entry::Message(ast::Message { ref id, .. }) => {
(id.name, Entry::Message((res_pos, entry_pos)))
}
ast::Entry::Term(ast::Term { ref id, .. }) => {
(id.name, Entry::Term((res_pos, entry_pos)))
}
_ => continue,
};
match self.entries.entry(id.to_string()) {
HashEntry::Vacant(empty) => {
empty.insert(entry);
}
HashEntry::Occupied(_) => {
let kind = match entry {
Entry::Message(..) => EntryKind::Message,
Entry::Term(..) => EntryKind::Term,
_ => unreachable!(),
};
errors.push(FluentError::Overriding {
kind,
id: id.to_string(),
});
}
}
}
self.resources.push(r);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
///
/// If any entry in the resource uses the same identifier as an already
/// existing key in the bundle, the entry will override the previous one.
///
/// The method can take any type that can be borrowed as [`FluentResource`]:
/// - `FluentResource`
/// - `&FluentResource`
/// - `Rc<FluentResource>`
/// - `Arc<FluentResource>`
///
/// This allows the user to introduce custom resource management and share
/// resources between instances of `FluentBundle`.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("
/// hello = Hi!
/// goodbye = Bye!
/// ");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
///
/// let ftl_string = String::from("
/// hello = Another Hi!
/// ");
/// let resource2 = 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.");
///
/// bundle.add_resource_overriding(resource2);
///
/// let mut errors = vec![];
/// let msg = bundle.get_message("hello")
/// .expect("Failed to retrieve the message");
/// let value = msg.value().expect("Failed to retrieve the value of the message");
/// assert_eq!(bundle.format_pattern(value, None, &mut errors), "Another Hi!");
/// ```
///
/// # Whitespace
///
/// Message ids must have no leading whitespace. Message values that span
/// multiple lines must have leading whitespace on all but the first line. These
/// are standard FTL syntax rules that may prove a bit troublesome in source
/// code formatting. The [`indoc!`] crate can help with stripping extra indentation
/// if you wish to indent your entire message.
///
/// [FTL syntax]: https://projectfluent.org/fluent/guide/
/// [`indoc!`]: https://github.com/dtolnay/indoc
/// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
pub fn add_resource_overriding(&mut self, r: R)
where
R: Borrow<FluentResource>,
{
let res = r.borrow();
let res_pos = self.resources.len();
for (entry_pos, entry) in res.entries().enumerate() {
let (id, entry) = match entry {
ast::Entry::Message(ast::Message { ref id, .. }) => {
(id.name, Entry::Message((res_pos, entry_pos)))
}
ast::Entry::Term(ast::Term { ref id, .. }) => {
(id.name, Entry::Term((res_pos, entry_pos)))
}
_ => continue,
};
self.entries.insert(id.to_string(), entry);
}
self.resources.push(r);
}
/// When formatting patterns, `FluentBundle` inserts
/// Unicode Directionality Isolation Marks to indicate
/// that the direction of a placeable may differ from
/// the surrounding message.
///
/// This is important for cases such as when a
/// right-to-left user name is presented in the
/// left-to-right message.
///
/// In some cases, such as testing, the user may want
/// to disable the isolating.
pub fn set_use_isolating(&mut self, value: bool) {
self.use_isolating = value;
}
/// This method allows to specify a function that will
/// be called on all textual fragments of the pattern
/// during formatting.
///
/// This is currently primarily used for pseudolocalization,
/// and `fluent-pseudo` crate provides a function
/// that can be passed here.
pub fn set_transform(&mut self, func: Option<fn(&str) -> Cow<str>>) {
self.transform = func;
}
/// This method allows to specify a function that will
/// be called before any `FluentValue` is formatted
/// allowing overrides.
///
/// It's particularly useful for plugging in an external
/// formatter for `FluentValue::Number`.
pub fn set_formatter(&mut self, func: Option<fn(&FluentValue, &M) -> Option<String>>) {
self.formatter = func;
}
/// Returns true if this bundle contains a message with the given id.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("hello = Hi!");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Failed to 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.");
/// assert_eq!(true, bundle.has_message("hello"));
///
/// ```
pub fn has_message(&self, id: &str) -> bool
where
R: Borrow<FluentResource>,
{
self.get_entry_message(id).is_some()
}
/// Retrieves a `FluentMessage` from a bundle.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("hello-world = Hello World!");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Failed to 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.");
///
/// let msg = bundle.get_message("hello-world");
/// assert_eq!(msg.is_some(), true);
/// ```
pub fn get_message<'l>(&'l self, id: &str) -> Option<FluentMessage<'l>>
where
R: Borrow<FluentResource>,
{
self.get_entry_message(id).map(Into::into)
}
/// Writes a formatted pattern which comes from a `FluentMessage`.
///
/// # Example
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("hello-world = Hello World!");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Failed to 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.");
///
/// let msg = bundle.get_message("hello-world")
/// .expect("Failed to retrieve a FluentMessage.");
///
/// let pattern = msg.value()
/// .expect("Missing Value.");
/// let mut errors = vec![];
///
/// let mut s = String::new();
/// bundle.write_pattern(&mut s, &pattern, None, &mut errors)
/// .expect("Failed to write.");
///
/// assert_eq!(s, "Hello World!");
/// ```
pub fn write_pattern<'bundle, W>(
&'bundle self,
w: &mut W,
pattern: &'bundle ast::Pattern<&str>,
args: Option<&'bundle FluentArgs>,
errors: &mut Vec<FluentError>,
) -> fmt::Result
where
R: Borrow<FluentResource>,
W: fmt::Write,
M: MemoizerKind,
{
let mut scope = Scope::new(self, args, Some(errors));
pattern.write(w, &mut scope)
}
/// Formats a pattern which comes from a `FluentMessage`.
///
/// # Example
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("hello-world = Hello World!");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Failed to 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.");
///
/// let msg = bundle.get_message("hello-world")
/// .expect("Failed to retrieve a FluentMessage.");
///
/// let pattern = msg.value()
/// .expect("Missing Value.");
/// let mut errors = vec![];
///
/// let result = bundle.format_pattern(&pattern, None, &mut errors);
///
/// assert_eq!(result, "Hello World!");
/// ```
pub fn format_pattern<'bundle>(
&'bundle self,
pattern: &'bundle ast::Pattern<&'bundle str>,
args: Option<&FluentArgs>,
errors: &mut Vec<FluentError>,
) -> Cow<'bundle, str>
where
R: Borrow<FluentResource>,
M: MemoizerKind,
{
let mut scope = Scope::new(self, args, Some(errors));
let value = pattern.resolve(&mut scope);
value.into_string(&scope)
}
/// Makes the provided rust function available to messages with the name `id`. See
/// the [FTL syntax guide] to learn how these are used in messages.
///
/// FTL functions accept both positional and named args. The rust function you
/// provide therefore has two parameters: a slice of values for the positional
/// args, and a `FluentArgs` for named args.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("length = { STRLEN(\"12345\") }");
/// 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 a fn that maps from string to string length
/// bundle.add_function("STRLEN", |positional, _named| match positional {
/// [FluentValue::String(str)] => str.len().into(),
/// _ => FluentValue::Error,
/// }).expect("Failed to add a function to the bundle.");
///
/// let msg = bundle.get_message("length").expect("Message doesn't exist.");
/// let mut errors = vec![];
/// let pattern = msg.value().expect("Message has no value.");
/// let value = bundle.format_pattern(&pattern, None, &mut errors);
/// assert_eq!(&value, "5");
/// ```
///
/// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html
pub fn add_function<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
where
F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static,
{
match self.entries.entry(id.to_owned()) {
HashEntry::Vacant(entry) => {
entry.insert(Entry::Function(Box::new(func)));
Ok(())
}
HashEntry::Occupied(_) => Err(FluentError::Overriding {
kind: EntryKind::Function,
id: id.to_owned(),
}),
}
}
/// 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<R> Default for FluentBundle<R, IntlLangMemoizer> {
fn default() -> Self {
Self::new(vec![LanguageIdentifier::default()])
}
}
impl<R> FluentBundle<R, IntlLangMemoizer> {
/// Constructs a `FluentBundle`. The first element in `locales` should be the
/// language this bundle represents, and will be used to determine the
/// correct plural rules for this bundle. You can optionally provide extra
/// languages in the list; they will be used as fallback date and time
/// formatters if a formatter for the primary language is unavailable.
///
/// # Examples
///
/// ```
/// use fluent_bundle::FluentBundle;
/// use fluent_bundle::FluentResource;
/// use unic_langid::langid;
///
/// let langid_en = langid!("en-US");
/// let mut bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid_en]);
/// ```
///
/// # Errors
///
/// This will panic if no formatters can be found for the locales.
pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
let first_locale = locales.first().cloned().unwrap_or_default();
Self {
locales,
resources: vec![],
entries: FxHashMap::default(),
intls: IntlLangMemoizer::new(first_locale),
use_isolating: true,
transform: None,
formatter: None,
}
}
}
impl crate::memoizer::MemoizerKind for IntlLangMemoizer {
fn new(lang: LanguageIdentifier) -> Self
where
Self: Sized,
{
Self::new(lang)
}
fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
where
Self: Sized,
I: intl_memoizer::Memoizable + Send + Sync + 'static,
I::Args: Send + Sync + 'static,
U: FnOnce(&I) -> R,
{
self.with_try_get(args, cb)
}
fn stringify_value(
&self,
value: &dyn crate::types::FluentType,
) -> std::borrow::Cow<'static, str> {
value.as_string(self)
}
}
+67
View File
@@ -0,0 +1,67 @@
use intl_memoizer::{concurrent::IntlLangMemoizer, Memoizable};
use rustc_hash::FxHashMap;
use unic_langid::LanguageIdentifier;
use crate::memoizer::MemoizerKind;
use crate::types::FluentType;
/// Specialized [`FluentBundle`](crate::bundle::FluentBundle) over
/// concurrent [`IntlLangMemoizer`].
///
/// A concurrent `FluentBundle` can be constructed with the
/// [`FluentBundle::new_concurrent`] method.
///
/// See [`FluentBundle`](crate::FluentBundle) for the non-concurrent specialization.
pub type FluentBundle<R> = crate::bundle::FluentBundle<R, IntlLangMemoizer>;
impl<R> FluentBundle<R> {
/// A constructor analogous to [`FluentBundle::new`] but operating
/// on a concurrent version of [`IntlLangMemoizer`] over [`Mutex`](std::sync::Mutex).
///
/// # Example
///
/// ```
/// use fluent_bundle::concurrent::FluentBundle;
/// use fluent_bundle::FluentResource;
/// use unic_langid::langid;
///
/// let langid_en = langid!("en-US");
/// let mut bundle: FluentBundle<FluentResource> =
/// FluentBundle::new_concurrent(vec![langid_en]);
/// ```
pub fn new_concurrent(locales: Vec<LanguageIdentifier>) -> Self {
let first_locale = locales.first().cloned().unwrap_or_default();
Self {
locales,
resources: vec![],
entries: FxHashMap::default(),
intls: IntlLangMemoizer::new(first_locale),
use_isolating: true,
transform: None,
formatter: None,
}
}
}
impl MemoizerKind for IntlLangMemoizer {
fn new(lang: LanguageIdentifier) -> Self
where
Self: Sized,
{
Self::new(lang)
}
fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
where
Self: Sized,
I: Memoizable + Send + Sync + 'static,
I::Args: Send + Sync + 'static,
U: FnOnce(&I) -> R,
{
self.with_try_get(args, cb)
}
fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str> {
value.as_string_threadsafe(self)
}
}
+73
View File
@@ -0,0 +1,73 @@
//! `Entry` is used to store the lookup information for Messages, Terms and Functions in
//! `FluentBundle` instances.
use std::borrow::Borrow;
use fluent_syntax::ast;
use crate::args::FluentArgs;
use crate::bundle::FluentBundle;
use crate::resource::FluentResource;
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((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<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((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
}
}
_ => None,
})
}
fn get_entry_term(&self, id: &str) -> Option<&ast::Term<&str>> {
self.entries.get(id).and_then(|ref entry| match entry {
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
}
}
_ => None,
})
}
fn get_entry_function(&self, id: &str) -> Option<&FluentFunction> {
self.entries.get(id).and_then(|ref entry| match entry {
Entry::Function(function) => Some(function),
_ => None,
})
}
}
+86
View File
@@ -0,0 +1,86 @@
use crate::resolver::ResolverError;
use fluent_syntax::parser::ParserError;
use std::error::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EntryKind {
Message,
Term,
Function,
}
impl std::fmt::Display for EntryKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Message => f.write_str("message"),
Self::Term => f.write_str("term"),
Self::Function => f.write_str("function"),
}
}
}
/// Core error type for Fluent runtime system.
///
/// It contains three main types of errors that may come up
/// during runtime use of the fluent-bundle crate.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FluentError {
/// An error which occurs when
/// [`FluentBundle::add_resource`](crate::bundle::FluentBundle::add_resource)
/// adds entries that are already registered in a given [`FluentBundle`](crate::FluentBundle).
///
/// # Example
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("intro = Welcome, { $name }.");
/// let res1 = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
///
/// let ftl_string = String::from("intro = Hi, { $name }.");
/// let res2 = 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(&res1)
/// .expect("Failed to add FTL resources to the bundle.");
///
/// assert!(bundle.add_resource(&res2).is_err());
/// ```
Overriding {
kind: EntryKind,
id: String,
},
ParserError(ParserError),
ResolverError(ResolverError),
}
impl std::fmt::Display for FluentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Overriding { kind, id } => {
write!(f, "Attempt to override an existing {}: \"{}\".", kind, id)
}
Self::ParserError(err) => write!(f, "Parser error: {}", err),
Self::ResolverError(err) => write!(f, "Resolver error: {}", err),
}
}
}
impl Error for FluentError {}
impl From<ResolverError> for FluentError {
fn from(error: ResolverError) -> Self {
Self::ResolverError(error)
}
}
impl From<ParserError> for FluentError {
fn from(error: ParserError) -> Self {
Self::ParserError(error)
}
}
+128
View File
@@ -0,0 +1,128 @@
//! Fluent is a modern localization system designed to improve how software is translated.
//!
//! `fluent-bundle` is a mid-level component of the [Fluent Localization
//! System](https://www.projectfluent.org).
//!
//! The crate builds on top of the low level [`fluent-syntax`](https://crates.io/crates/fluent-syntax) package, and provides
//! foundational types and structures required for executing localization at runtime.
//!
//! There are four core concepts to understand Fluent runtime:
//!
//! * [`FluentMessage`] - A single translation unit
//! * [`FluentResource`] - A list of [`FluentMessage`] units
//! * [`FluentBundle`](crate::bundle::FluentBundle) - A collection of [`FluentResource`] lists
//! * [`FluentArgs`] - A list of elements used to resolve a [`FluentMessage`] value
//!
//! ## Example
//!
//! ```
//! use fluent_bundle::{FluentBundle, FluentValue, FluentResource, FluentArgs};
//! // Used to provide a locale for the bundle.
//! use unic_langid::langid;
//!
//! // 1. Crate a FluentResource
//!
//! let ftl_string = r#"
//!
//! hello-world = Hello, world!
//! intro = Welcome, { $name }.
//!
//! "#.to_string();
//!
//! let res = FluentResource::try_new(ftl_string)
//! .expect("Failed to parse an FTL string.");
//!
//!
//! // 2. Crate a FluentBundle
//!
//! let langid_en = langid!("en-US");
//! let mut bundle = FluentBundle::new(vec![langid_en]);
//!
//!
//! // 3. Add the resource to the bundle
//!
//! bundle
//! .add_resource(res)
//! .expect("Failed to add FTL resources to the bundle.");
//!
//!
//! // 4. Retrieve a FluentMessage from the bundle
//!
//! let msg = bundle.get_message("hello-world")
//! .expect("Message doesn't exist.");
//!
//!
//! // 5. Format the value of the simple message
//!
//! let mut errors = vec![];
//!
//! let pattern = msg.value()
//! .expect("Message has no value.");
//!
//! let value = bundle.format_pattern(&pattern, None, &mut errors);
//!
//! assert_eq!(
//! bundle.format_pattern(&pattern, None, &mut errors),
//! "Hello, world!"
//! );
//!
//! // 6. Format the value of the message with arguments
//!
//! let mut args = FluentArgs::new();
//! args.set("name", "John");
//!
//! let msg = bundle.get_message("intro")
//! .expect("Message doesn't exist.");
//!
//! let pattern = msg.value()
//! .expect("Message has no value.");
//!
//! // The FSI/PDI isolation marks ensure that the direction of
//! // the text from the variable is not affected by the translation.
//! assert_eq!(
//! bundle.format_pattern(&pattern, Some(&args), &mut errors),
//! "Welcome, \u{2068}John\u{2069}."
//! );
//! ```
//!
//! # Ergonomics & Higher Level APIs
//!
//! Reading the example, you may notice how verbose it feels.
//! Many core methods are fallible, others accumulate errors, and there
//! are intermediate structures used in operations.
//!
//! This is intentional as it serves as building blocks for variety of different
//! scenarios allowing implementations to handle errors, cache and
//! optimize results.
//!
//! At the moment it is expected that users will use
//! 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;
mod errors;
#[doc(hidden)]
pub mod memoizer;
mod message;
#[doc(hidden)]
pub mod resolver;
mod resource;
pub mod types;
pub use args::FluentArgs;
/// Specialized [`FluentBundle`](crate::bundle::FluentBundle) over
/// non-concurrent [`IntlLangMemoizer`](intl_memoizer::IntlLangMemoizer).
///
/// This is the basic variant of the [`FluentBundle`](crate::bundle::FluentBundle).
///
/// The concurrent specialization can be constructed with
/// [`FluentBundle::new_concurrent`](crate::concurrent::FluentBundle::new_concurrent).
pub type FluentBundle<R> = bundle::FluentBundle<R, intl_memoizer::IntlLangMemoizer>;
pub use errors::FluentError;
pub use message::{FluentAttribute, FluentMessage};
pub use resource::FluentResource;
#[doc(inline)]
pub use types::FluentValue;
+30
View File
@@ -0,0 +1,30 @@
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;
/// 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>;
}
+274
View File
@@ -0,0 +1,274 @@
use fluent_syntax::ast;
/// [`FluentAttribute`] is a component of a compound [`FluentMessage`].
///
/// It represents a key-value pair providing a translation of a component
/// of a user interface widget localized by the given message.
///
/// # Example
///
/// ```
/// use fluent_bundle::{FluentResource, FluentBundle};
///
/// let source = r#"
///
/// confirm-modal = Are you sure?
/// .confirm = Yes
/// .cancel = No
/// .tooltip = Closing the window will lose all unsaved data.
///
/// "#;
///
/// let resource = FluentResource::try_new(source.to_string())
/// .expect("Failed to parse the resource.");
///
/// let mut bundle = FluentBundle::default();
/// bundle.add_resource(resource)
/// .expect("Failed to add a resource.");
///
/// let msg = bundle.get_message("confirm-modal")
/// .expect("Failed to retrieve a message.");
///
/// let mut err = vec![];
///
/// let attributes = msg.attributes().map(|attr| {
/// bundle.format_pattern(attr.value(), None, &mut err)
/// }).collect::<Vec<_>>();
///
/// assert_eq!(attributes[0], "Yes");
/// assert_eq!(attributes[1], "No");
/// assert_eq!(attributes[2], "Closing the window will lose all unsaved data.");
/// ```
#[derive(Debug, PartialEq)]
pub struct FluentAttribute<'m> {
node: &'m ast::Attribute<&'m str>,
}
impl<'m> FluentAttribute<'m> {
/// Retrieves an id of an attribute.
///
/// # Example
///
/// ```
/// # use fluent_bundle::{FluentResource, FluentBundle};
/// # let source = r#"
/// # confirm-modal =
/// # .confirm = Yes
/// # "#;
/// # let resource = FluentResource::try_new(source.to_string())
/// # .expect("Failed to parse the resource.");
/// # let mut bundle = FluentBundle::default();
/// # bundle.add_resource(resource)
/// # .expect("Failed to add a resource.");
/// let msg = bundle.get_message("confirm-modal")
/// .expect("Failed to retrieve a message.");
///
/// let attr1 = msg.attributes().next()
/// .expect("Failed to retrieve an attribute.");
///
/// assert_eq!(attr1.id(), "confirm");
/// ```
pub fn id(&self) -> &'m str {
self.node.id.name
}
/// Retrieves an value of an attribute.
///
/// # Example
///
/// ```
/// # use fluent_bundle::{FluentResource, FluentBundle};
/// # let source = r#"
/// # confirm-modal =
/// # .confirm = Yes
/// # "#;
/// # let resource = FluentResource::try_new(source.to_string())
/// # .expect("Failed to parse the resource.");
/// # let mut bundle = FluentBundle::default();
/// # bundle.add_resource(resource)
/// # .expect("Failed to add a resource.");
/// let msg = bundle.get_message("confirm-modal")
/// .expect("Failed to retrieve a message.");
///
/// let attr1 = msg.attributes().next()
/// .expect("Failed to retrieve an attribute.");
///
/// let mut err = vec![];
///
/// let value = attr1.value();
/// assert_eq!(
/// bundle.format_pattern(value, None, &mut err),
/// "Yes"
/// );
/// ```
pub fn value(&self) -> &'m ast::Pattern<&'m str> {
&self.node.value
}
}
impl<'m> From<&'m ast::Attribute<&'m str>> for FluentAttribute<'m> {
fn from(attr: &'m ast::Attribute<&'m str>) -> Self {
FluentAttribute { node: attr }
}
}
/// [`FluentMessage`] is a basic translation unit of the Fluent system.
///
/// The instance of a message is returned from the
/// [`FluentBundle::get_message`](crate::bundle::FluentBundle::get_message)
/// method, for the lifetime of the [`FluentBundle`](crate::bundle::FluentBundle) instance.
///
/// # Example
///
/// ```
/// use fluent_bundle::{FluentResource, FluentBundle};
///
/// let source = r#"
///
/// hello-world = Hello World!
///
/// "#;
///
/// let resource = FluentResource::try_new(source.to_string())
/// .expect("Failed to parse the resource.");
///
/// let mut bundle = FluentBundle::default();
/// bundle.add_resource(resource)
/// .expect("Failed to add a resource.");
///
/// let msg = bundle.get_message("hello-world")
/// .expect("Failed to retrieve a message.");
///
/// assert!(msg.value().is_some());
/// ```
///
/// That value can be then passed to
/// [`FluentBundle::format_pattern`](crate::bundle::FluentBundle::format_pattern) to be formatted
/// within the context of a given [`FluentBundle`](crate::bundle::FluentBundle) instance.
///
/// # Compound Message
///
/// A message may contain a `value`, but it can also contain a list of [`FluentAttribute`] elements.
///
/// If a message contains attributes, it is called a "compound" message.
///
/// In such case, the message contains a list of key-value attributes that represent
/// different translation values associated with a single translation unit.
///
/// This is useful for scenarios where a [`FluentMessage`] is associated with a
/// complex User Interface widget which has multiple attributes that need to be translated.
/// ```text
/// confirm-modal = Are you sure?
/// .confirm = Yes
/// .cancel = No
/// .tooltip = Closing the window will lose all unsaved data.
/// ```
#[derive(Debug, PartialEq)]
pub struct FluentMessage<'m> {
node: &'m ast::Message<&'m str>,
}
impl<'m> FluentMessage<'m> {
/// Retrieves an option of a [`ast::Pattern`](fluent_syntax::ast::Pattern).
///
/// # Example
///
/// ```
/// # use fluent_bundle::{FluentResource, FluentBundle};
/// # let source = r#"
/// # hello-world = Hello World!
/// # "#;
/// # let resource = FluentResource::try_new(source.to_string())
/// # .expect("Failed to parse the resource.");
/// # let mut bundle = FluentBundle::default();
/// # bundle.add_resource(resource)
/// # .expect("Failed to add a resource.");
/// let msg = bundle.get_message("hello-world")
/// .expect("Failed to retrieve a message.");
///
/// if let Some(value) = msg.value() {
/// let mut err = vec![];
/// assert_eq!(
/// bundle.format_pattern(value, None, &mut err),
/// "Hello World!"
/// );
/// # assert_eq!(err.len(), 0);
/// }
/// ```
pub fn value(&self) -> Option<&'m ast::Pattern<&'m str>> {
self.node.value.as_ref()
}
/// An iterator over [`FluentAttribute`] elements.
///
/// # Example
///
/// ```
/// # use fluent_bundle::{FluentResource, FluentBundle};
/// # let source = r#"
/// # hello-world =
/// # .label = This is a label
/// # .accesskey = C
/// # "#;
/// # let resource = FluentResource::try_new(source.to_string())
/// # .expect("Failed to parse the resource.");
/// # let mut bundle = FluentBundle::default();
/// # bundle.add_resource(resource)
/// # .expect("Failed to add a resource.");
/// let msg = bundle.get_message("hello-world")
/// .expect("Failed to retrieve a message.");
///
/// let mut err = vec![];
///
/// for attr in msg.attributes() {
/// let _ = bundle.format_pattern(attr.value(), None, &mut err);
/// }
/// # assert_eq!(err.len(), 0);
/// ```
pub fn attributes(&self) -> impl Iterator<Item = FluentAttribute<'m>> {
self.node.attributes.iter().map(Into::into)
}
/// Retrieve a single [`FluentAttribute`] element.
///
/// # Example
///
/// ```
/// # use fluent_bundle::{FluentResource, FluentBundle};
/// # let source = r#"
/// # hello-world =
/// # .label = This is a label
/// # .accesskey = C
/// # "#;
/// # let resource = FluentResource::try_new(source.to_string())
/// # .expect("Failed to parse the resource.");
/// # let mut bundle = FluentBundle::default();
/// # bundle.add_resource(resource)
/// # .expect("Failed to add a resource.");
/// let msg = bundle.get_message("hello-world")
/// .expect("Failed to retrieve a message.");
///
/// let mut err = vec![];
///
/// if let Some(attr) = msg.get_attribute("label") {
/// assert_eq!(
/// bundle.format_pattern(attr.value(), None, &mut err),
/// "This is a label"
/// );
/// }
/// # assert_eq!(err.len(), 0);
/// ```
pub fn get_attribute(&self, key: &str) -> Option<FluentAttribute<'m>> {
self.node
.attributes
.iter()
.find(|attr| attr.id.name == key)
.map(Into::into)
}
}
impl<'m> From<&'m ast::Message<&'m str>> for FluentMessage<'m> {
fn from(msg: &'m ast::Message<&'m str>) -> Self {
FluentMessage { node: msg }
}
}
@@ -0,0 +1,101 @@
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(Clone, Debug, Eq, PartialEq)]
pub enum ReferenceKind {
Function {
id: String,
},
Message {
id: String,
attribute: Option<String>,
},
Term {
id: String,
attribute: Option<String>,
},
Variable {
id: String,
},
}
impl<T> From<&InlineExpression<T>> for ReferenceKind
where
T: ToString,
{
fn from(exp: &InlineExpression<T>) -> Self {
match exp {
InlineExpression::FunctionReference { id, .. } => Self::Function {
id: id.name.to_string(),
},
InlineExpression::MessageReference { id, attribute } => Self::Message {
id: id.name.to_string(),
attribute: attribute.as_ref().map(|i| i.name.to_string()),
},
InlineExpression::TermReference { id, attribute, .. } => Self::Term {
id: id.name.to_string(),
attribute: attribute.as_ref().map(|i| i.name.to_string()),
},
InlineExpression::VariableReference { id, .. } => Self::Variable {
id: id.name.to_string(),
},
_ => unreachable!(),
}
}
}
/// 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(Clone, Debug, Eq, PartialEq)]
pub enum ResolverError {
Reference(ReferenceKind),
NoValue(String),
MissingDefault,
Cyclic,
TooManyPlaceables,
}
impl std::fmt::Display for ResolverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Reference(exp) => match exp {
ReferenceKind::Function { id } => write!(f, "Unknown function: {}()", id),
ReferenceKind::Message {
id,
attribute: None,
} => write!(f, "Unknown message: {}", id),
ReferenceKind::Message {
id,
attribute: Some(attribute),
} => write!(f, "Unknown attribute: {}.{}", id, attribute),
ReferenceKind::Term {
id,
attribute: None,
} => write!(f, "Unknown term: -{}", id),
ReferenceKind::Term {
id,
attribute: Some(attribute),
} => write!(f, "Unknown attribute: -{}.{}", id, attribute),
ReferenceKind::Variable { id } => write!(f, "Unknown variable: ${}", id),
},
Self::NoValue(id) => write!(f, "No value: {}", id),
Self::MissingDefault => f.write_str("No default"),
Self::Cyclic => f.write_str("Cyclical dependency detected"),
Self::TooManyPlaceables => f.write_str("Too many placeables"),
}
}
}
impl<T> From<&InlineExpression<T>> for ResolverError
where
T: ToString,
{
fn from(exp: &InlineExpression<T>) -> Self {
Self::Reference(exp.into())
}
}
impl Error for ResolverError {}
@@ -0,0 +1,66 @@
use super::scope::Scope;
use super::WriteValue;
use std::borrow::Borrow;
use std::fmt;
use fluent_syntax::ast;
use crate::memoizer::MemoizerKind;
use crate::resolver::{ResolveValue, ResolverError};
use crate::resource::FluentResource;
use crate::types::FluentValue;
impl<'bundle> WriteValue<'bundle> for ast::Expression<&'bundle str> {
fn write<'ast, 'args, 'errors, W, R, M>(
&'ast self,
w: &mut W,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
) -> fmt::Result
where
W: fmt::Write,
R: Borrow<FluentResource>,
M: MemoizerKind,
{
match self {
Self::Inline(exp) => exp.write(w, scope),
Self::Select { selector, variants } => {
let selector = selector.resolve(scope);
match selector {
FluentValue::String(_) | FluentValue::Number(_) => {
for variant in variants {
let key = match variant.key {
ast::VariantKey::Identifier { name } => name.into(),
ast::VariantKey::NumberLiteral { value } => {
FluentValue::try_number(value)
}
};
if key.matches(&selector, scope) {
return variant.value.write(w, scope);
}
}
}
_ => {}
}
for variant in variants {
if variant.default {
return variant.value.write(w, scope);
}
}
scope.add_error(ResolverError::MissingDefault);
Ok(())
}
}
}
fn write_error<W>(&self, w: &mut W) -> fmt::Result
where
W: fmt::Write,
{
match self {
Self::Inline(exp) => exp.write_error(w),
Self::Select { selector, .. } => selector.write_error(w),
}
}
}
@@ -0,0 +1,196 @@
use super::scope::Scope;
use super::{ResolveValue, ResolverError, WriteValue};
use std::borrow::Borrow;
use std::fmt;
use fluent_syntax::ast;
use fluent_syntax::unicode::{unescape_unicode, unescape_unicode_to_string};
use crate::entry::GetEntry;
use crate::memoizer::MemoizerKind;
use crate::resource::FluentResource;
use crate::types::FluentValue;
impl<'bundle> WriteValue<'bundle> for ast::InlineExpression<&'bundle str> {
fn write<'ast, 'args, 'errors, W, R, M>(
&'ast self,
w: &mut W,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
) -> fmt::Result
where
W: fmt::Write,
R: Borrow<FluentResource>,
M: MemoizerKind,
{
match self {
Self::StringLiteral { value } => unescape_unicode(w, value),
Self::MessageReference { id, attribute } => {
if let Some(msg) = scope.bundle.get_entry_message(id.name) {
if let Some(attr) = attribute {
msg.attributes
.iter()
.find_map(|a| {
if a.id.name == attr.name {
Some(scope.track(w, &a.value, self))
} else {
None
}
})
.unwrap_or_else(|| scope.write_ref_error(w, self))
} else {
msg.value
.as_ref()
.map(|value| scope.track(w, value, self))
.unwrap_or_else(|| {
scope.add_error(ResolverError::NoValue(id.name.to_string()));
w.write_char('{')?;
self.write_error(w)?;
w.write_char('}')
})
}
} else {
scope.write_ref_error(w, self)
}
}
Self::NumberLiteral { value } => FluentValue::try_number(value).write(w, scope),
Self::TermReference {
id,
attribute,
arguments,
} => {
let (_, resolved_named_args) = scope.get_arguments(arguments.as_ref());
scope.local_args = Some(resolved_named_args);
let result = scope
.bundle
.get_entry_term(id.name)
.and_then(|term| {
if let Some(attr) = attribute {
term.attributes.iter().find_map(|a| {
if a.id.name == attr.name {
Some(scope.track(w, &a.value, self))
} else {
None
}
})
} else {
Some(scope.track(w, &term.value, self))
}
})
.unwrap_or_else(|| scope.write_ref_error(w, self));
scope.local_args = None;
result
}
Self::FunctionReference { id, arguments } => {
let (resolved_positional_args, resolved_named_args) =
scope.get_arguments(Some(arguments));
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);
if let FluentValue::Error = result {
self.write_error(w)
} else {
w.write_str(&result.into_string(scope))
}
} else {
scope.write_ref_error(w, self)
}
}
Self::VariableReference { id } => {
let args = scope.local_args.as_ref().or(scope.args);
if let Some(arg) = args.and_then(|args| args.get(id.name)) {
arg.write(w, scope)
} else {
if scope.local_args.is_none() {
scope.add_error(self.into());
}
w.write_char('{')?;
self.write_error(w)?;
w.write_char('}')
}
}
Self::Placeable { expression } => expression.write(w, scope),
}
}
fn write_error<W>(&self, w: &mut W) -> fmt::Result
where
W: fmt::Write,
{
match self {
Self::MessageReference {
id,
attribute: Some(attribute),
} => write!(w, "{}.{}", id.name, attribute.name),
Self::MessageReference {
id,
attribute: None,
} => w.write_str(id.name),
Self::TermReference {
id,
attribute: Some(attribute),
..
} => write!(w, "-{}.{}", id.name, attribute.name),
Self::TermReference {
id,
attribute: None,
..
} => write!(w, "-{}", id.name),
Self::FunctionReference { id, .. } => write!(w, "{}()", id.name),
Self::VariableReference { id } => write!(w, "${}", id.name),
_ => unreachable!(),
}
}
}
impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> {
fn resolve<'ast, 'args, 'errors, R, M>(
&'ast self,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
) -> FluentValue<'bundle>
where
R: Borrow<FluentResource>,
M: MemoizerKind,
{
match self {
Self::StringLiteral { value } => unescape_unicode_to_string(value).into(),
Self::NumberLiteral { value } => FluentValue::try_number(value),
Self::VariableReference { id } => {
if let Some(local_args) = &scope.local_args {
if let Some(arg) = local_args.get(id.name) {
return arg.clone();
}
} else if let Some(arg) = scope.args.and_then(|args| args.get(id.name)) {
return arg.into_owned();
}
if scope.local_args.is_none() {
scope.add_error(self.into());
}
FluentValue::Error
}
Self::FunctionReference { id, arguments } => {
let (resolved_positional_args, resolved_named_args) =
scope.get_arguments(Some(arguments));
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
} else {
FluentValue::Error
}
}
_ => {
let mut result = String::new();
self.write(&mut result, scope).expect("Failed to write");
result.into()
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
//! 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;
mod pattern;
mod scope;
pub use errors::ResolverError;
pub use scope::Scope;
use std::borrow::Borrow;
use std::fmt;
use crate::memoizer::MemoizerKind;
use crate::resource::FluentResource;
use crate::types::FluentValue;
/// Resolves an AST node to a [`FluentValue`].
pub(crate) trait ResolveValue<'bundle> {
/// Resolves an AST node to a [`FluentValue`].
fn resolve<'ast, 'args, 'errors, R, M>(
&'ast self,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
) -> FluentValue<'bundle>
where
R: Borrow<FluentResource>,
M: MemoizerKind;
}
/// Resolves an AST node to a string that is written to source `W`.
pub(crate) trait WriteValue<'bundle> {
/// Resolves an AST node to a string that is written to source `W`.
fn write<'ast, 'args, 'errors, W, R, M>(
&'ast self,
w: &mut W,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
) -> fmt::Result
where
W: fmt::Write,
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;
}
@@ -0,0 +1,108 @@
use super::scope::Scope;
use super::{ResolverError, WriteValue};
use std::borrow::Borrow;
use std::fmt;
use fluent_syntax::ast;
use crate::memoizer::MemoizerKind;
use crate::resolver::ResolveValue;
use crate::resource::FluentResource;
use crate::types::FluentValue;
const MAX_PLACEABLES: u8 = 100;
impl<'bundle> WriteValue<'bundle> for ast::Pattern<&'bundle str> {
fn write<'ast, 'args, 'errors, W, R, M>(
&'ast self,
w: &mut W,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
) -> fmt::Result
where
W: fmt::Write,
R: Borrow<FluentResource>,
M: MemoizerKind,
{
let len = self.elements.len();
for elem in &self.elements {
if scope.dirty {
return Ok(());
}
match elem {
ast::PatternElement::TextElement { value } => {
if let Some(ref transform) = scope.bundle.transform {
w.write_str(&transform(value))?;
} else {
w.write_str(value)?;
}
}
ast::PatternElement::Placeable { ref expression } => {
scope.placeables += 1;
if scope.placeables > MAX_PLACEABLES {
scope.dirty = true;
scope.add_error(ResolverError::TooManyPlaceables);
return Ok(());
}
let needs_isolation = scope.bundle.use_isolating
&& len > 1
&& !matches!(
expression,
ast::Expression::Inline(ast::InlineExpression::MessageReference { .. },)
| ast::Expression::Inline(
ast::InlineExpression::TermReference { .. },
)
| ast::Expression::Inline(
ast::InlineExpression::StringLiteral { .. },
)
);
if needs_isolation {
w.write_char('\u{2068}')?;
}
scope.maybe_track(w, self, expression)?;
if needs_isolation {
w.write_char('\u{2069}')?;
}
}
}
}
Ok(())
}
fn write_error<W>(&self, _w: &mut W) -> fmt::Result
where
W: fmt::Write,
{
unreachable!()
}
}
impl<'bundle> ResolveValue<'bundle> for ast::Pattern<&'bundle str> {
fn resolve<'ast, 'args, 'errors, R, M>(
&'ast self,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
) -> FluentValue<'bundle>
where
R: Borrow<FluentResource>,
M: MemoizerKind,
{
let len = self.elements.len();
if len == 1 {
if let ast::PatternElement::TextElement { value } = self.elements[0] {
return scope
.bundle
.transform
.map_or_else(|| value.into(), |transform| transform(value).into());
}
}
let mut result = String::new();
self.write(&mut result, scope)
.expect("Failed to write to a string.");
result.into()
}
}
+140
View File
@@ -0,0 +1,140 @@
use crate::bundle::FluentBundle;
use crate::memoizer::MemoizerKind;
use crate::resolver::{ResolveValue, ResolverError, WriteValue};
use crate::types::FluentValue;
use crate::{FluentArgs, FluentError, FluentResource};
use fluent_syntax::ast;
use std::borrow::Borrow;
use std::fmt;
/// State for a single `ResolveValue::to_value` call.
pub struct Scope<'bundle, 'ast, 'args, 'errors, R, M> {
/// The current `FluentBundle` instance.
pub bundle: &'bundle FluentBundle<R, M>,
/// The current arguments passed by the developer.
pub(super) args: Option<&'args FluentArgs<'args>>,
/// Local args
pub(super) local_args: Option<FluentArgs<'bundle>>,
/// The running count of resolved placeables. Used to detect the Billion
/// Laughs and Quadratic Blowup attacks.
pub(super) placeables: u8,
/// Tracks hashes to prevent infinite recursion.
traveled: smallvec::SmallVec<[&'ast ast::Pattern<&'bundle str>; 2]>,
/// Track errors accumulated during resolving.
pub errors: Option<&'errors mut Vec<FluentError>>,
/// Makes the resolver bail.
pub dirty: bool,
}
impl<'bundle, 'ast, 'args, 'errors, R, M> Scope<'bundle, 'ast, 'args, 'errors, R, M> {
pub fn new(
bundle: &'bundle FluentBundle<R, M>,
args: Option<&'args FluentArgs>,
errors: Option<&'errors mut Vec<FluentError>>,
) -> Self {
Scope {
bundle,
args,
local_args: None,
placeables: 0,
traveled: Default::default(),
errors,
dirty: false,
}
}
pub fn add_error(&mut self, error: ResolverError) {
if let Some(errors) = self.errors.as_mut() {
errors.push(error.into());
}
}
/// 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,
pattern: &'ast ast::Pattern<&'bundle str>,
exp: &'ast ast::Expression<&'bundle str>,
) -> fmt::Result
where
R: Borrow<FluentResource>,
W: fmt::Write,
M: MemoizerKind,
{
if self.traveled.is_empty() {
self.traveled.push(pattern);
}
exp.write(w, self)?;
if self.dirty {
w.write_char('{')?;
exp.write_error(w)?;
w.write_char('}')
} else {
Ok(())
}
}
pub fn track<W>(
&mut self,
w: &mut W,
pattern: &'ast ast::Pattern<&'bundle str>,
exp: &'ast ast::InlineExpression<&'bundle str>,
) -> fmt::Result
where
R: Borrow<FluentResource>,
W: fmt::Write,
M: MemoizerKind,
{
if self.traveled.contains(&pattern) {
self.add_error(ResolverError::Cyclic);
w.write_char('{')?;
exp.write_error(w)?;
w.write_char('}')
} else {
self.traveled.push(pattern);
let result = pattern.write(w, self);
self.traveled.pop();
result
}
}
pub fn write_ref_error<W>(
&mut self,
w: &mut W,
exp: &ast::InlineExpression<&str>,
) -> fmt::Result
where
W: fmt::Write,
{
self.add_error(exp.into());
w.write_char('{')?;
exp.write_error(w)?;
w.write_char('}')
}
pub fn get_arguments(
&mut self,
arguments: Option<&'ast ast::CallArguments<&'bundle str>>,
) -> (Vec<FluentValue<'bundle>>, FluentArgs<'bundle>)
where
R: Borrow<FluentResource>,
M: MemoizerKind,
{
if let Some(ast::CallArguments { positional, named }) = arguments {
let positional = positional.iter().map(|expr| expr.resolve(self)).collect();
let named = named
.iter()
.map(|arg| (arg.name.name, arg.value.resolve(self)))
.collect();
(positional, named)
} else {
(Vec::new(), FluentArgs::new())
}
}
}
+171
View File
@@ -0,0 +1,171 @@
use fluent_syntax::ast;
use fluent_syntax::parser::{parse_runtime, ParserError};
use self_cell::self_cell;
type Resource<'s> = ast::Resource<&'s str>;
self_cell!(
pub struct InnerFluentResource {
owner: String,
#[covariant]
dependent: Resource,
}
impl {Debug}
);
/// A resource containing a list of localization messages.
///
/// [`FluentResource`] wraps an [`Abstract Syntax Tree`](../fluent_syntax/ast/index.html) produced by the
/// [`parser`](../fluent_syntax/parser/index.html) and provides an access to a list
/// of its entries.
///
/// A good mental model for a resource is a single FTL file, but in the future
/// there's nothing preventing a resource from being stored in a data base,
/// pre-parsed format or in some other structured form.
///
/// # Example
///
/// ```
/// use fluent_bundle::FluentResource;
///
/// let source = r#"
///
/// hello-world = Hello World!
///
/// "#;
///
/// let resource = FluentResource::try_new(source.to_string())
/// .expect("Errors encountered while parsing a resource.");
///
/// assert_eq!(resource.entries().count(), 1);
/// ```
///
/// # Ownership
///
/// A resource owns the source string and the AST contains references
/// to the slices of the source.
#[derive(Debug)]
pub struct FluentResource(InnerFluentResource);
impl FluentResource {
/// A fallible constructor of a new [`FluentResource`].
///
/// It takes an encoded `Fluent Translation List` string, parses
/// it and stores both, the input string and the AST view of it,
/// for runtime use.
///
/// # Example
///
/// ```
/// use fluent_bundle::FluentResource;
///
/// let source = r#"
///
/// hello-world = Hello, { $user }!
///
/// "#;
///
/// let resource = FluentResource::try_new(source.to_string());
///
/// assert!(resource.is_ok());
/// ```
///
/// # Errors
///
/// The method will return the resource irrelevant of parse errors
/// encountered during parsing of the source, but in case of errors,
/// the `Err` variant will contain both the structure and a vector
/// of errors.
pub fn try_new(source: String) -> Result<Self, (Self, Vec<ParserError>)> {
let mut errors = None;
let res = InnerFluentResource::new(source, |source| match parse_runtime(source.as_str()) {
Ok(ast) => ast,
Err((ast, err)) => {
errors = Some(err);
ast
}
});
match errors {
None => Ok(Self(res)),
Some(err) => Err((Self(res), err)),
}
}
/// Returns a reference to the source string that was used
/// to construct the [`FluentResource`].
///
/// # Example
///
/// ```
/// use fluent_bundle::FluentResource;
///
/// let source = "hello-world = Hello, { $user }!";
///
/// let resource = FluentResource::try_new(source.to_string())
/// .expect("Failed to parse FTL.");
///
/// assert_eq!(
/// resource.source(),
/// "hello-world = Hello, { $user }!"
/// );
/// ```
pub fn source(&self) -> &str {
self.0.borrow_owner()
}
/// Returns an iterator over [`entries`](fluent_syntax::ast::Entry) of the [`FluentResource`].
///
/// # Example
///
/// ```
/// use fluent_bundle::FluentResource;
/// use fluent_syntax::ast;
///
/// let source = r#"
///
/// hello-world = Hello, { $user }!
///
/// "#;
///
/// let resource = FluentResource::try_new(source.to_string())
/// .expect("Failed to parse FTL.");
///
/// assert_eq!(
/// resource.entries().count(),
/// 1
/// );
/// assert!(matches!(resource.entries().next(), Some(ast::Entry::Message(_))));
/// ```
pub fn entries(&self) -> impl Iterator<Item = &ast::Entry<&str>> {
self.0.borrow_dependent().body.iter()
}
/// Returns an [`Entry`](fluent_syntax::ast::Entry) at the
/// given index out of the [`FluentResource`].
///
/// # Example
///
/// ```
/// use fluent_bundle::FluentResource;
/// use fluent_syntax::ast;
///
/// let source = r#"
///
/// hello-world = Hello, { $user }!
///
/// "#;
///
/// let resource = FluentResource::try_new(source.to_string())
/// .expect("Failed to parse FTL.");
///
/// assert!(matches!(resource.get_entry(0), Some(ast::Entry::Message(_))));
/// ```
pub fn get_entry(&self, idx: usize) -> Option<&ast::Entry<&str>> {
self.0.borrow_dependent().body.get(idx)
}
}
+326
View File
@@ -0,0 +1,326 @@
//! `types` module contains types necessary for Fluent runtime
//! value handling.
//! The core struct is [`FluentValue`] which is a type that can be passed
//! to the [`FluentBundle::format_pattern`](crate::bundle::FluentBundle) as an argument, it can be passed
//! to any Fluent Function, and any function may return it.
//!
//! This part of functionality is not fully hashed out yet, since we're waiting
//! for the internationalization APIs to mature, at which point all number
//! formatting operations will be moved out of Fluent.
//!
//! For now, [`FluentValue`] can be a string, a number, or a custom [`FluentType`]
//! which allows users of the library to implement their own types of values,
//! such as dates, or more complex structures needed for their bindings.
mod number;
mod plural;
pub use number::*;
use plural::PluralRules;
use std::any::Any;
use std::borrow::{Borrow, Cow};
use std::fmt;
use std::str::FromStr;
use intl_pluralrules::{PluralCategory, PluralRuleType};
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,
) -> Cow<'static, str>;
}
impl PartialEq for dyn FluentType + Send {
fn eq(&self, other: &Self) -> bool {
self.equals(other.as_any())
}
}
pub trait AnyEq: Any + 'static {
fn equals(&self, other: &dyn Any) -> bool;
fn as_any(&self) -> &dyn Any;
}
impl<T: Any + PartialEq> AnyEq for T {
fn equals(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>() == Some(self)
}
fn as_any(&self) -> &dyn Any {
self
}
}
/// The `FluentValue` enum represents values which can be formatted to a String.
///
/// Those values are either passed as arguments to [`FluentBundle::format_pattern`] or
/// produced by functions, or generated in the process of pattern resolution.
///
/// [`FluentBundle::format_pattern`]: crate::bundle::FluentBundle::format_pattern
#[derive(Debug)]
pub enum FluentValue<'source> {
String(Cow<'source, str>),
Number(FluentNumber),
Custom(Box<dyn FluentType + Send>),
None,
Error,
}
impl PartialEq for FluentValue<'_> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(FluentValue::String(s), FluentValue::String(s2)) => s == s2,
(FluentValue::Number(s), FluentValue::Number(s2)) => s == s2,
(FluentValue::Custom(s), FluentValue::Custom(s2)) => s == s2,
_ => false,
}
}
}
impl Clone for FluentValue<'_> {
fn clone(&self) -> Self {
match self {
FluentValue::String(s) => FluentValue::String(s.clone()),
FluentValue::Number(s) => FluentValue::Number(s.clone()),
FluentValue::Custom(s) => {
let new_value: Box<dyn FluentType + Send> = s.duplicate();
FluentValue::Custom(new_value)
}
FluentValue::Error => FluentValue::Error,
FluentValue::None => FluentValue::None,
}
}
}
impl<'source> FluentValue<'source> {
/// 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: &'source str) -> Self {
if let Ok(number) = FluentNumber::from_str(value) {
number.into()
} else {
value.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,
scope: &Scope<R, M>,
) -> bool
where
M: MemoizerKind,
{
match (self, other) {
(FluentValue::String(a), FluentValue::String(b)) => a == b,
(FluentValue::Number(a), FluentValue::Number(b)) => a == b,
(FluentValue::String(a), FluentValue::Number(b)) => {
let cat = match a.as_ref() {
"zero" => PluralCategory::ZERO,
"one" => PluralCategory::ONE,
"two" => PluralCategory::TWO,
"few" => PluralCategory::FEW,
"many" => PluralCategory::MANY,
"other" => PluralCategory::OTHER,
_ => return false,
};
// 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::<PluralRules, _, _>((r#type,), |pr| {
pr.0.select(b) == Ok(cat)
})
.unwrap()
}
_ => false,
}
}
/// 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,
R: Borrow<FluentResource>,
M: MemoizerKind,
{
if let Some(formatter) = &scope.bundle.formatter {
if let Some(val) = formatter(self, &scope.bundle.intls) {
return w.write_str(&val);
}
}
match self {
FluentValue::String(s) => w.write_str(s),
FluentValue::Number(n) => w.write_str(&n.as_string()),
FluentValue::Custom(s) => w.write_str(&scope.bundle.intls.stringify_value(&**s)),
FluentValue::Error => Ok(()),
FluentValue::None => Ok(()),
}
}
/// Converts the [`FluentValue`] to a string.
///
/// Clones inner values when owned, borrowed data is not cloned.
/// Prefer using [`FluentValue::into_string()`] when possible.
pub fn as_string<R: Borrow<FluentResource>, M>(&self, scope: &Scope<R, M>) -> Cow<'source, str>
where
M: MemoizerKind,
{
if let Some(formatter) = &scope.bundle.formatter {
if let Some(val) = formatter(self, &scope.bundle.intls) {
return val.into();
}
}
match self {
FluentValue::String(s) => s.clone(),
FluentValue::Number(n) => n.as_string(),
FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
FluentValue::Error => "".into(),
FluentValue::None => "".into(),
}
}
/// Converts the [`FluentValue`] to a string.
///
/// Takes self by-value to be able to skip expensive clones.
/// Prefer this method over [`FluentValue::as_string()`] when possible.
pub fn into_string<R: Borrow<FluentResource>, M>(self, scope: &Scope<R, M>) -> Cow<'source, str>
where
M: MemoizerKind,
{
if let Some(formatter) = &scope.bundle.formatter {
if let Some(val) = formatter(&self, &scope.bundle.intls) {
return val.into();
}
}
match self {
FluentValue::String(s) => s,
FluentValue::Number(n) => n.as_string(),
FluentValue::Custom(s) => scope.bundle.intls.stringify_value(s.as_ref()),
FluentValue::Error => "".into(),
FluentValue::None => "".into(),
}
}
pub fn into_owned<'a>(&self) -> FluentValue<'a> {
match self {
FluentValue::String(str) => FluentValue::String(Cow::from(str.to_string())),
FluentValue::Number(s) => FluentValue::Number(s.clone()),
FluentValue::Custom(s) => FluentValue::Custom(s.duplicate()),
FluentValue::Error => FluentValue::Error,
FluentValue::None => FluentValue::None,
}
}
}
impl From<String> for FluentValue<'_> {
fn from(s: String) -> Self {
FluentValue::String(s.into())
}
}
impl<'source> From<&'source String> for FluentValue<'source> {
fn from(s: &'source String) -> Self {
FluentValue::String(s.into())
}
}
impl<'source> From<&'source str> for FluentValue<'source> {
fn from(s: &'source str) -> Self {
FluentValue::String(s.into())
}
}
impl<'source> From<Cow<'source, str>> for FluentValue<'source> {
fn from(s: Cow<'source, str>) -> Self {
FluentValue::String(s)
}
}
impl<'source, T> From<Option<T>> for FluentValue<'source>
where
T: Into<FluentValue<'source>>,
{
fn from(v: Option<T>) -> Self {
match v {
Some(v) => v.into(),
None => FluentValue::None,
}
}
}
+260
View File
@@ -0,0 +1,260 @@
use std::borrow::Cow;
use std::convert::TryInto;
use std::default::Default;
use std::str::FromStr;
use intl_pluralrules::operands::PluralOperands;
use crate::args::FluentArgs;
use crate::types::FluentValue;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
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(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum FluentNumberStyle {
#[default]
Decimal,
Currency,
Percent,
}
impl From<&str> for FluentNumberStyle {
fn from(input: &str) -> Self {
match input {
"decimal" => Self::Decimal,
"currency" => Self::Currency,
"percent" => Self::Percent,
_ => Self::default(),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum FluentNumberCurrencyDisplayStyle {
#[default]
Symbol,
Code,
Name,
}
impl From<&str> for FluentNumberCurrencyDisplayStyle {
fn from(input: &str) -> Self {
match input {
"symbol" => Self::Symbol,
"code" => Self::Code,
"name" => Self::Name,
_ => Self::default(),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct FluentNumberOptions {
pub r#type: FluentNumberType,
pub style: FluentNumberStyle,
pub currency: Option<String>,
pub currency_display: FluentNumberCurrencyDisplayStyle,
pub use_grouping: bool,
pub minimum_integer_digits: Option<usize>,
pub minimum_fraction_digits: Option<usize>,
pub maximum_fraction_digits: Option<usize>,
pub minimum_significant_digits: Option<usize>,
pub maximum_significant_digits: Option<usize>,
}
impl Default for FluentNumberOptions {
fn default() -> Self {
Self {
r#type: Default::default(),
style: Default::default(),
currency: None,
currency_display: Default::default(),
use_grouping: true,
minimum_integer_digits: None,
minimum_fraction_digits: None,
maximum_fraction_digits: None,
minimum_significant_digits: None,
maximum_significant_digits: None,
}
}
}
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();
}
("currency", FluentValue::String(n)) => {
self.currency = Some(n.to_string());
}
("currencyDisplay", FluentValue::String(n)) => {
self.currency_display = n.as_ref().into();
}
("useGrouping", FluentValue::String(n)) => {
self.use_grouping = n != "false";
}
("minimumIntegerDigits", FluentValue::Number(n)) => {
self.minimum_integer_digits = Some(n.into());
}
("minimumFractionDigits", FluentValue::Number(n)) => {
self.minimum_fraction_digits = Some(n.into());
}
("maximumFractionDigits", FluentValue::Number(n)) => {
self.maximum_fraction_digits = Some(n.into());
}
("minimumSignificantDigits", FluentValue::Number(n)) => {
self.minimum_significant_digits = Some(n.into());
}
("maximumSignificantDigits", FluentValue::Number(n)) => {
self.maximum_significant_digits = Some(n.into());
}
_ => {}
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FluentNumber {
pub value: f64,
pub options: FluentNumberOptions,
}
impl FluentNumber {
pub const fn new(value: f64, options: FluentNumberOptions) -> Self {
Self { value, options }
}
pub fn as_string(&self) -> Cow<'static, str> {
let mut val = self.value.to_string();
if let Some(minfd) = self.options.minimum_fraction_digits {
if let Some(pos) = val.find('.') {
let frac_num = val.len() - pos - 1;
let missing = minfd.saturating_sub(frac_num);
val = format!("{}{}", val, "0".repeat(missing));
} else {
val = format!("{}.{}", val, "0".repeat(minfd));
}
}
val.into()
}
}
impl FromStr for FluentNumber {
type Err = std::num::ParseFloatError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
f64::from_str(input).map(|n| {
let mfd = input.find('.').map(|pos| input.len() - pos - 1);
let opts = FluentNumberOptions {
minimum_fraction_digits: mfd,
..Default::default()
};
Self::new(n, opts)
})
}
}
impl From<FluentNumber> for FluentValue<'_> {
fn from(input: FluentNumber) -> Self {
FluentValue::Number(input)
}
}
macro_rules! from_num {
($num:ty) => {
impl From<$num> for FluentNumber {
fn from(n: $num) -> Self {
Self {
value: n as f64,
options: FluentNumberOptions::default(),
}
}
}
impl From<&$num> for FluentNumber {
fn from(n: &$num) -> Self {
Self {
value: *n as f64,
options: FluentNumberOptions::default(),
}
}
}
impl From<FluentNumber> for $num {
fn from(input: FluentNumber) -> Self {
input.value as $num
}
}
impl From<&FluentNumber> for $num {
fn from(input: &FluentNumber) -> Self {
input.value as $num
}
}
impl From<$num> for FluentValue<'_> {
fn from(n: $num) -> Self {
FluentValue::Number(n.into())
}
}
impl From<&$num> for FluentValue<'_> {
fn from(n: &$num) -> Self {
FluentValue::Number(n.into())
}
}
};
($($num:ty)+) => {
$(from_num!($num);)+
};
}
impl From<&FluentNumber> for PluralOperands {
fn from(input: &FluentNumber) -> Self {
let mut operands: Self = input
.value
.try_into()
.expect("Failed to generate operands out of FluentNumber");
if let Some(mfd) = input.options.minimum_fraction_digits {
if mfd > operands.v {
operands.f *= 10_u64.pow(mfd as u32 - operands.v as u32);
operands.v = mfd;
}
}
// XXX: Add support for other options.
operands
}
}
from_num!(i8 i16 i32 i64 i128 isize);
from_num!(u8 u16 u32 u64 u128 usize);
from_num!(f32 f64);
#[cfg(test)]
mod tests {
use crate::types::FluentValue;
#[test]
fn value_from_copy_ref() {
let x = 1i16;
let y = &x;
let z: FluentValue = y.into();
assert_eq!(z, FluentValue::try_number("1"));
}
}
+22
View File
@@ -0,0 +1,22 @@
use fluent_langneg::{negotiate_languages, NegotiationStrategy};
use intl_memoizer::Memoizable;
use intl_pluralrules::{PluralRuleType, PluralRules as IntlPluralRules};
use unic_langid::LanguageIdentifier;
pub struct PluralRules(pub IntlPluralRules);
impl Memoizable for PluralRules {
type Args = (PluralRuleType,);
type Error = &'static str;
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
let default_lang: LanguageIdentifier = "en".parse().unwrap();
let pr_lang = negotiate_languages(
&[lang],
&IntlPluralRules::get_locales(args.0),
Some(&default_lang),
NegotiationStrategy::Lookup,
)[0]
.clone();
Ok(Self(IntlPluralRules::create(pr_lang, args.0)?))
}
}