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"));
}
}