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

I18n #700

Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
f8af562
added language files for tests
89Q12 Jun 16, 2022
e9b1be5
added htmls files to test translation
89Q12 Jun 16, 2022
9877915
added tests
89Q12 Jun 16, 2022
2eb0fdf
added parser for localize(foo, bar: baz)
89Q12 Jun 16, 2022
7d11fd2
added tests for the localize parser
89Q12 Jun 16, 2022
8336929
added visit_localize, arm: Expr::Localize, field:localized_messages
89Q12 Jun 16, 2022
72dda5e
added extraction for the locale field and localizer field
89Q12 Jun 16, 2022
858ea69
added locale attribute
89Q12 Jun 16, 2022
465483d
corrected locale attribute
89Q12 Jun 16, 2022
39a1370
added feature localization
89Q12 Jun 16, 2022
47cbb51
added #[cfg(feature = "localization")]
89Q12 Jun 16, 2022
1874729
Fixed errors and added comment
89Q12 Jun 16, 2022
98ff019
fix cargo files
89Q12 Jun 16, 2022
07db0e7
fix test: test_parse_nested_localize
89Q12 Jun 16, 2022
8bd252e
added #![cfg(feature = "localization")] to i18n tests
89Q12 Jun 16, 2022
c96ada6
added askama::Local struct with impl
89Q12 Jun 17, 2022
d1a46fe
changed tests to use askama::Local
89Q12 Jun 17, 2022
cdbc7d0
removed init_translation macro since its not needed anymore
89Q12 Jun 17, 2022
4c7300c
refactored no args test adn its template
89Q12 Jun 17, 2022
7e1f5e6
Merge branch 'main' of https://github.com/11Tuvork28/askama into i18n
89Q12 Jun 17, 2022
17f95e7
removed last todos
89Q12 Jun 17, 2022
2c827f2
Added test for invalid tags with no fallack language
89Q12 Jun 17, 2022
c3f5684
fixed lint error from Lint workflow
89Q12 Jun 17, 2022
2658da3
Fixed typo th -> the
89Q12 Jun 17, 2022
7b89872
fixed test I messed up
89Q12 Jun 17, 2022
5ce38a3
Added fn quoted_ident to support only localize("foo", bar:baz) and
89Q12 Jun 17, 2022
7a11db6
Updated test templates -> quoted all messages
89Q12 Jun 17, 2022
355163a
Added cut() to localze function
89Q12 Jun 17, 2022
62027c8
Revert "Added cut() to localze function" because expr_any uses alt wh…
89Q12 Jun 17, 2022
bbe5991
HashMap<String, ...> to HashMap<&str, ...>
89Q12 Jun 17, 2022
f3ec8c7
removed .to_string()
89Q12 Jun 17, 2022
cc01d9b
removed "dep:" from toml files and removed println
89Q12 Jun 17, 2022
afbc64f
A bunch of changes
Kijewski Jun 17, 2022
b3714f4
Merge pull request #1 from Kijewski/i18n
89Q12 Jun 18, 2022
569a79b
Remove unnecessary feature guard
89Q12 Jun 19, 2022
db5c3e2
Corrected comment
89Q12 Jun 19, 2022
129786d
Validate localization at compile time
Kijewski Jun 19, 2022
ba50d10
Merge pull request #2 from Kijewski/br-i18n-with-compile-time-checks
89Q12 Jun 19, 2022
ca2ca74
Fixed various Clippy complaints
89Q12 Jun 19, 2022
da0319b
added feature guards to make the compiler happy and fix errors
89Q12 Jun 19, 2022
da9f506
changed forbid(unsafe) to forbid(unsafe_code)
89Q12 Jun 19, 2022
96c63c5
Created typ alias PathResources for Vec<(PathBuf, Resource<String>
89Q12 Jun 19, 2022
7663bf8
Fix "all" the clippy warnings
Kijewski Jun 19, 2022
03b4979
Merge pull request #3 from Kijewski/br-i18n-with-compile-time-checks
89Q12 Jun 19, 2022
08d34bd
Merge branch 'djc:main' into i18n
89Q12 Jul 12, 2022
39b0c4a
Merge branch 'main' into i18n
89Q12 Jul 28, 2022
6872a72
Fix lint job
89Q12 Sep 5, 2022
92fadc9
fix: Remove commented out i18n tests
LeoniePhiline Oct 10, 2022
98a4777
fix: Add trailing newline to i18n test templates
LeoniePhiline Oct 10, 2022
63ed5c9
fix: `cargo fmt`, remove trailing whitespace
LeoniePhiline Oct 10, 2022
073e92a
fix: Rename feature `localization` to `i18n`
LeoniePhiline Oct 10, 2022
f944c86
opinionated: Rename initialization macro `localization!` to `i18n_load!`
LeoniePhiline Oct 10, 2022
c32b22d
style: Change wording "have to" to "need to"
LeoniePhiline Oct 10, 2022
426575e
refactor: Confine i18n code inside i18n modules
LeoniePhiline Oct 10, 2022
81cce1a
fix: Hide dependency `fluent_templates` from library users
LeoniePhiline Oct 10, 2022
1c315d6
fix(deps): Update dependency `fluent-templates` to `0.8.0`
LeoniePhiline Oct 10, 2022
6384ae2
docs: Add initial i18n module documentation
LeoniePhiline Oct 10, 2022
7f82494
Merge pull request #4 from LeoniePhiline/i18n
89Q12 Oct 11, 2022
3f002ef
docs: Rename example template struct
LeoniePhiline Oct 11, 2022
925d1b5
Merge branch 'djc:main' into i18n
89Q12 Oct 11, 2022
8f30ba5
Merge pull request #5 from LeoniePhiline/i18n
89Q12 Oct 11, 2022
b52aaf3
fix: Implement missing recursive `is_cachable()` for `Expr::Localize`
LeoniePhiline Oct 11, 2022
9909c36
fix(style): Name localization message identifier as in Fluent Project
LeoniePhiline Oct 11, 2022
6cda9f5
fix(style): Normalize test names, prefix all with `test_`
LeoniePhiline Oct 11, 2022
9e1f326
Merge pull request #6 from LeoniePhiline/i18n
89Q12 Oct 11, 2022
7c15904
fix: Add missing trailing newline
LeoniePhiline Oct 11, 2022
5c35e12
Merge branch '11Tuvork28:i18n' into i18n
LeoniePhiline Oct 11, 2022
2bbba70
fix: Compilation without feature `i18n` failed
LeoniePhiline Oct 11, 2022
b5dda4c
Merge pull request #7 from LeoniePhiline/i18n
89Q12 Oct 11, 2022
f9dfa60
Merge branch 'djc:main' into i18n
89Q12 Jan 12, 2023
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
3 changes: 3 additions & 0 deletions askama/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ maintenance = { status = "actively-developed" }
default = ["config", "humansize", "num-traits", "urlencode"]
config = ["askama_derive/config"]
humansize = ["askama_derive/humansize", "dep_humansize"]
localization = ["askama_derive/localization", "fluent-templates", "unic-langid"]
markdown = ["askama_derive/markdown", "comrak"]
num-traits = ["askama_derive/num-traits", "dep_num_traits"]
serde-json = ["askama_derive/serde-json", "askama_escape/json", "serde", "serde_json"]
Expand All @@ -42,10 +43,12 @@ askama_escape = { version = "0.10.3", path = "../askama_escape" }
comrak = { version = "0.13", optional = true, default-features = false }
dep_humansize = { package = "humansize", version = "1.1.0", optional = true }
dep_num_traits = { package = "num-traits", version = "0.2.6", optional = true }
fluent-templates = { version = "0.7.1", optional = true, default-features = false }
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.8", optional = true }
unic-langid = { version= "0.9.0", optional = true}

[package.metadata.docs.rs]
features = ["config", "humansize", "num-traits", "serde-json", "serde-yaml"]
34 changes: 34 additions & 0 deletions askama/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ use std::fmt;

pub use askama_derive::Template;
pub use askama_escape::{Html, MarkupDisplay, Text};
#[cfg(feature = "localization")]
#[doc(hidden)]
pub use fluent_templates::fluent_bundle::FluentValue;
#[cfg(feature = "localization")]
use fluent_templates::{Loader, StaticLoader};

#[doc(hidden)]
pub use crate as shared;
Expand Down Expand Up @@ -218,3 +223,32 @@ mod tests {
note = "file-level dependency tracking is handled automatically without build script"
)]
pub fn rerun_if_templates_changed() {}

