Add vars interpolation

This commit is contained in:
Terry Raimondo 2019-10-09 17:34:37 +02:00
parent 01c2437f24
commit 17527c4780
5 changed files with 147 additions and 17 deletions

View File

@ -1,10 +1,10 @@
[package]
name = "internationalization"
version = "0.0.2"
version = "0.0.3"
authors = ["Terry Raimondo <terry.raimondo@gmail.com>"]
edition = "2018"
description = "Easy to use I18n"
keywords = ["i18n", "internationalization"]
keywords = ["i18n", "internationalization", "locales"]
license = "MIT/Apache-2.0"
repository = "https://github.com/terry90/internationalization-rs"
homepage = "https://github.com/terry90/internationalization-rs"
@ -15,6 +15,8 @@ glob = "0.3.0"
quote = "1.0.2"
serde_json = "1.0.41"
proc-macro2 = "1.0"
regex = "1.3.1"
lazy_static = "1.4.0"
[badges]
travis-ci = { repository = "terry90/internationalization-rs" }

View File

@ -19,6 +19,10 @@ the files must look like this:
```json
{
"err.user.not_found": {
"fr": "Utilisateur introuvable: $email, $id",
"en": "User not found: $email, $id"
},
"err.answer.all": {
"fr": "Échec lors de la récupération des réponses",
"en": "Failed to retrieve answers"
@ -43,6 +47,18 @@ fn main() {
}
```
You can use interpolation, any number of argument is OK but you should note that they have to be sorted alphabetically.
To use variables, call the `t!` macro like this:
```rust
fn main() {
let lang = "en";
let res = t!("err.user.not_found", email: "me@localhost", id: "1", lang);
assert_eq!("User not found: me@localhost, ID: 1", res);
}
```
## Installation
Internationalization is available on [crates.io](https://crates.io/crates/internationalization), include it in your `Cargo.toml`:

View File

@ -1,6 +1,8 @@
use glob::glob;
use proc_macro2::TokenStream;
use lazy_static::lazy_static;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use regex::Regex;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
@ -33,26 +35,79 @@ fn read_locales() -> Translations {
translations
}
fn extract_vars(tr: &str) -> Vec<String> {
lazy_static! {
static ref RE: Regex = Regex::new("\\$[a-zA-Z0-9_-]+").unwrap();
}
let mut a = RE
.find_iter(tr)
.map(|mat| mat.as_str().to_owned())
.collect::<Vec<String>>();
a.sort();
println!("-----\n{:?}\n-----", &a);
a
}
fn convert_vars_to_idents(vars: &Vec<String>) -> Vec<Ident> {
vars.iter()
.map(|var| Ident::new(&var[1..], Span::call_site()))
.collect()
}
fn generate_code(translations: Translations) -> proc_macro2::TokenStream {
let mut branches = Vec::<TokenStream>::new();
for (key, trs) in translations {
let mut langs = Vec::<TokenStream>::new();
let mut needs_interpolation = false;
let mut vars = Vec::new();
for (lang, tr) in trs {
let l = quote! {
#lang => #tr,
};
langs.push(l)
let lang_vars = extract_vars(&tr);
needs_interpolation = lang_vars.len() > 0;
if needs_interpolation {
let idents = convert_vars_to_idents(&lang_vars);
vars.extend(lang_vars.clone());
langs.push(quote! {
#lang => #tr#(.replace(#lang_vars, $#idents))*,
});
} else {
langs.push(quote! {
#lang => #tr.to_owned(),
});
}
}
vars.sort();
vars.dedup();
let vars_ident = convert_vars_to_idents(&vars);
if needs_interpolation {
branches.push(quote! {
(#key, #(#vars_ident: $#vars_ident:expr, )*$lang:expr) => {
match $lang.as_ref() {
#(#langs)*
e => panic!("Missing language: {}", e)
}
};
});
branches.push(quote! {
(#key, $($e:tt)*) => {
compile_error!(stringify!(Please provide: #(#vars_ident),* >> The order matters!));
};
});
} else {
branches.push(quote! {
(#key, $lang:expr) => {
match $lang.as_ref() {
#(#langs)*
e => panic!("Missing language: {}", e)
}
};
});
}
let branch = quote! {
(#key, $lang:expr) => {
match $lang.as_ref() {
#(#langs)*
e => panic!("Missing language: {}", e)
}
};
};
branches.push(branch);
}
quote! {

View File

@ -1,4 +1,15 @@
{
"hello": {
"en": "Hello $name!",
"fr": "Salut $name !"
},
"multiple.vars": {
"fr": "Nom: $name, Truc: $thing, bingo: $bingo"
},
"inconsistent.vars": {
"en": "Name: $thing, ok: $ok",
"fr": "Salut $name !"
},
"key.test": {
"en": "This is a test",
"fr": "C'est un test"
@ -6,5 +17,8 @@
"err.not_allowed": {
"en": "You are not allowed to do this",
"fr": "Vous n'êtes pas autorisé à faire cela"
},
"err.user.not_found": {
"en": "User not found: $email, ID: $id"
}
}

View File

@ -19,6 +19,10 @@
//! ```json
//! {
//! "err.user.not_found": {
//! "fr": "Utilisateur introuvable: $email, $id",
//! "en": "User not found: $email, $id"
//! },
//! "err.answer.all": {
//! "fr": "Échec lors de la récupération des réponses",
//! "en": "Failed to retrieve answers"
@ -56,8 +60,20 @@
//! // Code will not compile
//! }
//!
//! ```
//! You can use interpolation, any number of argument is OK but you should note that they have to be sorted alphabetically.
//! To use variables, call the `t!` macro like this:
//!
//! ```rust
//! fn main() {
//! let lang = "en";
//! let res = t!("err.user.not_found", email: "me@localhost", id: "1", lang);
//!
//! assert_eq!("User not found: me@localhost, ID: 1", res);
//! }
//! ```
//!
//! ## Installation
//! Internationalization is available on [crates.io](https://crates.io/crates/internationalization), include it in your `Cargo.toml`:
@ -103,6 +119,33 @@ mod tests {
"You are not allowed to do this"
);
}
#[test]
fn it_interpolates() {
assert_eq!(t!("hello", name: "Fred", "fr"), "Salut Fred !");
assert_eq!(t!("hello", name: "Fred", "en"), "Hello Fred!");
}
#[test]
fn it_interpolates_multiple_vars() {
assert_eq!(
t!("multiple.vars", bingo: "bingo", name: "Fred", thing: "thing", "fr"),
"Nom: Fred, Truc: thing, bingo: bingo"
);
}
#[test]
fn it_interpolates_inconsistent_vars() {
assert_eq!(
t!("inconsistent.vars", name: "Fred", ok: "top", thing: "thing", "fr"),
"Salut Fred !"
);
assert_eq!(
t!("inconsistent.vars", name: "Fred", ok: "top", thing: "thing", "en"),
"Name: thing, ok: top"
);
}
#[test]
#[should_panic]
fn it_fails_to_translate() {