From 17527c47803a091199713883628d752a49881d15 Mon Sep 17 00:00:00 2001 From: Terry Raimondo Date: Wed, 9 Oct 2019 17:34:37 +0200 Subject: [PATCH] Add vars interpolation --- Cargo.toml | 6 ++-- README.md | 16 +++++++++ build.rs | 83 +++++++++++++++++++++++++++++++++++++++-------- locales/test.json | 14 ++++++++ src/lib.rs | 45 ++++++++++++++++++++++++- 5 files changed, 147 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4c801ce..2f341e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "internationalization" -version = "0.0.2" +version = "0.0.3" authors = ["Terry Raimondo "] 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" } diff --git a/README.md b/README.md index 863aa2d..ba197d3 100644 --- a/README.md +++ b/README.md @@ -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`: diff --git a/build.rs b/build.rs index aa20f26..38bccf0 100644 --- a/build.rs +++ b/build.rs @@ -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 { + 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::>(); + a.sort(); + + println!("-----\n{:?}\n-----", &a); + a +} + +fn convert_vars_to_idents(vars: &Vec) -> Vec { + vars.iter() + .map(|var| Ident::new(&var[1..], Span::call_site())) + .collect() +} + fn generate_code(translations: Translations) -> proc_macro2::TokenStream { let mut branches = Vec::::new(); for (key, trs) in translations { let mut langs = Vec::::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! { diff --git a/locales/test.json b/locales/test.json index 8092081..09b5b3e 100644 --- a/locales/test.json +++ b/locales/test.json @@ -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" } } diff --git a/src/lib.rs b/src/lib.rs index 4385b23..fc09b3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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() {