#[cfg(feature = "localization")]
pub struct Locale<'a> {
loader: &'a StaticLoader,
language: unic_langid::LanguageIdentifier,
}

#[cfg(feature = "localization")]
impl Locale<'_> {
pub fn new(language: unic_langid::LanguageIdentifier, loader: &'static StaticLoader) -> Self {
Self { loader, language }
}

pub fn translate<'a>(
&self,
text_id: &str,
args: impl IntoIterator<Item = (&'a str, FluentValue<'a>)>,
) -> String {
use std::collections::HashMap;
use std::iter::FromIterator;

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, text_id, args)
}
}
5 changes: 3 additions & 2 deletions askama_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ proc-macro = true
[features]
config = ["serde", "toml"]
humansize = []
localization = []
markdown = []
urlencode = []
num-traits = []
serde-json = []
serde-yaml = []
num-traits = []
urlencode = []
with-actix-web = []
with-axum = []
with-gotham = []
Expand Down
30 changes: 30 additions & 0 deletions askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1295,9 +1295,39 @@ impl<'a> Generator<'a> {
Expr::RustMacro(name, args) => self.visit_rust_macro(buf, name, args),
Expr::Try(ref expr) => self.visit_try(buf, expr.as_ref())?,
Expr::Tuple(ref exprs) => self.visit_tuple(buf, exprs)?,
Expr::Localize(ref message, ref args) => self.visit_localize(buf, message, args)?,
})
}

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

