Bug 1069931 - Implement 'discard' element from SVG Animations r=emilio,hsivonen,dholb...
[gecko.git] / third_party / rust / fluent-bundle / src / bundle.rs
blob3d085cfee541e0898748001b611f168945ad89bc
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.
7 use rustc_hash::FxHashMap;
8 use std::borrow::Borrow;
9 use std::borrow::Cow;
10 use std::collections::hash_map::Entry as HashEntry;
11 use std::default::Default;
12 use std::fmt;
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.
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.
101 /// ## Format
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
118 /// and fix them.
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.
128 /// # Concurrency
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>,
138     pub(crate) intls: M,
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.
146     ///
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.
150     ///
151     /// The method can take any type that can be borrowed to `FluentResource`:
152     ///   - FluentResource
153     ///   - &FluentResource
154     ///   - Rc<FluentResource>
155     ///   - Arc<FluentResurce>
156     ///
157     /// This allows the user to introduce custom resource management and share
158     /// resources between instances of `FluentBundle`.
159     ///
160     /// # Examples
161     ///
162     /// ```
163     /// use fluent_bundle::{FluentBundle, FluentResource};
164     /// use unic_langid::langid;
165     ///
166     /// let ftl_string = String::from("
167     /// hello = Hi!
168     /// goodbye = Bye!
169     /// ");
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"));
177     /// ```
178     ///
179     /// # Whitespace
180     ///
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.
186     ///
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>>
191     where
192         R: Borrow<FluentResource>,
193     {
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)))
203                 }
204                 ast::Entry::Term(ast::Term { ref id, .. }) => {
205                     (id.name, Entry::Term((res_pos, entry_pos)))
206                 }
207                 _ => continue,
208             };
210             match self.entries.entry(id.to_string()) {
211                 HashEntry::Vacant(empty) => {
212                     empty.insert(entry);
213                 }
214                 HashEntry::Occupied(_) => {
215                     let kind = match entry {
216                         Entry::Message(..) => EntryKind::Message,
217                         Entry::Term(..) => EntryKind::Term,
218                         _ => unreachable!(),
219                     };
220                     errors.push(FluentError::Overriding {
221                         kind,
222                         id: id.to_string(),
223                     });
224                 }
225             }
226         }
227         self.resources.push(r);
229         if errors.is_empty() {
230             Ok(())
231         } else {
232             Err(errors)
233         }
234     }
236     /// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
237     ///
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.
240     ///
241     /// The method can take any type that can be borrowed as FluentResource:
242     ///   - FluentResource
243     ///   - &FluentResource
244     ///   - Rc<FluentResource>
245     ///   - Arc<FluentResurce>
246     ///
247     /// This allows the user to introduce custom resource management and share
248     /// resources between instances of `FluentBundle`.
249     ///
250     /// # Examples
251     ///
252     /// ```
253     /// use fluent_bundle::{FluentBundle, FluentResource};
254     /// use unic_langid::langid;
255     ///
256     /// let ftl_string = String::from("
257     /// hello = Hi!
258     /// goodbye = Bye!
259     /// ");
260     /// let resource = FluentResource::try_new(ftl_string)
261     ///     .expect("Could not parse an FTL string.");
262     ///
263     /// let ftl_string = String::from("
264     /// hello = Another Hi!
265     /// ");
266     /// let resource2 = FluentResource::try_new(ftl_string)
267     ///     .expect("Could not parse an FTL string.");
268     ///
269     /// let langid_en = langid!("en-US");
270     ///
271     /// let mut bundle = FluentBundle::new(vec![langid_en]);
272     /// bundle.add_resource(resource)
273     ///     .expect("Failed to add FTL resources to the bundle.");
274     ///
275     /// bundle.add_resource_overriding(resource2);
276     ///
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!");
282     /// ```
283     ///
284     /// # Whitespace
285     ///
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.
291     ///
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)
296     where
297         R: Borrow<FluentResource>,
298     {
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)))
306                 }
307                 ast::Entry::Term(ast::Term { ref id, .. }) => {
308                     (id.name, Entry::Term((res_pos, entry_pos)))
309                 }
310                 _ => continue,
311             };
313             self.entries.insert(id.to_string(), entry);
314         }
315         self.resources.push(r);
316     }
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.
322     ///
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.
326     ///
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;
331     }
333     /// This method allows to specify a function that will
334     /// be called on all textual fragments of the pattern
335     /// during formatting.
336     ///
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;
342     }
344     /// This method allows to specify a function that will
345     /// be called before any `FluentValue` is formatted
346     /// allowing overrides.
347     ///
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;
352     }
354     /// Returns true if this bundle contains a message with the given id.
355     ///
356     /// # Examples
357     ///
358     /// ```
359     /// use fluent_bundle::{FluentBundle, FluentResource};
360     /// use unic_langid::langid;
361     ///
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"));
370     ///
371     /// ```
372     pub fn has_message(&self, id: &str) -> bool
373     where
374         R: Borrow<FluentResource>,
375     {
376         self.get_entry_message(id).is_some()
377     }
379     /// Retrieves a `FluentMessage` from a bundle.
380     ///
381     /// # Examples
382     ///
383     /// ```
384     /// use fluent_bundle::{FluentBundle, FluentResource};
385     /// use unic_langid::langid;
386     ///
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.");
390     ///
391     /// let langid_en = langid!("en-US");
392     /// let mut bundle = FluentBundle::new(vec![langid_en]);
393     ///
394     /// bundle.add_resource(&resource)
395     ///     .expect("Failed to add FTL resources to the bundle.");
396     ///
397     /// let msg = bundle.get_message("hello-world");
398     /// assert_eq!(msg.is_some(), true);
399     /// ```
400     pub fn get_message<'l>(&'l self, id: &str) -> Option<FluentMessage<'l>>
401     where
402         R: Borrow<FluentResource>,
403     {
404         self.get_entry_message(id).map(Into::into)
405     }
407     /// Writes a formatted pattern which comes from a `FluentMessage`.
408     ///
409     /// # Example
410     ///
411     /// ```
412     /// use fluent_bundle::{FluentBundle, FluentResource};
413     /// use unic_langid::langid;
414     ///
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.");
418     ///
419     /// let langid_en = langid!("en-US");
420     /// let mut bundle = FluentBundle::new(vec![langid_en]);
421     ///
422     /// bundle.add_resource(&resource)
423     ///     .expect("Failed to add FTL resources to the bundle.");
424     ///
425     /// let msg = bundle.get_message("hello-world")
426     ///     .expect("Failed to retrieve a FluentMessage.");
427     ///
428     /// let pattern = msg.value()
429     ///     .expect("Missing Value.");
430     /// let mut errors = vec![];
431     ///
432     /// let mut s = String::new();
433     /// bundle.write_pattern(&mut s, &pattern, None, &mut errors)
434     ///     .expect("Failed to write.");
435     ///
436     /// assert_eq!(s, "Hello World!");
437     /// ```
438     pub fn write_pattern<'bundle, W>(
439         &'bundle self,
440         w: &mut W,
441         pattern: &'bundle ast::Pattern<&str>,
442         args: Option<&'bundle FluentArgs>,
443         errors: &mut Vec<FluentError>,
444     ) -> fmt::Result
445     where
446         R: Borrow<FluentResource>,
447         W: fmt::Write,
448         M: MemoizerKind,
449     {
450         let mut scope = Scope::new(self, args, Some(errors));
451         pattern.write(w, &mut scope)
452     }
454     /// Formats a pattern which comes from a `FluentMessage`.
455     ///
456     /// # Example
457     ///
458     /// ```
459     /// use fluent_bundle::{FluentBundle, FluentResource};
460     /// use unic_langid::langid;
461     ///
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.");
465     ///
466     /// let langid_en = langid!("en-US");
467     /// let mut bundle = FluentBundle::new(vec![langid_en]);
468     ///
469     /// bundle.add_resource(&resource)
470     ///     .expect("Failed to add FTL resources to the bundle.");
471     ///
472     /// let msg = bundle.get_message("hello-world")
473     ///     .expect("Failed to retrieve a FluentMessage.");
474     ///
475     /// let pattern = msg.value()
476     ///     .expect("Missing Value.");
477     /// let mut errors = vec![];
478     ///
479     /// let result = bundle.format_pattern(&pattern, None, &mut errors);
480     ///
481     /// assert_eq!(result, "Hello World!");
482     /// ```
483     pub fn format_pattern<'bundle>(
484         &'bundle self,
485         pattern: &'bundle ast::Pattern<&str>,
486         args: Option<&'bundle FluentArgs>,
487         errors: &mut Vec<FluentError>,
488     ) -> Cow<'bundle, str>
489     where
490         R: Borrow<FluentResource>,
491         M: MemoizerKind,
492     {
493         let mut scope = Scope::new(self, args, Some(errors));
494         let value = pattern.resolve(&mut scope);
495         value.as_string(&scope)
496     }
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.
500     ///
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.
504     ///
505     /// # Examples
506     ///
507     /// ```
508     /// use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
509     /// use unic_langid::langid;
510     ///
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.");
518     ///
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.");
524     ///
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");
530     /// ```
531     ///
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>
534     where
535         F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static,
536     {
537         match self.entries.entry(id.to_owned()) {
538             HashEntry::Vacant(entry) => {
539                 entry.insert(Entry::Function(Box::new(func)));
540                 Ok(())
541             }
542             HashEntry::Occupied(_) => Err(FluentError::Overriding {
543                 kind: EntryKind::Function,
544                 id: id.to_owned(),
545             }),
546         }
547     }
550 impl<R> Default for FluentBundle<R, IntlLangMemoizer> {
551     fn default() -> Self {
552         Self::new(vec![LanguageIdentifier::default()])
553     }
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.
562     ///
563     /// # Examples
564     ///
565     /// ```
566     /// use fluent_bundle::FluentBundle;
567     /// use fluent_bundle::FluentResource;
568     /// use unic_langid::langid;
569     ///
570     /// let langid_en = langid!("en-US");
571     /// let mut bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid_en]);
572     /// ```
573     ///
574     /// # Errors
575     ///
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();
579         Self {
580             locales,
581             resources: vec![],
582             entries: FxHashMap::default(),
583             intls: IntlLangMemoizer::new(first_locale),
584             use_isolating: true,
585             transform: None,
586             formatter: None,
587         }
588     }
591 impl crate::memoizer::MemoizerKind for IntlLangMemoizer {
592     fn new(lang: LanguageIdentifier) -> Self
593     where
594         Self: Sized,
595     {
596         Self::new(lang)
597     }
599     fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
600     where
601         Self: Sized,
602         I: intl_memoizer::Memoizable + Send + Sync + 'static,
603         I::Args: Send + Sync + 'static,
604         U: FnOnce(&I) -> R,
605     {
606         self.with_try_get(args, cb)
607     }
609     fn stringify_value(
610         &self,
611         value: &dyn crate::types::FluentType,
612     ) -> std::borrow::Cow<'static, str> {
613         value.as_string(self)
614     }