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}