Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Askama base crate i18n support #845

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions askama/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ with-mendes = ["askama_derive/with-mendes"]
with-rocket = ["askama_derive/with-rocket"]
with-tide = ["askama_derive/with-tide"]
with-warp = ["askama_derive/with-warp"]
i18n = ["askama_derive/i18n", "fluent-templates"]
Copy link

@Ben-PH Ben-PH Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this is a fluid backed i18n, would it be appropriate to self-document that in the feature name? e.g. i18n_fluent?


# deprecated
mime = []
Expand All @@ -48,6 +49,7 @@ percent-encoding = { version = "2.1.0", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
fluent-templates = { version = "0.8.0", optional = true }

[package.metadata.docs.rs]
features = ["config", "humansize", "num-traits", "serde-json", "serde-yaml"]
74 changes: 74 additions & 0 deletions askama/src/i18n.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Module for compile time checked localization
//!
//! # Example:
//!
//! [Fluent Translation List](https://projectfluent.org/) resource file `i18n/es-MX/basic.ftl`:
//!
//! ```ftl
//! greeting = ¡Hola, { $name }!
//! ```
//!
//! Askama HTML template `templates/example.html`:
//!
//! ```html
//! <h1>{{ localize("greeting", name: name) }}</h1>
//! ```
//!
//! Rust usage:
//! ```ignore
//! use askama::i18n::{langid, Locale};
//! use askama::Template;
//!
//! askama::i18n::load!(LOCALES);
//!
//! #[derive(Template)]
//! #[template(path = "example.html")]
//! struct ExampleTemplate<'a> {
//! #[locale]
//! loc: Locale<'a>,
//! name: &'a str,
//! }
//!
//! let template = ExampleTemplate {
//! loc: Locale::new(langid!("es-MX"), &LOCALES),
//! name: "Hilda",
//! };
//!
//! // "<h1>¡Hola, Hilda!</h1>"
//! template.render().unwrap();
//! ```

use std::collections::HashMap;
use std::iter::FromIterator;

// Re-export conventiently as `askama::i18n::load!()`.
// Proc-macro crates can only export macros from their root namespace.
/// Load locales at compile time. See example above for usage.
pub use askama_derive::i18n_load as load;

pub use fluent_templates::{self, fluent_bundle::FluentValue, fs::langid, LanguageIdentifier};
use fluent_templates::{Loader, StaticLoader};

pub struct Locale<'a> {
loader: &'a StaticLoader,
language: LanguageIdentifier,
}

impl Locale<'_> {
pub fn new(language: LanguageIdentifier, loader: &'static StaticLoader) -> Self {
Self { loader, language }
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc-comment here would be handy.

pub fn translate<'a>(
&self,
msg_id: &str,
args: impl IntoIterator<Item = (&'a str, FluentValue<'a>)>,
) -> Option<String> {
let args = HashMap::<&str, FluentValue<'_>>::from_iter(args);
let args = match args.is_empty() {
true => None,
false => Some(&args),
};
self.loader.lookup_complete(&self.language, msg_id, args)
}
}
2 changes: 2 additions & 0 deletions askama/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
mod error;
pub mod filters;
pub mod helpers;
#[cfg(feature = "i18n")]
pub mod i18n;

use std::fmt;

Expand Down
3 changes: 3 additions & 0 deletions askama_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ with-mendes = []
with-rocket = []
with-tide = []
with-warp = []
i18n = ["fluent-syntax", "fluent-templates", "serde", "basic-toml"]

[dependencies]
mime = "0.3"
Expand All @@ -39,3 +40,5 @@ quote = "1"
serde = { version = "1.0", optional = true, features = ["derive"] }
syn = "2"
basic-toml = { version = "0.1.1", optional = true }
fluent-syntax = { version = "0.11.0", optional = true, default-features = false }
fluent-templates = { version = "0.8.0", optional = true, default-features = false }
31 changes: 31 additions & 0 deletions askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1376,9 +1376,40 @@ impl<'a> Generator<'a> {
Expr::RustMacro(ref path, args) => self.visit_rust_macro(buf, path, args),
Expr::Try(ref expr) => self.visit_try(buf, expr.as_ref())?,
Expr::Tuple(ref exprs) => self.visit_tuple(buf, exprs)?,
Expr::Localize(ref msg_id, ref args) => self.visit_localize(buf, msg_id, args)?,
})
}

fn visit_localize(
&mut self,
buf: &mut Buffer,
msg_id: &Expr<'_>,
args: &[(&str, Expr<'_>)],
) -> Result<DisplayWrap, CompileError> {
let localizer =
self.input.localizer.as_deref().ok_or(
"You need to annotate a field with #[locale] to use the localize() function.",
)?;

buf.write(&format!(
"self.{}.translate(",
normalize_identifier(localizer)
));
self.visit_expr(buf, msg_id)?;
buf.writeln(", [")?;
buf.indent();
for (k, v) in args {
buf.write(&format!("({:?}, ::askama::i18n::FluentValue::from(", k));
self.visit_expr(buf, v)?;
buf.writeln(")),")?;
Comment on lines +1401 to +1404
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personal preference for key and val

}
buf.dedent()?;
// Safe to unwrap, as `msg_id` is checked at compile time.
buf.write("]).unwrap()");

Ok(DisplayWrap::Unwrapped)
}

fn visit_try(
&mut self,
buf: &mut Buffer,
Expand Down
Loading