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

I18n testing #846

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]

# 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 }
}

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