diff options
Diffstat (limited to 'third_party/rust/fluent-bundle/src')
18 files changed, 2642 insertions, 0 deletions
diff --git a/third_party/rust/fluent-bundle/src/args.rs b/third_party/rust/fluent-bundle/src/args.rs new file mode 100644 index 0000000000..b2d17a84b6 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/args.rs @@ -0,0 +1,120 @@ +use std::borrow::Cow; +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. +/// +/// # 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> { + pub fn new() -> Self { + Self::default() + } + + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + 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 + } + } + + 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(); + let idx = match self.0.binary_search_by_key(&&key, |(k, _)| k) { + Ok(idx) => idx, + Err(idx) => idx, + }; + self.0.insert(idx, (key, value.into())); + } + + 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() + } +} diff --git a/third_party/rust/fluent-bundle/src/bundle.rs b/third_party/rust/fluent-bundle/src/bundle.rs new file mode 100644 index 0000000000..3d085cfee5 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/bundle.rs @@ -0,0 +1,615 @@ +//! `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`](intl_memoizer::concurrent::IntlLangMemoizer) that uses [`Mutex`](std::sync::Mutex) and there's [`FluentBundle::new_concurrent`] which works with that. +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<FluentResurce> + /// + /// 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<FluentResurce> + /// + /// 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 primarly 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<&str>, + args: Option<&'bundle 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.as_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(), + }), + } + } +} + +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.get(0).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) + } +} diff --git a/third_party/rust/fluent-bundle/src/concurrent.rs b/third_party/rust/fluent-bundle/src/concurrent.rs new file mode 100644 index 0000000000..b87225efee --- /dev/null +++ b/third_party/rust/fluent-bundle/src/concurrent.rs @@ -0,0 +1,59 @@ +use intl_memoizer::{concurrent::IntlLangMemoizer, Memoizable}; +use rustc_hash::FxHashMap; +use unic_langid::LanguageIdentifier; + +use crate::bundle::FluentBundle; +use crate::memoizer::MemoizerKind; +use crate::types::FluentType; + +impl<R> FluentBundle<R, IntlLangMemoizer> { + /// A constructor analogous to [`FluentBundle::new`] but operating + /// on a concurrent version of [`IntlLangMemoizer`] over [`Mutex`](std::sync::Mutex). + /// + /// # Example + /// + /// ``` + /// use fluent_bundle::bundle::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.get(0).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) + } +} diff --git a/third_party/rust/fluent-bundle/src/entry.rs b/third_party/rust/fluent-bundle/src/entry.rs new file mode 100644 index 0000000000..1ac8ecf01b --- /dev/null +++ b/third_party/rust/fluent-bundle/src/entry.rs @@ -0,0 +1,62 @@ +//! `Entry` is used to store 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>; + +pub enum Entry { + Message((usize, usize)), + Term((usize, usize)), + Function(FluentFunction), +} + +pub trait GetEntry { + fn get_entry_message(&self, id: &str) -> Option<&ast::Message<&str>>; + fn get_entry_term(&self, id: &str) -> Option<&ast::Term<&str>>; + 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)? { + 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(pos) => { + let res = self.resources.get(pos.0)?.borrow(); + if let ast::Entry::Term(ref msg) = res.get_entry(pos.1)? { + 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, + }) + } +} diff --git a/third_party/rust/fluent-bundle/src/errors.rs b/third_party/rust/fluent-bundle/src/errors.rs new file mode 100644 index 0000000000..ec4a02c4b4 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/errors.rs @@ -0,0 +1,86 @@ +use crate::resolver::ResolverError; +use fluent_syntax::parser::ParserError; +use std::error::Error; + +#[derive(Debug, PartialEq, Clone)] +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(Debug, PartialEq, Clone)] +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) + } +} diff --git a/third_party/rust/fluent-bundle/src/lib.rs b/third_party/rust/fluent-bundle/src/lib.rs new file mode 100644 index 0000000000..faf3e9ba60 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/lib.rs @@ -0,0 +1,127 @@ +//! 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`](../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 bundle; +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::bundle::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; diff --git a/third_party/rust/fluent-bundle/src/memoizer.rs b/third_party/rust/fluent-bundle/src/memoizer.rs new file mode 100644 index 0000000000..c738a857b2 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/memoizer.rs @@ -0,0 +1,18 @@ +use crate::types::FluentType; +use intl_memoizer::Memoizable; +use unic_langid::LanguageIdentifier; + +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> + where + Self: Sized, + I: Memoizable + Send + Sync + 'static, + I::Args: Send + Sync + 'static, + U: FnOnce(&I) -> R; + + fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str>; +} diff --git a/third_party/rust/fluent-bundle/src/message.rs b/third_party/rust/fluent-bundle/src/message.rs new file mode 100644 index 0000000000..a6cc00d77e --- /dev/null +++ b/third_party/rust/fluent-bundle/src/message.rs @@ -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 } + } +} diff --git a/third_party/rust/fluent-bundle/src/resolver/errors.rs b/third_party/rust/fluent-bundle/src/resolver/errors.rs new file mode 100644 index 0000000000..831d8474a5 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/errors.rs @@ -0,0 +1,96 @@ +use fluent_syntax::ast::InlineExpression; +use std::error::Error; + +#[derive(Debug, PartialEq, Clone)] +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!(), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +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 {} diff --git a/third_party/rust/fluent-bundle/src/resolver/expression.rs b/third_party/rust/fluent-bundle/src/resolver/expression.rs new file mode 100644 index 0000000000..d0d02decd3 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/expression.rs @@ -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<'p> WriteValue for ast::Expression<&'p str> { + fn write<'scope, 'errors, W, R, M>( + &'scope self, + w: &mut W, + scope: &mut Scope<'scope, '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 { .. } => unreachable!(), + } + } +} diff --git a/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs b/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs new file mode 100644 index 0000000000..b9e89b6e8e --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs @@ -0,0 +1,181 @@ +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<'p> WriteValue for ast::InlineExpression<&'p str> { + fn write<'scope, 'errors, W, R, M>( + &'scope self, + w: &mut W, + scope: &mut Scope<'scope, '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.as_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<'p> ResolveValue for ast::InlineExpression<&'p str> { + fn resolve<'source, 'errors, R, M>( + &'source self, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> FluentValue<'source> + 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 } => { + let args = scope.local_args.as_ref().or(scope.args); + + if let Some(arg) = args.and_then(|args| args.get(id.name)) { + arg.clone() + } else { + if scope.local_args.is_none() { + scope.add_error(self.into()); + } + FluentValue::Error + } + } + _ => { + let mut result = String::new(); + self.write(&mut result, scope).expect("Failed to write"); + result.into() + } + } + } +} diff --git a/third_party/rust/fluent-bundle/src/resolver/mod.rs b/third_party/rust/fluent-bundle/src/resolver/mod.rs new file mode 100644 index 0000000000..f137bcc91b --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/mod.rs @@ -0,0 +1,42 @@ +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; + +// Converts an AST node to a `FluentValue`. +pub(crate) trait ResolveValue { + fn resolve<'source, 'errors, R, M>( + &'source self, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> FluentValue<'source> + where + R: Borrow<FluentResource>, + M: MemoizerKind; +} + +pub(crate) trait WriteValue { + fn write<'source, 'errors, W, R, M>( + &'source self, + w: &mut W, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> fmt::Result + where + W: fmt::Write, + R: Borrow<FluentResource>, + M: MemoizerKind; + + fn write_error<W>(&self, _w: &mut W) -> fmt::Result + where + W: fmt::Write; +} diff --git a/third_party/rust/fluent-bundle/src/resolver/pattern.rs b/third_party/rust/fluent-bundle/src/resolver/pattern.rs new file mode 100644 index 0000000000..4e01d4ca47 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/pattern.rs @@ -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<'p> WriteValue for ast::Pattern<&'p str> { + fn write<'scope, 'errors, W, R, M>( + &'scope self, + w: &mut W, + scope: &mut Scope<'scope, '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<'p> ResolveValue for ast::Pattern<&'p str> { + fn resolve<'source, 'errors, R, M>( + &'source self, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> FluentValue<'source> + 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() + } +} diff --git a/third_party/rust/fluent-bundle/src/resolver/scope.rs b/third_party/rust/fluent-bundle/src/resolver/scope.rs new file mode 100644 index 0000000000..004701137e --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/scope.rs @@ -0,0 +1,141 @@ +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<'scope, 'errors, R, M> { + /// The current `FluentBundle` instance. + pub bundle: &'scope FluentBundle<R, M>, + /// The current arguments passed by the developer. + pub(super) args: Option<&'scope FluentArgs<'scope>>, + /// Local args + pub(super) local_args: Option<FluentArgs<'scope>>, + /// 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. + travelled: smallvec::SmallVec<[&'scope ast::Pattern<&'scope str>; 2]>, + /// Track errors accumulated during resolving. + pub errors: Option<&'errors mut Vec<FluentError>>, + /// Makes the resolver bail. + pub dirty: bool, +} + +impl<'scope, 'errors, R, M> Scope<'scope, 'errors, R, M> { + pub fn new( + bundle: &'scope FluentBundle<R, M>, + args: Option<&'scope FluentArgs>, + errors: Option<&'errors mut Vec<FluentError>>, + ) -> Self { + Scope { + bundle, + args, + local_args: None, + placeables: 0, + travelled: 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: &'scope ast::Pattern<&str>, + exp: &'scope ast::Expression<&str>, + ) -> fmt::Result + where + R: Borrow<FluentResource>, + W: fmt::Write, + M: MemoizerKind, + { + if self.travelled.is_empty() { + self.travelled.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: &'scope ast::Pattern<&str>, + exp: &ast::InlineExpression<&str>, + ) -> fmt::Result + where + R: Borrow<FluentResource>, + W: fmt::Write, + M: MemoizerKind, + { + if self.travelled.contains(&pattern) { + self.add_error(ResolverError::Cyclic); + w.write_char('{')?; + exp.write_error(w)?; + w.write_char('}') + } else { + self.travelled.push(pattern); + let result = pattern.write(w, self); + self.travelled.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<&'scope ast::CallArguments<&'scope str>>, + ) -> (Vec<FluentValue<'scope>>, FluentArgs<'scope>) + 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()) + } + } +} diff --git a/third_party/rust/fluent-bundle/src/resource.rs b/third_party/rust/fluent-bundle/src/resource.rs new file mode 100644 index 0000000000..0c39c838f4 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resource.rs @@ -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) + } +} diff --git a/third_party/rust/fluent-bundle/src/types/mod.rs b/third_party/rust/fluent-bundle/src/types/mod.rs new file mode 100644 index 0000000000..714fe4c76f --- /dev/null +++ b/third_party/rust/fluent-bundle/src/types/mod.rs @@ -0,0 +1,202 @@ +//! `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; + +pub trait FluentType: fmt::Debug + AnyEq + 'static { + fn duplicate(&self) -> Box<dyn FluentType + Send>; + fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str>; + 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>() + .map_or(false, |that| self == that) + } + 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`]: ../bundle/struct.FluentBundle.html#method.format_pattern +#[derive(Debug)] +pub enum FluentValue<'source> { + String(Cow<'source, str>), + Number(FluentNumber), + Custom(Box<dyn FluentType + Send>), + None, + Error, +} + +impl<'s> PartialEq for FluentValue<'s> { + 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<'s> Clone for FluentValue<'s> { + 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> { + pub fn try_number<S: ToString>(v: S) -> Self { + let s = v.to_string(); + if let Ok(num) = FluentNumber::from_str(&s) { + num.into() + } else { + s.into() + } + } + + pub fn matches<R: Borrow<FluentResource>, M>( + &self, + other: &FluentValue, + scope: &Scope<R, M>, + ) -> bool + where + M: MemoizerKind, + { + match (self, other) { + (&FluentValue::String(ref a), &FluentValue::String(ref b)) => a == b, + (&FluentValue::Number(ref a), &FluentValue::Number(ref b)) => a == b, + (&FluentValue::String(ref a), &FluentValue::Number(ref 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, + }; + scope + .bundle + .intls + .with_try_get_threadsafe::<PluralRules, _, _>( + (PluralRuleType::CARDINAL,), + |pr| pr.0.select(b) == Ok(cat), + ) + .unwrap() + } + _ => false, + } + } + + 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(()), + } + } + + 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(), + } + } +} + +impl<'source> From<String> for FluentValue<'source> { + fn from(s: 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) + } +} diff --git a/third_party/rust/fluent-bundle/src/types/number.rs b/third_party/rust/fluent-bundle/src/types/number.rs new file mode 100644 index 0000000000..d39291ff46 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/types/number.rs @@ -0,0 +1,252 @@ +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(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum FluentNumberStyle { + Decimal, + Currency, + Percent, +} + +impl std::default::Default for FluentNumberStyle { + fn default() -> Self { + Self::Decimal + } +} + +impl From<&str> for FluentNumberStyle { + fn from(input: &str) -> Self { + match input { + "decimal" => Self::Decimal, + "currency" => Self::Currency, + "percent" => Self::Percent, + _ => Self::default(), + } + } +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum FluentNumberCurrencyDisplayStyle { + Symbol, + Code, + Name, +} + +impl std::default::Default for FluentNumberCurrencyDisplayStyle { + fn default() -> Self { + Self::Symbol + } +} + +impl From<&str> for FluentNumberCurrencyDisplayStyle { + fn from(input: &str) -> Self { + match input { + "symbol" => Self::Symbol, + "code" => Self::Code, + "name" => Self::Name, + _ => Self::default(), + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct FluentNumberOptions { + 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 { + 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) { + ("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(Debug, PartialEq, Clone)] +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 = if frac_num > minfd { + 0 + } else { + minfd - 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<'l> From<FluentNumber> for FluentValue<'l> { + 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)); + } +} diff --git a/third_party/rust/fluent-bundle/src/types/plural.rs b/third_party/rust/fluent-bundle/src/types/plural.rs new file mode 100644 index 0000000000..1151fd6d36 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/types/plural.rs @@ -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)?)) + } +} |