fluent_bundle/
bundle.rs

1//! `FluentBundle` is a collection of localization messages in Fluent.
2//!
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
5//! together.
6
7use rustc_hash::FxHashMap;
8use std::borrow::Borrow;
9use std::borrow::Cow;
10use std::collections::hash_map::Entry as HashEntry;
11use std::default::Default;
12use std::fmt;
13
14use fluent_syntax::ast;
15use intl_memoizer::IntlLangMemoizer;
16use unic_langid::LanguageIdentifier;
17
18use crate::args::FluentArgs;
19use crate::entry::Entry;
20use crate::entry::GetEntry;
21use crate::errors::{EntryKind, FluentError};
22use crate::memoizer::MemoizerKind;
23use crate::message::FluentMessage;
24use crate::resolver::{ResolveValue, Scope, WriteValue};
25use crate::resource::FluentResource;
26use crate::types::FluentValue;
27
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.
30///
31/// # Examples
32///
33/// ```
34/// use fluent_bundle::{FluentBundle, FluentResource, FluentValue, FluentArgs};
35/// use unic_langid::langid;
36///
37/// // 1. Create a FluentResource
38///
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.");
42///
43///
44/// // 2. Create a FluentBundle
45///
46/// let langid_en = langid!("en-US");
47/// let mut bundle = FluentBundle::new(vec![langid_en]);
48///
49///
50/// // 3. Add the resource to the bundle
51///
52/// bundle.add_resource(&resource)
53///     .expect("Failed to add FTL resources to the bundle.");
54///
55///
56/// // 4. Retrieve a FluentMessage from the bundle
57///
58/// let msg = bundle.get_message("intro")
59///     .expect("Message doesn't exist.");
60///
61/// let mut args = FluentArgs::new();
62/// args.set("name", "Rustacean");
63///
64///
65/// // 5. Format the value of the message
66///
67/// let mut errors = vec![];
68///
69/// let pattern = msg.value()
70///     .expect("Message has no value.");
71///
72/// assert_eq!(
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}."
78/// );
79///
80/// ```
81///
82/// # `FluentBundle` Life Cycle
83///
84/// ## Create a bundle
85///
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.
88///
89/// Fluent uses [`LanguageIdentifier`] which can be created using `langid!` macro.
90///
91/// ## Add Resources
92///
93/// Next, call [`add_resource`](FluentBundle::add_resource) one or more times, supplying translations in the FTL syntax.
94///
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).
98///
99/// The [`FluentBundle`] instance is now ready to be used for localization.
100///
101/// ## Format
102///
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.
106///
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.
112///
113/// If errors were encountered during formatting, they will be
114/// accumulated in the [`Vec<FluentError>`](FluentError) passed as the third argument.
115///
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
118/// and fix them.
119///
120///
121/// # Locale Fallback Chain
122///
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.
127///
128/// # Concurrency
129///
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`][concurrent::IntlLangMemoizer] that uses [`Mutex`](std::sync::Mutex) and there's [`FluentBundle::new_concurrent`] which works with that.
134///
135/// [concurrent::IntlLangMemoizer]: https://docs.rs/intl-memoizer/latest/intl_memoizer/concurrent/struct.IntlLangMemoizer.html
136pub struct FluentBundle<R, M> {
137    pub locales: Vec<LanguageIdentifier>,
138    pub(crate) resources: Vec<R>,
139    pub(crate) entries: FxHashMap<String, Entry>,
140    pub(crate) intls: M,
141    pub(crate) use_isolating: bool,
142    pub(crate) transform: Option<fn(&str) -> Cow<str>>,
143    pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
144}
145
146impl<R, M> FluentBundle<R, M> {
147    /// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
148    ///
149    /// If any entry in the resource uses the same identifier as an already
150    /// existing key in the bundle, the new entry will be ignored and a
151    /// `FluentError::Overriding` will be added to the result.
152    ///
153    /// The method can take any type that can be borrowed to [`FluentResource`]:
154    ///   - `FluentResource`
155    ///   - `&FluentResource`
156    ///   - `Rc<FluentResource>`
157    ///   - `Arc<FluentResource>`
158    ///
159    /// This allows the user to introduce custom resource management and share
160    /// resources between instances of `FluentBundle`.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use fluent_bundle::{FluentBundle, FluentResource};
166    /// use unic_langid::langid;
167    ///
168    /// let ftl_string = String::from("
169    /// hello = Hi!
170    /// goodbye = Bye!
171    /// ");
172    /// let resource = FluentResource::try_new(ftl_string)
173    ///     .expect("Could not parse an FTL string.");
174    /// let langid_en = langid!("en-US");
175    /// let mut bundle = FluentBundle::new(vec![langid_en]);
176    /// bundle.add_resource(resource)
177    ///     .expect("Failed to add FTL resources to the bundle.");
178    /// assert_eq!(true, bundle.has_message("hello"));
179    /// ```
180    ///
181    /// # Whitespace
182    ///
183    /// Message ids must have no leading whitespace. Message values that span
184    /// multiple lines must have leading whitespace on all but the first line. These
185    /// are standard FTL syntax rules that may prove a bit troublesome in source
186    /// code formatting. The [`indoc!`] crate can help with stripping extra indentation
187    /// if you wish to indent your entire message.
188    ///
189    /// [FTL syntax]: https://projectfluent.org/fluent/guide/
190    /// [`indoc!`]: https://github.com/dtolnay/indoc
191    /// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
192    pub fn add_resource(&mut self, r: R) -> Result<(), Vec<FluentError>>
193    where
194        R: Borrow<FluentResource>,
195    {
196        let mut errors = vec![];
197
198        let res = r.borrow();
199        let res_pos = self.resources.len();
200
201        for (entry_pos, entry) in res.entries().enumerate() {
202            let (id, entry) = match entry {
203                ast::Entry::Message(ast::Message { ref id, .. }) => {
204                    (id.name, Entry::Message((res_pos, entry_pos)))
205                }
206                ast::Entry::Term(ast::Term { ref id, .. }) => {
207                    (id.name, Entry::Term((res_pos, entry_pos)))
208                }
209                _ => continue,
210            };
211
212            match self.entries.entry(id.to_string()) {
213                HashEntry::Vacant(empty) => {
214                    empty.insert(entry);
215                }
216                HashEntry::Occupied(_) => {
217                    let kind = match entry {
218                        Entry::Message(..) => EntryKind::Message,
219                        Entry::Term(..) => EntryKind::Term,
220                        _ => unreachable!(),
221                    };
222                    errors.push(FluentError::Overriding {
223                        kind,
224                        id: id.to_string(),
225                    });
226                }
227            }
228        }
229        self.resources.push(r);
230
231        if errors.is_empty() {
232            Ok(())
233        } else {
234            Err(errors)
235        }
236    }
237
238    /// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
239    ///
240    /// If any entry in the resource uses the same identifier as an already
241    /// existing key in the bundle, the entry will override the previous one.
242    ///
243    /// The method can take any type that can be borrowed as [`FluentResource`]:
244    ///   - `FluentResource`
245    ///   - `&FluentResource`
246    ///   - `Rc<FluentResource>`
247    ///   - `Arc<FluentResource>`
248    ///
249    /// This allows the user to introduce custom resource management and share
250    /// resources between instances of `FluentBundle`.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use fluent_bundle::{FluentBundle, FluentResource};
256    /// use unic_langid::langid;
257    ///
258    /// let ftl_string = String::from("
259    /// hello = Hi!
260    /// goodbye = Bye!
261    /// ");
262    /// let resource = FluentResource::try_new(ftl_string)
263    ///     .expect("Could not parse an FTL string.");
264    ///
265    /// let ftl_string = String::from("
266    /// hello = Another Hi!
267    /// ");
268    /// let resource2 = FluentResource::try_new(ftl_string)
269    ///     .expect("Could not parse an FTL string.");
270    ///
271    /// let langid_en = langid!("en-US");
272    ///
273    /// let mut bundle = FluentBundle::new(vec![langid_en]);
274    /// bundle.add_resource(resource)
275    ///     .expect("Failed to add FTL resources to the bundle.");
276    ///
277    /// bundle.add_resource_overriding(resource2);
278    ///
279    /// let mut errors = vec![];
280    /// let msg = bundle.get_message("hello")
281    ///     .expect("Failed to retrieve the message");
282    /// let value = msg.value().expect("Failed to retrieve the value of the message");
283    /// assert_eq!(bundle.format_pattern(value, None, &mut errors), "Another Hi!");
284    /// ```
285    ///
286    /// # Whitespace
287    ///
288    /// Message ids must have no leading whitespace. Message values that span
289    /// multiple lines must have leading whitespace on all but the first line. These
290    /// are standard FTL syntax rules that may prove a bit troublesome in source
291    /// code formatting. The [`indoc!`] crate can help with stripping extra indentation
292    /// if you wish to indent your entire message.
293    ///
294    /// [FTL syntax]: https://projectfluent.org/fluent/guide/
295    /// [`indoc!`]: https://github.com/dtolnay/indoc
296    /// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
297    pub fn add_resource_overriding(&mut self, r: R)
298    where
299        R: Borrow<FluentResource>,
300    {
301        let res = r.borrow();
302        let res_pos = self.resources.len();
303
304        for (entry_pos, entry) in res.entries().enumerate() {
305            let (id, entry) = match entry {
306                ast::Entry::Message(ast::Message { ref id, .. }) => {
307                    (id.name, Entry::Message((res_pos, entry_pos)))
308                }
309                ast::Entry::Term(ast::Term { ref id, .. }) => {
310                    (id.name, Entry::Term((res_pos, entry_pos)))
311                }
312                _ => continue,
313            };
314
315            self.entries.insert(id.to_string(), entry);
316        }
317        self.resources.push(r);
318    }
319
320    /// When formatting patterns, `FluentBundle` inserts
321    /// Unicode Directionality Isolation Marks to indicate
322    /// that the direction of a placeable may differ from
323    /// the surrounding message.
324    ///
325    /// This is important for cases such as when a
326    /// right-to-left user name is presented in the
327    /// left-to-right message.
328    ///
329    /// In some cases, such as testing, the user may want
330    /// to disable the isolating.
331    pub fn set_use_isolating(&mut self, value: bool) {
332        self.use_isolating = value;
333    }
334
335    /// This method allows to specify a function that will
336    /// be called on all textual fragments of the pattern
337    /// during formatting.
338    ///
339    /// This is currently primarily used for pseudolocalization,
340    /// and `fluent-pseudo` crate provides a function
341    /// that can be passed here.
342    pub fn set_transform(&mut self, func: Option<fn(&str) -> Cow<str>>) {
343        self.transform = func;
344    }
345
346    /// This method allows to specify a function that will
347    /// be called before any `FluentValue` is formatted
348    /// allowing overrides.
349    ///
350    /// It's particularly useful for plugging in an external
351    /// formatter for `FluentValue::Number`.
352    pub fn set_formatter(&mut self, func: Option<fn(&FluentValue, &M) -> Option<String>>) {
353        self.formatter = func;
354    }
355
356    /// Returns true if this bundle contains a message with the given id.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use fluent_bundle::{FluentBundle, FluentResource};
362    /// use unic_langid::langid;
363    ///
364    /// let ftl_string = String::from("hello = Hi!");
365    /// let resource = FluentResource::try_new(ftl_string)
366    ///     .expect("Failed to parse an FTL string.");
367    /// let langid_en = langid!("en-US");
368    /// let mut bundle = FluentBundle::new(vec![langid_en]);
369    /// bundle.add_resource(&resource)
370    ///     .expect("Failed to add FTL resources to the bundle.");
371    /// assert_eq!(true, bundle.has_message("hello"));
372    ///
373    /// ```
374    pub fn has_message(&self, id: &str) -> bool
375    where
376        R: Borrow<FluentResource>,
377    {
378        self.get_entry_message(id).is_some()
379    }
380
381    /// Retrieves a `FluentMessage` from a bundle.
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// use fluent_bundle::{FluentBundle, FluentResource};
387    /// use unic_langid::langid;
388    ///
389    /// let ftl_string = String::from("hello-world = Hello World!");
390    /// let resource = FluentResource::try_new(ftl_string)
391    ///     .expect("Failed to parse an FTL string.");
392    ///
393    /// let langid_en = langid!("en-US");
394    /// let mut bundle = FluentBundle::new(vec![langid_en]);
395    ///
396    /// bundle.add_resource(&resource)
397    ///     .expect("Failed to add FTL resources to the bundle.");
398    ///
399    /// let msg = bundle.get_message("hello-world");
400    /// assert_eq!(msg.is_some(), true);
401    /// ```
402    pub fn get_message<'l>(&'l self, id: &str) -> Option<FluentMessage<'l>>
403    where
404        R: Borrow<FluentResource>,
405    {
406        self.get_entry_message(id).map(Into::into)
407    }
408
409    /// Writes a formatted pattern which comes from a `FluentMessage`.
410    ///
411    /// # Example
412    ///
413    /// ```
414    /// use fluent_bundle::{FluentBundle, FluentResource};
415    /// use unic_langid::langid;
416    ///
417    /// let ftl_string = String::from("hello-world = Hello World!");
418    /// let resource = FluentResource::try_new(ftl_string)
419    ///     .expect("Failed to parse an FTL string.");
420    ///
421    /// let langid_en = langid!("en-US");
422    /// let mut bundle = FluentBundle::new(vec![langid_en]);
423    ///
424    /// bundle.add_resource(&resource)
425    ///     .expect("Failed to add FTL resources to the bundle.");
426    ///
427    /// let msg = bundle.get_message("hello-world")
428    ///     .expect("Failed to retrieve a FluentMessage.");
429    ///
430    /// let pattern = msg.value()
431    ///     .expect("Missing Value.");
432    /// let mut errors = vec![];
433    ///
434    /// let mut s = String::new();
435    /// bundle.write_pattern(&mut s, &pattern, None, &mut errors)
436    ///     .expect("Failed to write.");
437    ///
438    /// assert_eq!(s, "Hello World!");
439    /// ```
440    pub fn write_pattern<'bundle, W>(
441        &'bundle self,
442        w: &mut W,
443        pattern: &'bundle ast::Pattern<&str>,
444        args: Option<&'bundle FluentArgs>,
445        errors: &mut Vec<FluentError>,
446    ) -> fmt::Result
447    where
448        R: Borrow<FluentResource>,
449        W: fmt::Write,
450        M: MemoizerKind,
451    {
452        let mut scope = Scope::new(self, args, Some(errors));
453        pattern.write(w, &mut scope)
454    }
455
456    /// Formats a pattern which comes from a `FluentMessage`.
457    ///
458    /// # Example
459    ///
460    /// ```
461    /// use fluent_bundle::{FluentBundle, FluentResource};
462    /// use unic_langid::langid;
463    ///
464    /// let ftl_string = String::from("hello-world = Hello World!");
465    /// let resource = FluentResource::try_new(ftl_string)
466    ///     .expect("Failed to parse an FTL string.");
467    ///
468    /// let langid_en = langid!("en-US");
469    /// let mut bundle = FluentBundle::new(vec![langid_en]);
470    ///
471    /// bundle.add_resource(&resource)
472    ///     .expect("Failed to add FTL resources to the bundle.");
473    ///
474    /// let msg = bundle.get_message("hello-world")
475    ///     .expect("Failed to retrieve a FluentMessage.");
476    ///
477    /// let pattern = msg.value()
478    ///     .expect("Missing Value.");
479    /// let mut errors = vec![];
480    ///
481    /// let result = bundle.format_pattern(&pattern, None, &mut errors);
482    ///
483    /// assert_eq!(result, "Hello World!");
484    /// ```
485    pub fn format_pattern<'bundle, 'args>(
486        &'bundle self,
487        pattern: &'bundle ast::Pattern<&'bundle str>,
488        args: Option<&'args FluentArgs>,
489        errors: &mut Vec<FluentError>,
490    ) -> Cow<'bundle, str>
491    where
492        R: Borrow<FluentResource>,
493        M: MemoizerKind,
494    {
495        let mut scope = Scope::new(self, args, Some(errors));
496        let value = pattern.resolve(&mut scope);
497        value.into_string(&scope)
498    }
499
500    /// Makes the provided rust function available to messages with the name `id`. See
501    /// the [FTL syntax guide] to learn how these are used in messages.
502    ///
503    /// FTL functions accept both positional and named args. The rust function you
504    /// provide therefore has two parameters: a slice of values for the positional
505    /// args, and a `FluentArgs` for named args.
506    ///
507    /// # Examples
508    ///
509    /// ```
510    /// use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
511    /// use unic_langid::langid;
512    ///
513    /// let ftl_string = String::from("length = { STRLEN(\"12345\") }");
514    /// let resource = FluentResource::try_new(ftl_string)
515    ///     .expect("Could not parse an FTL string.");
516    /// let langid_en = langid!("en-US");
517    /// let mut bundle = FluentBundle::new(vec![langid_en]);
518    /// bundle.add_resource(&resource)
519    ///     .expect("Failed to add FTL resources to the bundle.");
520    ///
521    /// // Register a fn that maps from string to string length
522    /// bundle.add_function("STRLEN", |positional, _named| match positional {
523    ///     [FluentValue::String(str)] => str.len().into(),
524    ///     _ => FluentValue::Error,
525    /// }).expect("Failed to add a function to the bundle.");
526    ///
527    /// let msg = bundle.get_message("length").expect("Message doesn't exist.");
528    /// let mut errors = vec![];
529    /// let pattern = msg.value().expect("Message has no value.");
530    /// let value = bundle.format_pattern(&pattern, None, &mut errors);
531    /// assert_eq!(&value, "5");
532    /// ```
533    ///
534    /// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html
535    pub fn add_function<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
536    where
537        F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static,
538    {
539        match self.entries.entry(id.to_owned()) {
540            HashEntry::Vacant(entry) => {
541                entry.insert(Entry::Function(Box::new(func)));
542                Ok(())
543            }
544            HashEntry::Occupied(_) => Err(FluentError::Overriding {
545                kind: EntryKind::Function,
546                id: id.to_owned(),
547            }),
548        }
549    }
550}
551
552impl<R> Default for FluentBundle<R, IntlLangMemoizer> {
553    fn default() -> Self {
554        Self::new(vec![LanguageIdentifier::default()])
555    }
556}
557
558impl<R> FluentBundle<R, IntlLangMemoizer> {
559    /// Constructs a FluentBundle. The first element in `locales` should be the
560    /// language this bundle represents, and will be used to determine the
561    /// correct plural rules for this bundle. You can optionally provide extra
562    /// languages in the list; they will be used as fallback date and time
563    /// formatters if a formatter for the primary language is unavailable.
564    ///
565    /// # Examples
566    ///
567    /// ```
568    /// use fluent_bundle::FluentBundle;
569    /// use fluent_bundle::FluentResource;
570    /// use unic_langid::langid;
571    ///
572    /// let langid_en = langid!("en-US");
573    /// let mut bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid_en]);
574    /// ```
575    ///
576    /// # Errors
577    ///
578    /// This will panic if no formatters can be found for the locales.
579    pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
580        let first_locale = locales.get(0).cloned().unwrap_or_default();
581        Self {
582            locales,
583            resources: vec![],
584            entries: FxHashMap::default(),
585            intls: IntlLangMemoizer::new(first_locale),
586            use_isolating: true,
587            transform: None,
588            formatter: None,
589        }
590    }
591}
592
593impl crate::memoizer::MemoizerKind for IntlLangMemoizer {
594    fn new(lang: LanguageIdentifier) -> Self
595    where
596        Self: Sized,
597    {
598        Self::new(lang)
599    }
600
601    fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
602    where
603        Self: Sized,
604        I: intl_memoizer::Memoizable + Send + Sync + 'static,
605        I::Args: Send + Sync + 'static,
606        U: FnOnce(&I) -> R,
607    {
608        self.with_try_get(args, cb)
609    }
610
611    fn stringify_value(
612        &self,
613        value: &dyn crate::types::FluentType,
614    ) -> std::borrow::Cow<'static, str> {
615        value.as_string(self)
616    }
617}