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

View File

@ -19,6 +19,10 @@ the files must look like this:
```json ```json
{ {
"err.user.not_found": {
"fr": "Utilisateur introuvable: $email, $id",
"en": "User not found: $email, $id"
},
"err.answer.all": { "err.answer.all": {
"fr": "Échec lors de la récupération des réponses", "fr": "Échec lors de la récupération des réponses",
"en": "Failed to retrieve answers" "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 ## Installation
Internationalization is available on [crates.io](https://crates.io/crates/internationalization), include it in your `Cargo.toml`: 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 glob::glob;
use proc_macro2::TokenStream; use lazy_static::lazy_static;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote; use quote::quote;
use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
@ -33,26 +35,79 @@ fn read_locales() -> Translations {
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 { fn generate_code(translations: Translations) -> proc_macro2::TokenStream {
let mut branches = Vec::<TokenStream>::new(); let mut branches = Vec::<TokenStream>::new();
for (key, trs) in translations { for (key, trs) in translations {
let mut langs = Vec::<TokenStream>::new(); let mut langs = Vec::<TokenStream>::new();
let mut needs_interpolation = false;
let mut vars = Vec::new();
for (lang, tr) in trs { for (lang, tr) in trs {
let l = quote! { let lang_vars = extract_vars(&tr);
#lang => #tr, needs_interpolation = lang_vars.len() > 0;
};
langs.push(l) 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(),
});
} }
let branch = quote! { }
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) => { (#key, $lang:expr) => {
match $lang.as_ref() { match $lang.as_ref() {
#(#langs)* #(#langs)*
e => panic!("Missing language: {}", e) e => panic!("Missing language: {}", e)
} }
}; };
}; });
branches.push(branch); }
} }
quote! { 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": { "key.test": {
"en": "This is a test", "en": "This is a test",
"fr": "C'est un test" "fr": "C'est un test"
@ -6,5 +17,8 @@
"err.not_allowed": { "err.not_allowed": {
"en": "You are not allowed to do this", "en": "You are not allowed to do this",
"fr": "Vous n'êtes pas autorisé à faire cela" "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 //! ```json
//! { //! {
//! "err.user.not_found": {
//! "fr": "Utilisateur introuvable: $email, $id",
//! "en": "User not found: $email, $id"
//! },
//! "err.answer.all": { //! "err.answer.all": {
//! "fr": "Échec lors de la récupération des réponses", //! "fr": "Échec lors de la récupération des réponses",
//! "en": "Failed to retrieve answers" //! "en": "Failed to retrieve answers"
@ -56,8 +60,20 @@
//! // Code will not compile //! // 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 //! ## Installation
//! Internationalization is available on [crates.io](https://crates.io/crates/internationalization), include it in your `Cargo.toml`: //! 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" "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] #[test]
#[should_panic] #[should_panic]
fn it_fails_to_translate() { fn it_fails_to_translate() {