Add compile time key check
This commit is contained in:
parent
67e3b1eeec
commit
fa5547181a
@ -8,8 +8,10 @@ keywords = ["i18n", "internationalization"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/terry90/internationalization-rs"
|
||||
homepage = "https://github.com/terry90/internationalization-rs"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
[build-dependencies]
|
||||
glob = "0.3.0"
|
||||
serde_json = "1.0.40"
|
||||
quote = "1.0.2"
|
||||
serde_json = "1.0.41"
|
||||
proc-macro2 = "1.0"
|
64
README.md
64
README.md
@ -1,17 +1,69 @@
|
||||
# Internationalization
|
||||
|
||||
An simple i18n implementation in Rust.
|
||||
![LICENSE](https://img.shields.io/crates/l/internationalization)
|
||||
[![Crates.io Version](https://img.shields.io/crates/v/internationalization.svg)](https://crates.io/crates/internationalization)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/terry90/internationalization-rs/badge.svg?branch=master)](https://coveralls.io/github/terry90/internationalization-rs?branch=master)
|
||||
|
||||
An simple compile time i18n implementation in Rust.
|
||||
It throws a compilation error if the translation key is not present, but since the `lang` argument is dynamic it will panic if the language has not been added for the matching key.
|
||||
|
||||
> API documentation [https://crates.io/crates/internationalization](https://crates.io/crates/internationalization)
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use internationalization::{init_i18n, t};
|
||||
fn main() {
|
||||
init_i18n!("locales/*.json", "fr", "en");
|
||||
Have a `locales/` folder somewhere in your app, root, src, anywhere. with `.json` files, nested in folders or not.
|
||||
It uses a glob pattern: `**/locales/**/*.json` to match your translation files.
|
||||
|
||||
the files must look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"err.answer.all": {
|
||||
"fr": "Échec lors de la récupération des réponses",
|
||||
"en": "Failed to retrieve answers"
|
||||
},
|
||||
"err.answer.delete.failed": {
|
||||
"fr": "Échec lors de la suppression de la réponse",
|
||||
"en": "Failed to delete answer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Any number of languages can be added, but you should provide them for everything since it will panic if a language is not found when queried for a key.
|
||||
|
||||
In your app, jsut call the `t!` macro
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let lang = "en";
|
||||
let res = t!("err.not_allowed", lang);
|
||||
|
||||
let res = t("err.not_allowed");
|
||||
assert_eq!("You are not allowed to do this", res);
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Internationalization is available on [crates.io](https://crates.io/crates/internationalization), include it in your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
internationalization = "0.0.2"
|
||||
```
|
||||
|
||||
Then include it in your code like this:
|
||||
|
||||
```rust
|
||||
#[macro_use]
|
||||
extern crate internationalization;
|
||||
```
|
||||
|
||||
Or use the macro where you want to use it:
|
||||
|
||||
```rust
|
||||
use internationalization::t;
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
Internationalization will not work if no `PWD` env var is set at compile time.
|
||||
|
82
build.rs
Normal file
82
build.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use glob::glob;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
|
||||
type Key = String;
|
||||
type Locale = String;
|
||||
type Value = String;
|
||||
type Translations = HashMap<Key, HashMap<Locale, Value>>;
|
||||
|
||||
fn read_locales() -> Translations {
|
||||
let mut translations: Translations = HashMap::new();
|
||||
|
||||
let build_directory = std::env::var("PWD").unwrap();
|
||||
let locales = format!("{}/**/locales/**/*.json", build_directory);
|
||||
println!("Reading {}", &locales);
|
||||
|
||||
for entry in glob(&locales).expect("Failed to read glob pattern") {
|
||||
let entry = entry.unwrap();
|
||||
println!("cargo:rerun-if-changed={}", entry.display());
|
||||
let file = File::open(entry).expect("Failed to open the file");
|
||||
let mut reader = std::io::BufReader::new(file);
|
||||
let mut content = String::new();
|
||||
reader
|
||||
.read_to_string(&mut content)
|
||||
.expect("Failed to read the file");
|
||||
let res: HashMap<String, HashMap<String, String>> =
|
||||
serde_json::from_str(&content).expect("Cannot parse locale file");
|
||||
translations.extend(res);
|
||||
}
|
||||
translations
|
||||
}
|
||||
|
||||
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();
|
||||
for (lang, tr) in trs {
|
||||
let l = quote! {
|
||||
#lang => #tr,
|
||||
};
|
||||
langs.push(l)
|
||||
}
|
||||
let branch = quote! {
|
||||
(#key, $lang:expr) => {
|
||||
match $lang.as_ref() {
|
||||
#(#langs)*
|
||||
e => panic!("Missing language: {}", e)
|
||||
}
|
||||
};
|
||||
};
|
||||
branches.push(branch);
|
||||
}
|
||||
|
||||
quote! {
|
||||
#[macro_export]
|
||||
macro_rules! t {
|
||||
#(#branches)*
|
||||
($key:expr, $lang:expr) => {
|
||||
compile_error!("Missing translation");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_code(code: TokenStream) {
|
||||
let dest = std::env::var("OUT_DIR").unwrap();
|
||||
let mut output = File::create(&std::path::Path::new(&dest).join("i18n.rs")).unwrap();
|
||||
output
|
||||
.write(code.to_string().as_bytes())
|
||||
.expect("Cannot write generated i18n code");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let translations = read_locales();
|
||||
let code = generate_code(translations);
|
||||
println!("{}", &code);
|
||||
write_code(code);
|
||||
}
|
6
locales/test.json
Normal file
6
locales/test.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"key.test": {
|
||||
"en": "This is a test",
|
||||
"fr": "C'est un test"
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
use glob::glob;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::sync::RwLock;
|
||||
|
||||
type Key = String;
|
||||
type Locale = String;
|
||||
type Value = String;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TR: RwLock<HashMap<Key, HashMap<Locale, Value>>> = RwLock::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub fn read_files(pattern: &str) -> Vec<String> {
|
||||
let mut contents = Vec::new();
|
||||
for entry in glob(pattern).expect("Failed to read glob pattern") {
|
||||
let file = File::open(entry.unwrap()).expect("Failed to open the file");
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut content = String::new();
|
||||
reader
|
||||
.read_to_string(&mut content)
|
||||
.expect("Failed to read the file");
|
||||
contents.push(content);
|
||||
}
|
||||
contents
|
||||
}
|
||||
|
||||
pub fn load_i18n(content: String) {
|
||||
let res: HashMap<String, HashMap<String, String>> =
|
||||
serde_json::from_str(&content).expect("Cannot parse I18n file");
|
||||
TR.write().unwrap().extend(res);
|
||||
}
|
||||
|
||||
/// Translates by key
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If a key is missing, the code will panic
|
||||
/// If a locale is not present for the key, ths will also panic
|
||||
///
|
||||
/// # Example
|
||||
/// ```no-run
|
||||
/// use internationalization::t;
|
||||
///
|
||||
/// fn main() {
|
||||
/// init_i18n!("locales/*.json", "fr", "en");
|
||||
///
|
||||
/// let res = t("err.not_allowed");
|
||||
/// assert_eq!("You are not allowed to do this", res);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn t(key: &str, locale: &str) -> String {
|
||||
match TR.read().unwrap().get(key) {
|
||||
Some(trs) => match trs.get(locale) {
|
||||
Some(value) => value.to_owned(),
|
||||
None => panic!("Missing language ({}) for key: {}", locale, key),
|
||||
},
|
||||
None => panic!("Missing key: {}", key),
|
||||
}
|
||||
}
|
88
src/lib.rs
88
src/lib.rs
@ -1,30 +1,86 @@
|
||||
//! # Internationalization
|
||||
//! An simple i18n implementation in Rust.
|
||||
|
||||
//! [![Crates.io Version](https://img.shields.io/crates/v/internationalization.svg)](https://crates.io/crates/internationalization)
|
||||
//! ![LICENSE](https://img.shields.io/crates/l/internationalization)
|
||||
//! [![Coverage Status](https://coveralls.io/repos/github/terry90/internationalization-rs/badge.svg?branch=master)](https://coveralls.io/github/terry90/internationalization-rs?branch=master)
|
||||
//!
|
||||
//! An simple compile time i18n implementation in Rust.
|
||||
//! It throws a compilation error if the translation key is not present, but since the `lang` argument is dynamic it will panic if the language has not been added for the matching key.
|
||||
|
||||
//! > API documentation [https://crates.io/crates/internationalization](https://crates.io/crates/internationalization)
|
||||
|
||||
//! ## Usage
|
||||
//!
|
||||
|
||||
//! Have a `locales/` folder somewhere in your app, root, src, anywhere. with `.json` files, nested in folders or not.
|
||||
//! It uses a glob pattern: `**/locales/**/*.json` to match your translation files.
|
||||
|
||||
//! the files must look like this:
|
||||
|
||||
//! ```json
|
||||
//! {
|
||||
//! "err.answer.all": {
|
||||
//! "fr": "Échec lors de la récupération des réponses",
|
||||
//! "en": "Failed to retrieve answers"
|
||||
//! },
|
||||
//! "err.answer.delete.failed": {
|
||||
//! "fr": "Échec lors de la suppression de la réponse",
|
||||
//! "en": "Failed to delete answer"
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! use internationalization::{init_i18n, t};
|
||||
//!
|
||||
|
||||
//! Any number of languages can be added, but you should provide them for everything since it will panic if a language is not found when queried for a key.
|
||||
|
||||
//! In your app, jsut call the `t!` macro
|
||||
|
||||
//! ```rust
|
||||
//! fn main() {
|
||||
//! init_i18n!("locales/*.json", "fr", "en");
|
||||
//!
|
||||
//! let res = t("err.not_allowed");
|
||||
//! let lang = "en";
|
||||
//! let res = t!("err.not_allowed", lang);
|
||||
|
||||
//! assert_eq!("You are not allowed to do this", res);
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod i18n;
|
||||
pub use i18n::t;
|
||||
//! ## Installation
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! init_i18n {
|
||||
( $path:expr, $( $lang:expr ),* ) => {
|
||||
use internationalization::i18n::{load_i18n, read_files};
|
||||
for content in read_files($path) {
|
||||
load_i18n(content)
|
||||
//! Internationalization is available on [crates.io](https://crates.io/crates/internationalization), include it in your `Cargo.toml`:
|
||||
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! internationalization = "0.0.2"
|
||||
//! ```
|
||||
|
||||
//! Then include it in your code like this:
|
||||
|
||||
//! ```rust
|
||||
//! #[macro_use]
|
||||
//! extern crate internationalization;
|
||||
//! ```
|
||||
|
||||
//! Or use the macro where you want to use it:
|
||||
|
||||
//! ```rust
|
||||
//! use internationalization::t;
|
||||
//! ```
|
||||
|
||||
//! ## Note
|
||||
|
||||
//! Internationalization will not work if no `PWD` env var is set at compile time.
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/i18n.rs"));
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_translates() {
|
||||
assert_eq!(t!("key.test", "en"), "This is a test");
|
||||
}
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn it_fails_to_translate() {
|
||||
assert_eq!(t!("key.test", "es"), "Hola");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user