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//! `fluent-templates` will recurse through each language directory as needed
91//! and will respect any `.gitignore` or `.ignore` files present.
92//!
93//! [uli]: https://docs.rs/unic-langid/0.9.0/unic_langid/
94//!
95//! ### Example Layout
96//! ```text
97//! locales
98//! ├── core.ftl
99//! ├── en-US
100//! │ └── main.ftl
101//! ├── fr
102//! │ └── main.ftl
103//! ├── zh-CN
104//! │ └── main.ftl
105//! └── zh-TW
106//! └── main.ftl
107//! ```
108//!
109//! ### Looking up fluent resources
110//! You can use the [`Loader`] trait to `lookup` a given fluent resource, and
111//! provide any additional arguments as needed with `lookup_with_args`. You
112//! can also look up attributes by appending a `.` to the name of the message.
113//!
114//! #### Example
115//! ```fluent
116//! # In `locales/en-US/main.ftl`
117//! hello-world = Hello World!
118//! greeting = Hello { $name }!
119//! .placeholder = Hello Friend!
120//!
121//! # In `locales/fr/main.ftl`
122//! hello-world = Bonjour le monde!
123//! greeting = Bonjour { $name }!
124//! .placeholder = Salut l'ami!
125//!
126//! # In `locales/de/main.ftl`
127//! hello-world = Hallo Welt!
128//! greeting = Hallo { $name }!
129//! .placeholder = Hallo Fruend!
130//! ```
131//!
132//! ```
133//! use std::{borrow::Cow, collections::HashMap};
134//!
135//! use unic_langid::{LanguageIdentifier, langid};
136//! use fluent_templates::{Loader, static_loader};
137//!
138//!const US_ENGLISH: LanguageIdentifier = langid!("en-US");
139//!const FRENCH: LanguageIdentifier = langid!("fr");
140//!const GERMAN: LanguageIdentifier = langid!("de");
141//!
142//! static_loader! {
143//! static LOCALES = {
144//! locales: "./tests/locales",
145//! fallback_language: "en-US",
146//! // Removes unicode isolating marks around arguments, you typically
147//! // should only set to false when testing.
148//! customise: |bundle| bundle.set_use_isolating(false),
149//! };
150//! }
151//!
152//! fn main() {
153//! assert_eq!("Hello World!", LOCALES.lookup(&US_ENGLISH, "hello-world"));
154//! assert_eq!("Bonjour le monde!", LOCALES.lookup(&FRENCH, "hello-world"));
155//! assert_eq!("Hallo Welt!", LOCALES.lookup(&GERMAN, "hello-world"));
156//!
157//! assert_eq!("Hello World!", LOCALES.try_lookup(&US_ENGLISH, "hello-world").unwrap());
158//! assert_eq!("Bonjour le monde!", LOCALES.try_lookup(&FRENCH, "hello-world").unwrap());
159//! assert_eq!("Hallo Welt!", LOCALES.try_lookup(&GERMAN, "hello-world").unwrap());
160//!
161//! let args = {
162//! let mut map = HashMap::new();
163//! map.insert(Cow::from("name"), "Alice".into());
164//! map
165//! };
166//!
167//! assert_eq!("Hello Friend!", LOCALES.lookup(&US_ENGLISH, "greeting.placeholder"));
168//! assert_eq!("Hello Alice!", LOCALES.lookup_with_args(&US_ENGLISH, "greeting", &args));
169//! assert_eq!("Salut l'ami!", LOCALES.lookup(&FRENCH, "greeting.placeholder"));
170//! assert_eq!("Bonjour Alice!", LOCALES.lookup_with_args(&FRENCH, "greeting", &args));
171//! assert_eq!("Hallo Fruend!", LOCALES.lookup(&GERMAN, "greeting.placeholder"));
172//! assert_eq!("Hallo Alice!", LOCALES.lookup_with_args(&GERMAN, "greeting", &args));
173//!
174//! assert_eq!("Hello Friend!", LOCALES.try_lookup(&US_ENGLISH, "greeting.placeholder").unwrap());
175//! assert_eq!("Hello Alice!", LOCALES.try_lookup_with_args(&US_ENGLISH, "greeting", &args).unwrap());
176//! assert_eq!("Salut l'ami!", LOCALES.try_lookup(&FRENCH, "greeting.placeholder").unwrap());
177//! assert_eq!("Bonjour Alice!", LOCALES.try_lookup_with_args(&FRENCH, "greeting", &args).unwrap());
178//! assert_eq!("Hallo Fruend!", LOCALES.try_lookup(&GERMAN, "greeting.placeholder").unwrap());
179//! assert_eq!("Hallo Alice!", LOCALES.try_lookup_with_args(&GERMAN, "greeting", &args).unwrap());
180//!
181//!
182//! let args = {
183//! let mut map = HashMap::new();
184//! map.insert(Cow::Borrowed("param"), "1".into());
185//! map.insert(Cow::Owned(format!("{}-param", "multi-word")), "2".into());
186//! map
187//! };
188//!
189//! assert_eq!("text one 1 second 2", LOCALES.lookup_with_args(&US_ENGLISH, "parameter2", &args));
190//! assert_eq!("texte une 1 seconde 2", LOCALES.lookup_with_args(&FRENCH, "parameter2", &args));
191//!
192//! assert_eq!("text one 1 second 2", LOCALES.try_lookup_with_args(&US_ENGLISH, "parameter2", &args).unwrap());
193//! assert_eq!("texte une 1 seconde 2", LOCALES.try_lookup_with_args(&FRENCH, "parameter2", &args).unwrap());
194//! }
195//! ```
196//!
197//! ### Tera
198//! With the `tera` feature you can use `FluentLoader` as a Tera function.
199//! It accepts a `key` parameter pointing to a fluent resource and `lang` for
200//! what language to get that key for. Optionally you can pass extra arguments
201//! to the function as arguments to the resource. `fluent-templates` will
202//! automatically convert argument keys from Tera's `snake_case` to the fluent's
203//! preferred `kebab-case` arguments.
204//! The `lang` parameter is optional when the default language of the corresponding
205//! `FluentLoader` is set (see [`FluentLoader::with_default_lang`]).
206//!
207//! ```toml
208//!fluent-templates = { version = "*", features = ["tera"] }
209//!```
210//!
211//! ```rust
212//! use fluent_templates::{FluentLoader, static_loader};
213//!
214//! static_loader! {
215//! static LOCALES = {
216//! locales: "./tests/locales",
217//! fallback_language: "en-US",
218//! // Removes unicode isolating marks around arguments, you typically
219//! // should only set to false when testing.
220//! customise: |bundle| bundle.set_use_isolating(false),
221//! };
222//! }
223//!
224//! fn main() {
225//! # #[cfg(feature = "tera")] {
226//! let mut tera = tera::Tera::default();
227//! let ctx = tera::Context::default();
228//! tera.register_function("fluent", FluentLoader::new(&*LOCALES));
229//! assert_eq!(
230//! "Hello World!",
231//! tera.render_str(r#"{{ fluent(key="hello-world", lang="en-US") }}"#, &ctx).unwrap()
232//! );
233//! assert_eq!(
234//! "Hello Alice!",
235//! tera.render_str(r#"{{ fluent(key="greeting", lang="en-US", name="Alice") }}"#, &ctx).unwrap()
236//! );
237//! }
238//! # }
239//! ```
240//!
241//! ### Handlebars
242//! In handlebars, `fluent-templates` will read the `lang` field in your
243//! [`handlebars::Context`] while rendering.
244//!
245//! ```toml
246//!fluent-templates = { version = "*", features = ["handlebars"] }
247//!```
248//!
249//! ```rust
250//! use fluent_templates::{FluentLoader, static_loader};
251//!
252//! static_loader! {
253//! static LOCALES = {
254//! locales: "./tests/locales",
255//! fallback_language: "en-US",
256//! // Removes unicode isolating marks around arguments, you typically
257//! // should only set to false when testing.
258//! customise: |bundle| bundle.set_use_isolating(false),
259//! };
260//! }
261//!
262//! fn main() {
263//! # #[cfg(feature = "handlebars")] {
264//! let mut handlebars = handlebars::Handlebars::new();
265//! handlebars.register_helper("fluent", Box::new(FluentLoader::new(&*LOCALES)));
266//! let data = serde_json::json!({"lang": "zh-CN"});
267//! assert_eq!("Hello World!", handlebars.render_template(r#"{{fluent "hello-world"}}"#, &data).unwrap());
268//! assert_eq!("Hello Alice!", handlebars.render_template(r#"{{fluent "greeting" name="Alice"}}"#, &data).unwrap());
269//! # }
270//! }
271//! ```
272//!
273//! ### Handlebars helper syntax.
274//! The main helper provided is the `{{fluent}}` helper. If you have the
275//! following Fluent file:
276//!
277//! ```fluent
278//! foo-bar = "foo bar"
279//! placeholder = this has a placeholder { $variable }
280//! placeholder2 = this has { $variable1 } { $variable2 }
281//! ```
282//!
283//! You can include the strings in your template with
284//!
285//! ```hbs
286//! <!-- will render "foo bar" -->
287//! {{fluent "foo-bar"}}
288//! <!-- will render "this has a placeholder baz" -->
289//! {{fluent "placeholder" variable="baz"}}
290//!```
291//!
292//! You may also use the `{{fluentparam}}` helper to specify [variables],
293//! especially if you need them to be multiline.
294//!
295//! ```hbs
296//! {{#fluent "placeholder2"}}
297//! {{#fluentparam "variable1"}}
298//! first line
299//! second line
300//! {{/fluentparam}}
301//! {{#fluentparam "variable2"}}
302//! first line
303//! second line
304//! {{/fluentparam}}
305//! {{/fluent}}
306//! ```
307//!
308//!
309//! [variables]: https://projectfluent.org/fluent/guide/variables.html
310//! [`static_loader!`]: ./macro.static_loader.html
311//! [`StaticLoader`]: ./struct.StaticLoader.html
312//! [`ArcLoader`]: ./struct.ArcLoader.html
313//! [`FluentLoader::with_default_lang`]: ./struct.FluentLoader.html#method.with_default_lang
314//! [`handlebars::Context`]: https://docs.rs/handlebars/3.1.0/handlebars/struct.Context.html
315#![warn(missing_docs)]
316
317#[doc(hidden)]
318pub extern crate fluent_bundle;
319
320#[doc(hidden)]
321pub type FluentBundle<R> =
322 fluent_bundle::bundle::FluentBundle<R, intl_memoizer::concurrent::IntlLangMemoizer>;
323
324pub use error::LoaderError;
325pub use loader::{ArcLoader, ArcLoaderBuilder, FluentLoader, Loader, MultiLoader, StaticLoader};
326
327mod error;
328#[doc(hidden)]
329pub mod fs;
330mod languages;
331#[doc(hidden)]
332pub mod loader;
333
334#[cfg(feature = "macros")]
335pub use fluent_template_macros::static_loader;
336#[cfg(feature = "macros")]
337pub use unic_langid::langid;
338pub use unic_langid::LanguageIdentifier;
339
340/// A convenience `Result` type that defaults to `error::Loader`.
341pub type Result<T, E = error::LoaderError> = std::result::Result<T, E>;
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::Loader;
347 use unic_langid::{langid, LanguageIdentifier};
348
349 #[test]
350 fn check_if_loader_is_object_safe() {
351 const US_ENGLISH: LanguageIdentifier = langid!("en-US");
352
353 let loader = ArcLoader::builder("./tests/locales", US_ENGLISH)
354 .customize(|bundle| bundle.set_use_isolating(false))
355 .build()
356 .unwrap();
357
358 let loader: Box<dyn Loader> = Box::new(loader);
359 assert_eq!("Hello World!", loader.lookup(&US_ENGLISH, "hello-world"));
360 }
361}