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`](intl_memoizer::concurrent::IntlLangMemoizer) that uses [`Mutex`](std::sync::Mutex) and there's [`FluentBundle::new_concurrent`] which works with that.
134pub 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>>,
142}
143
144impl<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![];
195
196 let res = r.borrow();
197 let res_pos = self.resources.len();
198
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 };
209
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);
228
229 if errors.is_empty() {
230 Ok(())
231 } else {
232 Err(errors)
233 }
234 }
235
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();
301
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 };
312
313 self.entries.insert(id.to_string(), entry);
314 }
315 self.resources.push(r);
316 }
317
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 }
332
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 }
343
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 }
353
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 }
378
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 }
406
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 }
453
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 }
497
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 }
548}
549
550impl<R> Default for FluentBundle<R, IntlLangMemoizer> {
551 fn default() -> Self {
552 Self::new(locales:vec![LanguageIdentifier::default()])
553 }
554}
555
556impl<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 }
589}
590
591impl crate::memoizer::MemoizerKind for IntlLangMemoizer {
592 fn new(lang: LanguageIdentifier) -> Self
593 where
594 Self: Sized,
595 {
596 Self::new(lang)
597 }
598
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 }
608
609 fn stringify_value(
610 &self,
611 value: &dyn crate::types::FluentType,
612 ) -> std::borrow::Cow<'static, str> {
613 value.as_string(self)
614 }
615}
616