Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Askama base crate i18n support #845

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
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