//! `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`](std::rc::Rc) or [`Arc`](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`](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) 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 { pub locales: Vec, pub(crate) resources: Vec, pub(crate) entries: FxHashMap, pub(crate) intls: M, pub(crate) use_isolating: bool, pub(crate) transform: Option Cow>, pub(crate) formatter: Option Option>, } impl FluentBundle { /// Adds a resource to the bundle, returning an empty [`Result`] 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 /// - Arc /// /// 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`]: https://doc.rust-lang.org/std/result/enum.Result.html pub fn add_resource(&mut self, r: R) -> Result<(), Vec> where R: Borrow, { 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`] 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 /// - Arc /// /// 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`]: https://doc.rust-lang.org/std/result/enum.Result.html pub fn add_resource_overriding(&mut self, r: R) where R: Borrow, { 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 Cow>) { 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 Option>) { 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, { 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> where R: Borrow, { 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, ) -> fmt::Result where R: Borrow, 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, ) -> Cow<'bundle, str> where R: Borrow, 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(&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 Default for FluentBundle { fn default() -> Self { Self::new(vec![LanguageIdentifier::default()]) } } impl FluentBundle { /// 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 = FluentBundle::new(vec![langid_en]); /// ``` /// /// # Errors /// /// This will panic if no formatters can be found for the locales. pub fn new(locales: Vec) -> 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(&self, args: I::Args, cb: U) -> Result 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) } }