Skip to main content

fluent_templates/
lib.rs

1//! # Fluent Templates: A High level Fluent API.
2//!
3//! `fluent-templates` lets you to easily integrate Fluent localisation into
4//! your Rust application or library. It does this by providing a high level
5//! "loader" API that loads fluent strings based on simple language negotiation,
6//! and the `FluentLoader` struct which is a `Loader` agnostic container type
7//! that comes with optional trait implementations for popular templating
8//! engines such as handlebars or tera that allow you to be able to use your
9//! localisations in your templates with no boilerplate.
10//!
11//! ## Loaders
12//! Currently this crate provides two different kinds of loaders that cover two
13//! main use cases.
14//!
15//! - [`static_loader!`] — A procedural macro that loads your fluent resources
16//!   at *compile-time* into your binary and creates a new [`StaticLoader`]
17//!   static variable that allows you to access the localisations.
18//!   `static_loader!` is most useful when you want to localise your
19//!   application and want to ship your fluent resources with your binary.
20//!
21//! - [`ArcLoader`] — A struct that loads your fluent resources at *run-time*
22//!   using `Arc` as its backing storage. `ArcLoader` is most useful for when
23//!   you want to be able to change and/or update localisations at run-time, or
24//!   if you're writing a developer tool that wants to provide fluent
25//!   localisation in your own application such as a static site generator.
26//!
27//!
28//! ## `static_loader!`
29//! The easiest way to use `fluent-templates` is to use the [`static_loader!`]
30//! procedural macro that will create a new [`StaticLoader`] static variable.
31//!
32//! ### Basic Example
33//! ```
34//! fluent_templates::static_loader! {
35//!     // Declare our `StaticLoader` named `LOCALES`.
36//!     static LOCALES = {
37//!         // The directory of localisations and fluent resources.
38//!         locales: "./tests/locales",
39//!         // The language to falback on if something is not present.
40//!         fallback_language: "en-US",
41//!         // Optional: A fluent resource that is shared with every locale.
42//!         core_locales: "./tests/locales/core.ftl",
43//!     };
44//! }
45//! # fn main() {}
46//! ```
47//!
48//! ### Customise Example
49//! You can also modify each `FluentBundle` on initialisation to be able to
50//! change configuration or add resources from Rust.
51//! ```
52//! use std::sync::LazyLock;
53//! use fluent_bundle::FluentResource;
54//! use fluent_templates::static_loader;
55//!
56//! static_loader! {
57//!     // Declare our `StaticLoader` named `LOCALES`.
58//!     static LOCALES = {
59//!         // The directory of localisations and fluent resources.
60//!         locales: "./tests/locales",
61//!         // The language to falback on if something is not present.
62//!         fallback_language: "en-US",
63//!         // Optional: A fluent resource that is shared with every locale.
64//!         core_locales: "./tests/locales/core.ftl",
65//!         // Optional: A function that is run over each fluent bundle.
66//!         customise: |bundle| {
67//!             // Since this will be called for each locale bundle and
68//!             // `FluentResource`s need to be either `&'static` or behind an
69//!             // `Arc` it's recommended you use lazily initialised
70//!             // static variables.
71//!             static CRATE_VERSION_FTL: LazyLock<FluentResource> = LazyLock::new(|| {
72//!                 let ftl_string = String::from(
73//!                     concat!("-crate-version = {}", env!("CARGO_PKG_VERSION"))
74//!                 );
75//!
76//!                 FluentResource::try_new(ftl_string).unwrap()
77//!             });
78//!
79//!             bundle.add_resource(&CRATE_VERSION_FTL);
80//!         }
81//!     };
82//! }
83//! # fn main() {}
84//! ```
85//!
86//! ## Locales Directory
87//! `fluent-templates` will collect all subdirectories that match a valid
88//! [Unicode Language Identifier][uli] and bundle all fluent files found in
89//! those directories and map those resources to the respective identifier.
90//! It also supports locale files named `locales/<lang>.ftl`, which are merged
91//! with any resources found in `locales/<lang>/`.
92//! `fluent-templates` will recurse through each language directory as needed
93//! and will respect any `.gitignore` or `.ignore` files present.
94//!
95//! [uli]: https://docs.rs/unic-langid/0.9.0/unic_langid/
96//!
97//! ### Example Layout
98//! ```text
99//! locales
100//! ├── core.ftl
101//! ├── en-US.ftl
102//! ├── en-US
103//! │   └── extra.ftl
104//! ├── fr
105//! │   └── main.ftl
106//! ├── zh-CN
107//! │   └── main.ftl
108//! └── zh-TW
109//!     └── main.ftl
110//! ```
111//!
112//! ### Looking up fluent resources
113//! You can use the [`Loader`] trait to `lookup` a given fluent resource, and
114//! provide any additional arguments as needed with `lookup_with_args`. You
115//! can also look up attributes by appending a `.` to the name of the message.
116//!
117//! #### Example
118//! ```fluent
119//!  # In `locales/en-US/main.ftl`
120//!  hello-world = Hello World!
121//!  greeting = Hello { $name }!
122//!         .placeholder = Hello Friend!
123//!
124//!  # In `locales/fr/main.ftl`
125//!  hello-world = Bonjour le monde!
126//!  greeting = Bonjour { $name }!
127//!         .placeholder = Salut l'ami!
128//!
129//!  # In `locales/de/main.ftl`
130//!  hello-world = Hallo Welt!
131//!  greeting = Hallo { $name }!
132//!         .placeholder = Hallo Fruend!
133//! ```
134//!
135//! ```
136//! use std::{borrow::Cow, collections::HashMap};
137//!
138//! use unic_langid::{LanguageIdentifier, langid};
139//! use fluent_templates::{Loader, static_loader};
140//!
141//!const US_ENGLISH: LanguageIdentifier = langid!("en-US");
142//!const FRENCH: LanguageIdentifier = langid!("fr");
143//!const GERMAN: LanguageIdentifier = langid!("de");
144//!
145//! static_loader! {
146//!     static LOCALES = {
147//!         locales: "./tests/locales",
148//!         fallback_language: "en-US",
149//!         // Removes unicode isolating marks around arguments, you typically
150//!         // should only set to false when testing.
151//!         customise: |bundle| bundle.set_use_isolating(false),
152//!     };
153//! }
154//!
155//! fn main() {
156//!     assert_eq!("Hello World!", LOCALES.lookup(&US_ENGLISH, "hello-world"));
157//!     assert_eq!("Bonjour le monde!", LOCALES.lookup(&FRENCH, "hello-world"));
158//!     assert_eq!("Hallo Welt!", LOCALES.lookup(&GERMAN, "hello-world"));
159//!
160//!     assert_eq!("Hello World!", LOCALES.try_lookup(&US_ENGLISH, "hello-world").unwrap());
161//!     assert_eq!("Bonjour le monde!", LOCALES.try_lookup(&FRENCH, "hello-world").unwrap());
162//!     assert_eq!("Hallo Welt!", LOCALES.try_lookup(&GERMAN, "hello-world").unwrap());
163//!
164//!     let args = {
165//!         let mut map = HashMap::new();
166//!         map.insert(Cow::from("name"), "Alice".into());
167//!         map
168//!     };
169//!
170//!     assert_eq!("Hello Friend!", LOCALES.lookup(&US_ENGLISH, "greeting.placeholder"));
171//!     assert_eq!("Hello Alice!", LOCALES.lookup_with_args(&US_ENGLISH, "greeting", &args));
172//!     assert_eq!("Salut l'ami!", LOCALES.lookup(&FRENCH, "greeting.placeholder"));
173//!     assert_eq!("Bonjour Alice!", LOCALES.lookup_with_args(&FRENCH, "greeting", &args));
174//!     assert_eq!("Hallo Fruend!", LOCALES.lookup(&GERMAN, "greeting.placeholder"));
175//!     assert_eq!("Hallo Alice!", LOCALES.lookup_with_args(&GERMAN, "greeting", &args));
176//!
177//!     assert_eq!("Hello Friend!", LOCALES.try_lookup(&US_ENGLISH, "greeting.placeholder").unwrap());
178//!     assert_eq!("Hello Alice!", LOCALES.try_lookup_with_args(&US_ENGLISH, "greeting", &args).unwrap());
179//!     assert_eq!("Salut l'ami!", LOCALES.try_lookup(&FRENCH, "greeting.placeholder").unwrap());
180//!     assert_eq!("Bonjour Alice!", LOCALES.try_lookup_with_args(&FRENCH, "greeting", &args).unwrap());
181//!     assert_eq!("Hallo Fruend!", LOCALES.try_lookup(&GERMAN, "greeting.placeholder").unwrap());
182//!     assert_eq!("Hallo Alice!", LOCALES.try_lookup_with_args(&GERMAN, "greeting", &args).unwrap());
183//!
184//!
185//!     let args = {
186//!         let mut map = HashMap::new();
187//!         map.insert(Cow::Borrowed("param"), "1".into());
188//!         map.insert(Cow::Owned(format!("{}-param", "multi-word")), "2".into());
189//!         map
190//!     };
191//!
192//!     assert_eq!("text one 1 second 2", LOCALES.lookup_with_args(&US_ENGLISH, "parameter2", &args));
193//!     assert_eq!("texte une 1 seconde 2", LOCALES.lookup_with_args(&FRENCH, "parameter2", &args));
194//!
195//!     assert_eq!("text one 1 second 2", LOCALES.try_lookup_with_args(&US_ENGLISH, "parameter2", &args).unwrap());
196//!     assert_eq!("texte une 1 seconde 2", LOCALES.try_lookup_with_args(&FRENCH, "parameter2", &args).unwrap());
197//! }
198//! ```
199//!
200//! ### Tera
201//! With the `tera` feature you can use `FluentLoader` as a Tera function.
202//! It accepts a `key` parameter pointing to a fluent resource and `lang` for
203//! what language to get that key for. Optionally you can pass extra arguments
204//! to the function as arguments to the resource. `fluent-templates` will
205//! automatically convert argument keys from Tera's `snake_case` to the fluent's
206//! preferred `kebab-case` arguments.
207//! The `lang` parameter is optional when the default language of the corresponding
208//! `FluentLoader` is set (see [`FluentLoader::with_default_lang`]).
209//!
210//! ```toml
211//!fluent-templates = { version = "*", features = ["tera"] }
212//!```
213//!
214//! ```rust
215//! use fluent_templates::{FluentLoader, static_loader};
216//!
217//! static_loader! {
218//!     static LOCALES = {
219//!         locales: "./tests/locales",
220//!         fallback_language: "en-US",
221//!         // Removes unicode isolating marks around arguments, you typically
222//!         // should only set to false when testing.
223//!         customise: |bundle| bundle.set_use_isolating(false),
224//!     };
225//! }
226//!
227//! fn main() {
228//! #   #[cfg(feature = "tera")] {
229//!         let mut tera = tera::Tera::default();
230//!         let ctx = tera::Context::default();
231//!         tera.register_function("fluent", FluentLoader::new(&*LOCALES));
232//!         assert_eq!(
233//!             "Hello World!",
234//!             tera.render_str(r#"{{ fluent(key="hello-world", lang="en-US") }}"#, &ctx).unwrap()
235//!         );
236//!         assert_eq!(
237//!             "Hello Alice!",
238//!             tera.render_str(r#"{{ fluent(key="greeting", lang="en-US", name="Alice") }}"#, &ctx).unwrap()
239//!         );
240//!     }
241//! # }
242//! ```
243//!
244//! ### Handlebars
245//! In handlebars, `fluent-templates` will read the `lang` field in your
246//! [`handlebars::Context`] while rendering.
247//!
248//! ```toml
249//!fluent-templates = { version = "*", features = ["handlebars"] }
250//!```
251//!
252//! ```rust
253//! use fluent_templates::{FluentLoader, static_loader};
254//!
255//! static_loader! {
256//!     static LOCALES = {
257//!         locales: "./tests/locales",
258//!         fallback_language: "en-US",
259//!         // Removes unicode isolating marks around arguments, you typically
260//!         // should only set to false when testing.
261//!         customise: |bundle| bundle.set_use_isolating(false),
262//!     };
263//! }
264//!
265//! fn main() {
266//! # #[cfg(feature = "handlebars")] {
267//!     let mut handlebars = handlebars::Handlebars::new();
268//!     handlebars.register_helper("fluent", Box::new(FluentLoader::new(&*LOCALES)));
269//!     let data = serde_json::json!({"lang": "zh-CN"});
270//!     assert_eq!("Hello World!", handlebars.render_template(r#"{{fluent "hello-world"}}"#, &data).unwrap());
271//!     assert_eq!("Hello Alice!", handlebars.render_template(r#"{{fluent "greeting" name="Alice"}}"#, &data).unwrap());
272//! # }
273//! }
274//! ```
275//!
276//! ### Handlebars helper syntax.
277//! The main helper provided is the `{{fluent}}` helper. If you have the
278//! following Fluent file:
279//!
280//! ```fluent
281//! foo-bar = "foo bar"
282//! placeholder = this has a placeholder { $variable }
283//! placeholder2 = this has { $variable1 } { $variable2 }
284//! ```
285//!
286//! You can include the strings in your template with
287//!
288//! ```hbs
289//! <!-- will render "foo bar" -->
290//! {{fluent "foo-bar"}}
291//! <!-- will render "this has a placeholder baz" -->
292//! {{fluent "placeholder" variable="baz"}}
293//!```
294//!
295//! You may also use the `{{fluentparam}}` helper to specify [variables],
296//! especially if you need them to be multiline.
297//!
298//! ```hbs
299//! {{#fluent "placeholder2"}}
300//!     {{#fluentparam "variable1"}}
301//!         first line
302//!         second line
303//!     {{/fluentparam}}
304//!     {{#fluentparam "variable2"}}
305//!         first line
306//!         second line
307//!     {{/fluentparam}}
308//! {{/fluent}}
309//! ```
310//!
311//!
312//! [variables]: https://projectfluent.org/fluent/guide/variables.html
313//! [`static_loader!`]: ./macro.static_loader.html
314//! [`StaticLoader`]: ./struct.StaticLoader.html
315//! [`ArcLoader`]: ./struct.ArcLoader.html
316//! [`FluentLoader::with_default_lang`]: ./struct.FluentLoader.html#method.with_default_lang
317//! [`handlebars::Context`]: https://docs.rs/handlebars/3.1.0/handlebars/struct.Context.html
318#![warn(missing_docs)]
319
320#[doc(hidden)]
321pub extern crate fluent_bundle;
322
323#[doc(hidden)]
324pub type FluentBundle<R> =
325    fluent_bundle::bundle::FluentBundle<R, intl_memoizer::concurrent::IntlLangMemoizer>;
326
327pub use error::LoaderError;
328pub use loader::{ArcLoader, ArcLoaderBuilder, FluentLoader, Loader, MultiLoader, StaticLoader};
329
330mod error;
331#[doc(hidden)]
332pub mod fs;
333mod languages;
334#[doc(hidden)]
335pub mod loader;
336
337#[cfg(feature = "macros")]
338pub use fluent_template_macros::static_loader;
339#[cfg(feature = "macros")]
340pub use unic_langid::langid;
341pub use unic_langid::LanguageIdentifier;
342
343/// A convenience `Result` type that defaults to `error::Loader`.
344pub type Result<T, E = error::LoaderError> = std::result::Result<T, E>;
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::Loader;
350    use unic_langid::{langid, LanguageIdentifier};
351
352    #[test]
353    fn check_if_loader_is_object_safe() {
354        const US_ENGLISH: LanguageIdentifier = langid!("en-US");
355
356        let loader = ArcLoader::builder("./tests/locales", US_ENGLISH)
357            .customize(|bundle| bundle.set_use_isolating(false))
358            .build()
359            .unwrap();
360
361        let loader: Box<dyn Loader> = Box::new(loader);
362        assert_eq!("Hello World!", loader.lookup(&US_ENGLISH, "hello-world"));
363    }
364}