buf.write(&format!(
"self.{}.translate(",
normalize_identifier(localizer)
));
self.visit_expr(buf, message)?;
buf.writeln(", [")?;
buf.indent();
for (k, v) in args {
buf.write(&format!("({:?}, ::askama::FluentValue::from(", k));
self.visit_expr(buf, v)?;
buf.writeln(")),")?;
}
buf.dedent()?;
buf.write("])");

Ok(DisplayWrap::Unwrapped)
}

fn visit_try(
&mut self,
buf: &mut Buffer,
Expand Down
35 changes: 34 additions & 1 deletion askama_derive/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(crate) struct TemplateInput<'a> {
pub(crate) mime_type: String,
pub(crate) parent: Option<&'a syn::Type>,
pub(crate) path: PathBuf,
pub(crate) localizer: Option<String>,
}

impl TemplateInput<'_> {
Expand Down Expand Up @@ -50,7 +51,6 @@ impl TemplateInput<'_> {
return Err("must include 'ext' attribute when using 'source' attribute".into())
}
};

// Check to see if a `_parent` field was defined on the context
// struct, and store the type for it for use in the code generator.
let parent = match ast.data {
Expand All @@ -65,6 +65,37 @@ impl TemplateInput<'_> {
_ => None,
};

let localizer = match ast.data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(ref fields),
..
}) => {
let mut localizers =
fields
.named
.iter()
.filter(|&f| f.ident.is_some())
.flat_map(
|f| match f.attrs.iter().any(|a| a.path.is_ident("locale")) {
true => Some(f.ident.as_ref()?.to_string()),
false => None,
},
);
match localizers.next() {
Some(localizer) => {
if !cfg!(feature = "localization") {
return Err("You have to active the \"localization\" feature to use #[locale] on fields.".into());
Comment thread
89Q12 marked this conversation as resolved.
Outdated
} else if localizers.next().is_some() {
return Err("You cannot mark more than one field as #[locale].".into());
}
Some(localizer)
}
None => None,
}
}
_ => None,
};

if parent.is_some() {
eprint!(
" --> in struct {}\n = use of deprecated field '_parent'\n",
Expand Down Expand Up @@ -119,6 +150,8 @@ impl TemplateInput<'_> {
mime_type,
parent,
path,
#[cfg(feature = "localization")]
Comment thread
89Q12 marked this conversation as resolved.
Outdated
localizer,
})
}

Expand Down
2 changes: 1 addition & 1 deletion askama_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod heritage;
mod input;
mod parser;

#[proc_macro_derive(Template, attributes(template))]
#[proc_macro_derive(Template, attributes(template, locale))]
pub fn derive_template(input: TokenStream) -> TokenStream {
generator::derive_template(input)
}
Expand Down
104 changes: 104 additions & 0 deletions askama_derive/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub(crate) enum Expr<'a> {
Call(Box<Expr<'a>>, Vec<Expr<'a>>),
RustMacro(&'a str, &'a str),
Try(Box<Expr<'a>>),
Localize(Box<Expr<'a>>, Vec<(&'a str, Expr<'a>)>),
}

impl Expr<'_> {
Expand Down Expand Up @@ -669,6 +670,37 @@ macro_rules! expr_prec_layer {
}
}

