fluent_templates/
lib.rs

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

#[doc(hidden)]
pub extern crate fluent_bundle;

#[doc(hidden)]
pub type FluentBundle<R> =
    fluent_bundle::bundle::FluentBundle<R, intl_memoizer::concurrent::IntlLangMemoizer>;

pub use error::LoaderError;
pub use loader::{ArcLoader, ArcLoaderBuilder, FluentLoader, Loader, MultiLoader, StaticLoader};

mod error;
#[doc(hidden)]
pub mod fs;
mod languages;
#[doc(hidden)]
pub mod loader;

#[cfg(feature = "macros")]
pub use fluent_template_macros::static_loader;
#[cfg(feature = "macros")]
pub use unic_langid::langid;
pub use unic_langid::LanguageIdentifier;

#[doc(hidden)]
pub use once_cell;

/// A convenience `Result` type that defaults to `error::Loader`.
pub type Result<T, E = error::LoaderError> = std::result::Result<T, E>;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Loader;
    use unic_langid::{langid, LanguageIdentifier};

    #[test]
    fn check_if_loader_is_object_safe() {
        const US_ENGLISH: LanguageIdentifier = langid!("en-US");

        let loader = ArcLoader::builder("./tests/locales", US_ENGLISH)
            .customize(|bundle| bundle.set_use_isolating(false))
            .build()
            .unwrap();

        let loader: Box<dyn Loader> = Box::new(loader);
        assert_eq!("Hello World!", loader.lookup(&US_ENGLISH, "hello-world"));
    }
}