1 //! `FluentBundle` is a collection of localization messages in Fluent.
3 //! It stores a list of messages in a single locale which can reference one another, use the same
4 //! internationalization formatters, functions, scopeironmental variables and are expected to be used
7 use rustc_hash::FxHashMap;
8 use std::borrow::Borrow;
10 use std::collections::hash_map::Entry as HashEntry;
11 use std::default::Default;
14 use fluent_syntax::ast;
15 use intl_memoizer::IntlLangMemoizer;
16 use unic_langid::LanguageIdentifier;
18 use crate::args::FluentArgs;
19 use crate::entry::Entry;
20 use crate::entry::GetEntry;
21 use crate::errors::{EntryKind, FluentError};
22 use crate::memoizer::MemoizerKind;
23 use crate::message::FluentMessage;
24 use crate::resolver::{ResolveValue, Scope, WriteValue};
25 use crate::resource::FluentResource;
26 use crate::types::FluentValue;
28 /// A collection of localization messages for a single locale, which are meant
29 /// to be used together in a single view, widget or any other UI abstraction.
34 /// use fluent_bundle::{FluentBundle, FluentResource, FluentValue, FluentArgs};
35 /// use unic_langid::langid;
37 /// // 1. Create a FluentResource
39 /// let ftl_string = String::from("intro = Welcome, { $name }.");
40 /// let resource = FluentResource::try_new(ftl_string)
41 /// .expect("Could not parse an FTL string.");
44 /// // 2. Create a FluentBundle
46 /// let langid_en = langid!("en-US");
47 /// let mut bundle = FluentBundle::new(vec![langid_en]);
50 /// // 3. Add the resource to the bundle
52 /// bundle.add_resource(&resource)
53 /// .expect("Failed to add FTL resources to the bundle.");
56 /// // 4. Retrieve a FluentMessage from the bundle
58 /// let msg = bundle.get_message("intro")
59 /// .expect("Message doesn't exist.");
61 /// let mut args = FluentArgs::new();
62 /// args.set("name", "Rustacean");
65 /// // 5. Format the value of the message
67 /// let mut errors = vec![];
69 /// let pattern = msg.value()
70 /// .expect("Message has no value.");
73 /// bundle.format_pattern(&pattern, Some(&args), &mut errors),
74 /// // The placeholder is wrapper in Unicode Directionality Marks
75 /// // to indicate that the placeholder may be of different direction
76 /// // than surrounding string.
77 /// "Welcome, \u{2068}Rustacean\u{2069}."
82 /// # `FluentBundle` Life Cycle
84 /// ## Create a bundle
86 /// To create a bundle, call [`FluentBundle::new`] with a locale list that represents the best
87 /// possible fallback chain for a given locale. The simplest case is a one-locale list.
89 /// Fluent uses [`LanguageIdentifier`] which can be created using `langid!` macro.
93 /// Next, call [`add_resource`](FluentBundle::add_resource) one or more times, supplying translations in the FTL syntax.
95 /// Since [`FluentBundle`] is generic over anything that can borrow a [`FluentResource`],
96 /// one can use [`FluentBundle`] to own its resources, store references to them,
97 /// or even [`Rc<FluentResource>`](std::rc::Rc) or [`Arc<FluentResource>`](std::sync::Arc).
99 /// The [`FluentBundle`] instance is now ready to be used for localization.
103 /// To format a translation, call [`get_message`](FluentBundle::get_message) to retrieve a [`FluentMessage`],
104 /// and then call [`format_pattern`](FluentBundle::format_pattern) on the message value or attribute in order to
105 /// retrieve the translated string.
107 /// The result of [`format_pattern`](FluentBundle::format_pattern) is an
108 /// [`Cow<str>`](std::borrow::Cow). It is
109 /// recommended to treat the result as opaque from the perspective of the program and use it only
110 /// to display localized messages. Do not examine it or alter in any way before displaying. This
111 /// is a general good practice as far as all internationalization operations are concerned.
113 /// If errors were encountered during formatting, they will be
114 /// accumulated in the [`Vec<FluentError>`](FluentError) passed as the third argument.
116 /// While they are not fatal, they usually indicate problems with the translation,
117 /// and should be logged or reported in a way that allows the developer to notice
121 /// # Locale Fallback Chain
123 /// [`FluentBundle`] stores messages in a single locale, but keeps a locale fallback chain for the
124 /// purpose of language negotiation with i18n formatters. For instance, if date and time formatting
125 /// are not available in the first locale, [`FluentBundle`] will use its `locales` fallback chain
126 /// to negotiate a sensible fallback for date and time formatting.
130 /// As you may have noticed, [`fluent_bundle::FluentBundle`](crate::FluentBundle) is a specialization of [`fluent_bundle::bundle::FluentBundle`](crate::bundle::FluentBundle)
131 /// which works with an [`IntlLangMemoizer`] over [`RefCell`](std::cell::RefCell).
132 /// In scenarios where the memoizer must work concurrently, there's an implementation of
133 /// [`IntlLangMemoizer`](intl_memoizer::concurrent::IntlLangMemoizer) that uses [`Mutex`](std::sync::Mutex) and there's [`FluentBundle::new_concurrent`] which works with that.
134 pub struct FluentBundle<R, M> {
135 pub locales: Vec<LanguageIdentifier>,
136 pub(crate) resources: Vec<R>,
137 pub(crate) entries: FxHashMap<String, Entry>,
139 pub(crate) use_isolating: bool,
140 pub(crate) transform: Option<fn(&str) -> Cow<str>>,
141 pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
144 impl<R, M> FluentBundle<R, M> {
145 /// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
147 /// If any entry in the resource uses the same identifier as an already
148 /// existing key in the bundle, the new entry will be ignored and a
149 /// `FluentError::Overriding` will be added to the result.
151 /// The method can take any type that can be borrowed to `FluentResource`:
153 /// - &FluentResource
154 /// - Rc<FluentResource>
155 /// - Arc<FluentResurce>
157 /// This allows the user to introduce custom resource management and share
158 /// resources between instances of `FluentBundle`.
163 /// use fluent_bundle::{FluentBundle, FluentResource};
164 /// use unic_langid::langid;
166 /// let ftl_string = String::from("
170 /// let resource = FluentResource::try_new(ftl_string)
171 /// .expect("Could not parse an FTL string.");
172 /// let langid_en = langid!("en-US");
173 /// let mut bundle = FluentBundle::new(vec![langid_en]);
174 /// bundle.add_resource(resource)
175 /// .expect("Failed to add FTL resources to the bundle.");
176 /// assert_eq!(true, bundle.has_message("hello"));
181 /// Message ids must have no leading whitespace. Message values that span
182 /// multiple lines must have leading whitespace on all but the first line. These
183 /// are standard FTL syntax rules that may prove a bit troublesome in source
184 /// code formatting. The [`indoc!`] crate can help with stripping extra indentation
185 /// if you wish to indent your entire message.
187 /// [FTL syntax]: https://projectfluent.org/fluent/guide/
188 /// [`indoc!`]: https://github.com/dtolnay/indoc
189 /// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
190 pub fn add_resource(&mut self, r: R) -> Result<(), Vec<FluentError>>
192 R: Borrow<FluentResource>,
194 let mut errors = vec![];
196 let res = r.borrow();
197 let res_pos = self.resources.len();
199 for (entry_pos, entry) in res.entries().enumerate() {
200 let (id, entry) = match entry {
201 ast::Entry::Message(ast::Message { ref id, .. }) => {
202 (id.name, Entry::Message((res_pos, entry_pos)))
204 ast::Entry::Term(ast::Term { ref id, .. }) => {
205 (id.name, Entry::Term((res_pos, entry_pos)))
210 match self.entries.entry(id.to_string()) {
211 HashEntry::Vacant(empty) => {
214 HashEntry::Occupied(_) => {
215 let kind = match entry {
216 Entry::Message(..) => EntryKind::Message,
217 Entry::Term(..) => EntryKind::Term,
220 errors.push(FluentError::Overriding {
227 self.resources.push(r);
229 if errors.is_empty() {
236 /// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
238 /// If any entry in the resource uses the same identifier as an already
239 /// existing key in the bundle, the entry will override the previous one.
241 /// The method can take any type that can be borrowed as FluentResource:
243 /// - &FluentResource
244 /// - Rc<FluentResource>
245 /// - Arc<FluentResurce>
247 /// This allows the user to introduce custom resource management and share
248 /// resources between instances of `FluentBundle`.
253 /// use fluent_bundle::{FluentBundle, FluentResource};
254 /// use unic_langid::langid;
256 /// let ftl_string = String::from("
260 /// let resource = FluentResource::try_new(ftl_string)
261 /// .expect("Could not parse an FTL string.");
263 /// let ftl_string = String::from("
264 /// hello = Another Hi!
266 /// let resource2 = FluentResource::try_new(ftl_string)
267 /// .expect("Could not parse an FTL string.");
269 /// let langid_en = langid!("en-US");
271 /// let mut bundle = FluentBundle::new(vec![langid_en]);
272 /// bundle.add_resource(resource)
273 /// .expect("Failed to add FTL resources to the bundle.");
275 /// bundle.add_resource_overriding(resource2);
277 /// let mut errors = vec![];
278 /// let msg = bundle.get_message("hello")
279 /// .expect("Failed to retrieve the message");
280 /// let value = msg.value().expect("Failed to retrieve the value of the message");
281 /// assert_eq!(bundle.format_pattern(value, None, &mut errors), "Another Hi!");
286 /// Message ids must have no leading whitespace. Message values that span
287 /// multiple lines must have leading whitespace on all but the first line. These
288 /// are standard FTL syntax rules that may prove a bit troublesome in source
289 /// code formatting. The [`indoc!`] crate can help with stripping extra indentation
290 /// if you wish to indent your entire message.
292 /// [FTL syntax]: https://projectfluent.org/fluent/guide/
293 /// [`indoc!`]: https://github.com/dtolnay/indoc
294 /// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
295 pub fn add_resource_overriding(&mut self, r: R)
297 R: Borrow<FluentResource>,
299 let res = r.borrow();
300 let res_pos = self.resources.len();
302 for (entry_pos, entry) in res.entries().enumerate() {
303 let (id, entry) = match entry {
304 ast::Entry::Message(ast::Message { ref id, .. }) => {
305 (id.name, Entry::Message((res_pos, entry_pos)))
307 ast::Entry::Term(ast::Term { ref id, .. }) => {
308 (id.name, Entry::Term((res_pos, entry_pos)))
313 self.entries.insert(id.to_string(), entry);
315 self.resources.push(r);
318 /// When formatting patterns, `FluentBundle` inserts
319 /// Unicode Directionality Isolation Marks to indicate
320 /// that the direction of a placeable may differ from
321 /// the surrounding message.
323 /// This is important for cases such as when a
324 /// right-to-left user name is presented in the
325 /// left-to-right message.
327 /// In some cases, such as testing, the user may want
328 /// to disable the isolating.
329 pub fn set_use_isolating(&mut self, value: bool) {
330 self.use_isolating = value;
333 /// This method allows to specify a function that will
334 /// be called on all textual fragments of the pattern
335 /// during formatting.
337 /// This is currently primarly used for pseudolocalization,
338 /// and `fluent-pseudo` crate provides a function
339 /// that can be passed here.
340 pub fn set_transform(&mut self, func: Option<fn(&str) -> Cow<str>>) {
341 self.transform = func;
344 /// This method allows to specify a function that will
345 /// be called before any `FluentValue` is formatted
346 /// allowing overrides.
348 /// It's particularly useful for plugging in an external
349 /// formatter for `FluentValue::Number`.
350 pub fn set_formatter(&mut self, func: Option<fn(&FluentValue, &M) -> Option<String>>) {
351 self.formatter = func;
354 /// Returns true if this bundle contains a message with the given id.
359 /// use fluent_bundle::{FluentBundle, FluentResource};
360 /// use unic_langid::langid;
362 /// let ftl_string = String::from("hello = Hi!");
363 /// let resource = FluentResource::try_new(ftl_string)
364 /// .expect("Failed to parse an FTL string.");
365 /// let langid_en = langid!("en-US");
366 /// let mut bundle = FluentBundle::new(vec![langid_en]);
367 /// bundle.add_resource(&resource)
368 /// .expect("Failed to add FTL resources to the bundle.");
369 /// assert_eq!(true, bundle.has_message("hello"));
372 pub fn has_message(&self, id: &str) -> bool
374 R: Borrow<FluentResource>,
376 self.get_entry_message(id).is_some()
379 /// Retrieves a `FluentMessage` from a bundle.
384 /// use fluent_bundle::{FluentBundle, FluentResource};
385 /// use unic_langid::langid;
387 /// let ftl_string = String::from("hello-world = Hello World!");
388 /// let resource = FluentResource::try_new(ftl_string)
389 /// .expect("Failed to parse an FTL string.");
391 /// let langid_en = langid!("en-US");
392 /// let mut bundle = FluentBundle::new(vec![langid_en]);
394 /// bundle.add_resource(&resource)
395 /// .expect("Failed to add FTL resources to the bundle.");
397 /// let msg = bundle.get_message("hello-world");
398 /// assert_eq!(msg.is_some(), true);
400 pub fn get_message<'l>(&'l self, id: &str) -> Option<FluentMessage<'l>>
402 R: Borrow<FluentResource>,
404 self.get_entry_message(id).map(Into::into)
407 /// Writes a formatted pattern which comes from a `FluentMessage`.
412 /// use fluent_bundle::{FluentBundle, FluentResource};
413 /// use unic_langid::langid;
415 /// let ftl_string = String::from("hello-world = Hello World!");
416 /// let resource = FluentResource::try_new(ftl_string)
417 /// .expect("Failed to parse an FTL string.");
419 /// let langid_en = langid!("en-US");
420 /// let mut bundle = FluentBundle::new(vec![langid_en]);
422 /// bundle.add_resource(&resource)
423 /// .expect("Failed to add FTL resources to the bundle.");
425 /// let msg = bundle.get_message("hello-world")
426 /// .expect("Failed to retrieve a FluentMessage.");
428 /// let pattern = msg.value()
429 /// .expect("Missing Value.");
430 /// let mut errors = vec![];
432 /// let mut s = String::new();
433 /// bundle.write_pattern(&mut s, &pattern, None, &mut errors)
434 /// .expect("Failed to write.");
436 /// assert_eq!(s, "Hello World!");
438 pub fn write_pattern<'bundle, W>(
441 pattern: &'bundle ast::Pattern<&str>,
442 args: Option<&'bundle FluentArgs>,
443 errors: &mut Vec<FluentError>,
446 R: Borrow<FluentResource>,
450 let mut scope = Scope::new(self, args, Some(errors));
451 pattern.write(w, &mut scope)
454 /// Formats a pattern which comes from a `FluentMessage`.
459 /// use fluent_bundle::{FluentBundle, FluentResource};
460 /// use unic_langid::langid;
462 /// let ftl_string = String::from("hello-world = Hello World!");
463 /// let resource = FluentResource::try_new(ftl_string)
464 /// .expect("Failed to parse an FTL string.");
466 /// let langid_en = langid!("en-US");
467 /// let mut bundle = FluentBundle::new(vec![langid_en]);
469 /// bundle.add_resource(&resource)
470 /// .expect("Failed to add FTL resources to the bundle.");
472 /// let msg = bundle.get_message("hello-world")
473 /// .expect("Failed to retrieve a FluentMessage.");
475 /// let pattern = msg.value()
476 /// .expect("Missing Value.");
477 /// let mut errors = vec![];
479 /// let result = bundle.format_pattern(&pattern, None, &mut errors);
481 /// assert_eq!(result, "Hello World!");
483 pub fn format_pattern<'bundle>(
485 pattern: &'bundle ast::Pattern<&str>,
486 args: Option<&'bundle FluentArgs>,
487 errors: &mut Vec<FluentError>,
488 ) -> Cow<'bundle, str>
490 R: Borrow<FluentResource>,
493 let mut scope = Scope::new(self, args, Some(errors));
494 let value = pattern.resolve(&mut scope);
495 value.as_string(&scope)
498 /// Makes the provided rust function available to messages with the name `id`. See
499 /// the [FTL syntax guide] to learn how these are used in messages.
501 /// FTL functions accept both positional and named args. The rust function you
502 /// provide therefore has two parameters: a slice of values for the positional
503 /// args, and a `FluentArgs` for named args.
508 /// use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
509 /// use unic_langid::langid;
511 /// let ftl_string = String::from("length = { STRLEN(\"12345\") }");
512 /// let resource = FluentResource::try_new(ftl_string)
513 /// .expect("Could not parse an FTL string.");
514 /// let langid_en = langid!("en-US");
515 /// let mut bundle = FluentBundle::new(vec![langid_en]);
516 /// bundle.add_resource(&resource)
517 /// .expect("Failed to add FTL resources to the bundle.");
519 /// // Register a fn that maps from string to string length
520 /// bundle.add_function("STRLEN", |positional, _named| match positional {
521 /// [FluentValue::String(str)] => str.len().into(),
522 /// _ => FluentValue::Error,
523 /// }).expect("Failed to add a function to the bundle.");
525 /// let msg = bundle.get_message("length").expect("Message doesn't exist.");
526 /// let mut errors = vec![];
527 /// let pattern = msg.value().expect("Message has no value.");
528 /// let value = bundle.format_pattern(&pattern, None, &mut errors);
529 /// assert_eq!(&value, "5");
532 /// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html
533 pub fn add_function<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
535 F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static,
537 match self.entries.entry(id.to_owned()) {
538 HashEntry::Vacant(entry) => {
539 entry.insert(Entry::Function(Box::new(func)));
542 HashEntry::Occupied(_) => Err(FluentError::Overriding {
543 kind: EntryKind::Function,
550 impl<R> Default for FluentBundle<R, IntlLangMemoizer> {
551 fn default() -> Self {
552 Self::new(vec![LanguageIdentifier::default()])
556 impl<R> FluentBundle<R, IntlLangMemoizer> {
557 /// Constructs a FluentBundle. The first element in `locales` should be the
558 /// language this bundle represents, and will be used to determine the
559 /// correct plural rules for this bundle. You can optionally provide extra
560 /// languages in the list; they will be used as fallback date and time
561 /// formatters if a formatter for the primary language is unavailable.
566 /// use fluent_bundle::FluentBundle;
567 /// use fluent_bundle::FluentResource;
568 /// use unic_langid::langid;
570 /// let langid_en = langid!("en-US");
571 /// let mut bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid_en]);
576 /// This will panic if no formatters can be found for the locales.
577 pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
578 let first_locale = locales.get(0).cloned().unwrap_or_default();
582 entries: FxHashMap::default(),
583 intls: IntlLangMemoizer::new(first_locale),
591 impl crate::memoizer::MemoizerKind for IntlLangMemoizer {
592 fn new(lang: LanguageIdentifier) -> Self
599 fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
602 I: intl_memoizer::Memoizable + Send + Sync + 'static,
603 I::Args: Send + Sync + 'static,
606 self.with_try_get(args, cb)
611 value: &dyn crate::types::FluentType,
612 ) -> std::borrow::Cow<'static, str> {
613 value.as_string(self)