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 24 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 @@ -31,6 +31,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"]
localization = ["dep:fluent-templates","dep:unic-langid"]
Comment thread
89Q12 marked this conversation as resolved.
Outdated

# deprecated
mime = []
Expand All @@ -46,6 +47,8 @@ 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 }
fluent-templates = { version= "0.7.1", optional=true}
unic-langid = {version= "0.9.0", optional=true}

[package.metadata.docs.rs]
features = ["config", "humansize", "num-traits", "serde-json", "serde-yaml"]
27 changes: 27 additions & 0 deletions askama/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,30 @@ mod tests {
note = "file-level dependency tracking is handled automatically without build script"
)]
pub fn rerun_if_templates_changed() {}

#[cfg(feature = "localization")]
use fluent_templates::Loader;
#[cfg(feature = "localization")]
pub struct Locale<'a> {
loader: &'a fluent_templates::StaticLoader,
language: unic_langid::LanguageIdentifier,
}
#[cfg(feature = "localization")]
impl<'a> Locale<'a> {
pub fn new(
language: unic_langid::LanguageIdentifier,
templates: &'static fluent_templates::StaticLoader,
) -> Locale<'a> {
Self {
loader: templates,
language,
}
}
pub fn translate(
&self,
text_id: &str,
args: &std::collections::HashMap<String, fluent_templates::fluent_bundle::FluentValue<'_>>,
) -> String {
self.loader.lookup_with_args(&self.language, text_id, args)
}
}
1 change: 1 addition & 0 deletions askama_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ with-mendes = []
with-rocket = []
with-tide = []
with-warp = []
localization = []

[dependencies]
mime = "0.3"
Expand Down
57 changes: 57 additions & 0 deletions askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use crate::CompileError;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};

#[cfg(feature = "localization")]
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::{cmp, hash, mem, str};
Expand Down Expand Up @@ -255,6 +257,9 @@ struct Generator<'a> {
// If set to `suppress`, the whitespace characters will be removed by default unless `+` is
// used.
whitespace: WhitespaceHandling,
#[cfg(feature = "localization")]
// Messages used with localize()
localized_messages: BTreeSet<String>,
}

impl<'a> Generator<'a> {
Expand All @@ -276,6 +281,8 @@ impl<'a> Generator<'a> {
buf_writable: vec![],
named: 0,
whitespace,
#[cfg(feature = "localization")]
localized_messages: BTreeSet::new(),
}
}

Expand Down Expand Up @@ -1295,9 +1302,59 @@ 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)?,
#[cfg(feature = "localization")]
Expr::Localize(message, attribute, ref args) => {
self.visit_localize(buf, message, attribute, args)?
}
})
}

#[cfg(feature = "localization")]
fn visit_localize(
&mut self,
buf: &mut Buffer,
message: &str,
attribute: Option<&str>,
args: &[(&str, Expr<'_>)],
) -> Result<DisplayWrap, CompileError> {
let localizer = self.input.localizer.as_ref().expect(
"A template struct must have a member with the `#[locale]` \
attribute that implements `askama::Localize` to enable calling the localize() filter",
);

let mut message = message.to_string();
if let Some(attribute) = attribute {
message.push_str(".");
message.push_str(attribute);
}

assert!(
message.chars().find(|c| *c == '"').is_none(),
Comment thread
Kijewski marked this conversation as resolved.
Outdated
"message ids with quotes in them break the generator, please remove"
);

self.localized_messages.insert(message.clone());

buf.write(&format!(
"self.{}.translate(\"{}\", &std::iter::FromIterator::from_iter(vec![",
localizer.0, message
));

for (i, (name, value)) in args.iter().enumerate() {
if i > 0 {
buf.write(", ");
}
buf.write(&format!(
"(\"{}\".to_string(), ({}).into())",
Comment thread
89Q12 marked this conversation as resolved.
Outdated
name,
self.visit_expr_root(value)?
));
}
buf.write("]))");

Ok(DisplayWrap::Unwrapped)
}

fn visit_try(
&mut self,
buf: &mut Buffer,
Expand Down
32 changes: 30 additions & 2 deletions askama_derive/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub(crate) struct TemplateInput<'a> {
pub(crate) mime_type: String,
pub(crate) parent: Option<&'a syn::Type>,
pub(crate) path: PathBuf,
#[cfg(feature = "localization")]
pub(crate) localizer: Option<(syn::Ident, &'a syn::Type)>,
}

impl TemplateInput<'_> {
Expand Down Expand Up @@ -50,7 +52,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 @@ -64,7 +65,32 @@ impl TemplateInput<'_> {
.map(|f| &f.ty),
_ => None,
};

#[cfg(feature = "localization")]
// if enabled, it tries to get the localizer
let localizer = match ast.data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(ref fields),
..
}) => {
let named = &fields.named;
let localizers: Vec<_> = named
.iter()
.filter(|f| f.ident.is_some())
.flat_map(|f| {
f.attrs
.iter()
.filter(|a| a.path.is_ident("locale"))
.map(move |_| (f.ident.to_owned().unwrap(), &f.ty))
})
.collect();
if localizers.len() > 1 {
panic!("Can't have multiple localizers for a single template!");
} else {
localizers.get(0).map(|l| l.to_owned())
}
}
_ => None,
};
if parent.is_some() {
eprint!(
" --> in struct {}\n = use of deprecated field '_parent'\n",
Expand Down Expand Up @@ -119,6 +145,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
84 changes: 81 additions & 3 deletions askama_derive/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ pub(crate) enum Expr<'a> {
Call(Box<Expr<'a>>, Vec<Expr<'a>>),
RustMacro(&'a str, &'a str),
Try(Box<Expr<'a>>),
#[cfg(feature = "localization")]
Localize(&'a str, Option<&'a str>, Vec<(&'a str, Expr<'a>)>),
}

impl Expr<'_> {
Expand Down Expand Up @@ -668,7 +670,29 @@ macro_rules! expr_prec_layer {
}
}
}