fn expr_localize_args(mut i: &str) -> IResult<&str, Vec<(&str, Expr<'_>)>> {
let mut args = Vec::<(&str, Expr<'_>)>::new();

let mut p = opt(tuple((ws(tag(",")), identifier, ws(tag(":")), expr_any)));
while let (j, Some((_, k, _, v))) = p(i)? {
if args.iter().any(|&(a, _)| a == k) {
eprintln!("Duplicated key: {:?}", k);
return Err(nom::Err::Failure(error_position!(i, ErrorKind::Tag)));
}

args.push((k, v));
i = j;
}

let (i, _) = opt(tag(","))(i)?;
Ok((i, args))
}

fn expr_localize(i: &str) -> IResult<&str, Expr<'_>> {
let (i, _) = pair(tag("localize"), ws(tag("(")))(i)?;
if cfg!(feature = "localization") {
cut(map(
tuple((expr_any, expr_localize_args, ws(tag(")")))),
|(text_id, args, _)| Expr::Localize(text_id.into(), args),
))(i)
} else {
eprintln!(r#"Please activate the "localization" to use localize()."#);
Err(nom::Err::Failure(error_position!(i, ErrorKind::Tag)))
}
}

expr_prec_layer!(expr_muldivmod, expr_filtered, "*", "/", "%");
expr_prec_layer!(expr_addsub, expr_muldivmod, "+", "-");
expr_prec_layer!(expr_shifts, expr_addsub, ">>", "<<");
Expand All @@ -686,6 +718,7 @@ fn expr_handle_ws(i: &str) -> IResult<&str, Whitespace> {
fn expr_any(i: &str) -> IResult<&str, Expr<'_>> {
let range_right = |i| pair(ws(alt((tag("..="), tag("..")))), opt(expr_or))(i);
alt((
expr_localize,
map(range_right, |(op, right)| {
Expr::Range(op, None, right.map(Box::new))
}),
Expand Down Expand Up @@ -1255,6 +1288,77 @@ mod tests {
super::parse("{% extend \"blah\" %}", &Syntax::default()).unwrap();
}

#[cfg(feature = "localization")]
#[test]
fn test_parse_localize() {
macro_rules! map {
($($k:expr => $v:expr),* $(,)?) => {{
use std::iter::{Iterator, IntoIterator};
Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*]))
}};
}

assert_eq!(
super::parse(r#"{{ localize("a", v: 32 + 7) }}"#, &Syntax::default()).unwrap(),
vec![Node::Expr(
Ws(None, None),
Expr::Localize(
Expr::StrLit("a").into(),
map!(
"v" => {
Expr::BinOp("+", Expr::NumLit("32").into(), Expr::NumLit("7").into())
}
),
)
)],
);

assert_eq!(
super::parse(
r#"{{ localize("a", b: "b", c: "c", d: "d") }}"#,
&Syntax::default(),
)
.unwrap(),
vec![Node::Expr(
Ws(None, None),
Expr::Localize(
Expr::StrLit("a").into(),
map!(
"b" => Expr::StrLit("b"),
"c" => Expr::StrLit("c"),
"d" => Expr::StrLit("d"),
),
)
)],
);

assert_eq!(
super::parse(
r#"{{ localize("a", v: localize("a", v: 32 + 7) ) }}"#,
&Syntax::default(),
)
.unwrap(),
vec![Node::Expr(
Ws(None, None),
Expr::Localize(
Expr::StrLit("a").into(),
map!(
"v" => Expr::Localize(
Expr::StrLit("a").into(),
map!(
"v" => Expr::BinOp(
"+",
Expr::NumLit("32").into(),
Expr::NumLit("7").into(),
),
),
),
),
),
)],
);
}

#[test]
fn test_parse_filter() {
use Expr::*;
Expand Down
5 changes: 4 additions & 1 deletion testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ edition = "2018"
publish = false

[features]
default = ["serde-json", "markdown"]
default = ["serde-json", "markdown", "localization"]
serde-json = ["serde_json", "askama/serde-json"]
markdown = ["comrak", "askama/markdown"]
localization = ["fluent-templates", "unic-langid", "askama/localization"]

[dependencies]
askama = { path = "../askama", version = "0.11.0-beta.1" }
comrak = { version = "0.13", default-features = false, optional = true }
fluent-templates = { version = "0.7.1", optional = true}
serde_json = { version = "1.0", optional = true }
unic-langid = { version = "0.9.0", optional = true }

[dev-dependencies]
criterion = "0.3"
Expand Down
3 changes: 3 additions & 0 deletions testing/i18n-basic/en-US/basic.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
greeting = Hello, { $name }!
age = You are { $hours } hours old.
test = This is a test
2 changes: 2 additions & 0 deletions testing/i18n-basic/es-MX/basic.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
greeting = ¡Hola, { $name }!
age = Tienes { $hours } horas.
2 changes: 2 additions & 0 deletions testing/templates/i18n.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h1>{{ localize("greeting", name: name) }}</h1>
<h3>{{ localize("age", hours: hours ) }}</h3>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: please make sure all the files end with a trailing newline.

1 change: 1 addition & 0 deletions testing/templates/i18n_broken.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>{{ localize("car", color: car_color) }}</h1>
2 changes: 2 additions & 0 deletions testing/templates/i18n_invalid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h1>{{ localize("greetingsss", name: name) }}</h1>
<h3>{{ localize("ages") }}</h3>
1 change: 1 addition & 0 deletions testing/templates/i18n_no_args.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h3>{{ localize("test", test: "") }}</h3>
Loading