#[cfg(feature = "localization")]
Comment thread
89Q12 marked this conversation as resolved.
Outdated
fn localize(i: &str) -> IResult<&str, Expr<'_>> {
let (tail, (_, _, message, attribute, args, _)) = tuple((
tag("localize"),
ws(tag("(")),
Comment thread
Kijewski marked this conversation as resolved.
identifier,
Comment thread
djc marked this conversation as resolved.
Outdated
opt(tuple((ws(tag(".")), identifier))),
opt(tuple((
ws(tag(",")),
separated_list0(ws(tag(",")), tuple((identifier, ws(tag(":")), expr_any))),
))),
ws(tag(")")),
))(i)?;
Ok((
tail,
Expr::Localize(
message,
attribute.map(|(_, a)| a),
args.map(|(_, args)| args.into_iter().map(|(k, _, v)| (k, v)).collect())
.unwrap_or_default(),
),
))
}
expr_prec_layer!(expr_muldivmod, expr_filtered, "*", "/", "%");
expr_prec_layer!(expr_addsub, expr_muldivmod, "+", "-");
expr_prec_layer!(expr_shifts, expr_addsub, ">>", "<<");
Expand All @@ -682,10 +706,14 @@ expr_prec_layer!(expr_or, expr_and, "||");
fn expr_handle_ws(i: &str) -> IResult<&str, Whitespace> {
alt((char('-'), char('+'), char('~')))(i).map(|(s, r)| (s, Whitespace::from(r)))
}

fn expr_any(i: &str) -> IResult<&str, Expr<'_>> {
let range_right = |i| pair(ws(alt((tag("..="), tag("..")))), opt(expr_or))(i);
alt((
#[cfg(feature = "localization")]
map(localize, |expr| match expr {
Expr::Localize(_, _, _) => expr,
_ => panic!("localize failed: {:?}", expr),
}),
map(range_right, |(op, right)| {
Expr::Range(op, None, right.map(Box::new))
}),
Expand Down Expand Up @@ -1254,7 +1282,57 @@ mod tests {
fn test_invalid_block() {
super::parse("{% extend \"blah\" %}", &Syntax::default()).unwrap();
}

#[cfg(feature = "localization")]
#[test]
fn test_parse_localize() {
assert_eq!(
super::parse("{{ localize(a, v: 32 + 7) }}", &Syntax::default()).unwrap(),
vec![Node::Expr(
Ws(None, None),
Expr::Localize(
"a",
None,
vec![(
"v",
Expr::BinOp("+", Expr::NumLit("32").into(), Expr::NumLit("7").into())
)]
)
)]
);
}
#[cfg(feature = "localization")]
#[test]
fn test_parse_nested_localize() {
assert_eq!(
super::parse(
"{{ localize(a, v: localize(a, v: 32 + 7) ) }}",
&Syntax::default()
)
.unwrap(),
vec![Node::Expr(
Ws(None, None),
Expr::Localize(
"a",
None,
vec![(
"v",
Expr::Localize(
"a",
None,
vec![(
"v",
Expr::BinOp(
"+",
Expr::NumLit("32").into(),
Expr::NumLit("7").into()
)
)]
)
)]
)
)]
);
}
#[test]
fn test_parse_filter() {
use Expr::*;
Expand Down
3 changes: 3 additions & 0 deletions testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ publish = false
default = ["serde-json", "markdown"]
serde-json = ["serde_json", "askama/serde-json"]
markdown = ["comrak", "askama/markdown"]
localization = ["dep:fluent-templates","dep:unic-langid"]

[dependencies]
askama = { path = "../askama", version = "0.11.0-beta.1" }
comrak = { version = "0.13", default-features = false, optional = true }
serde_json = { version = "1.0", optional = true }
fluent-templates = { version= "0.7.1", 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>
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