Skip to content

Commit

Permalink
Add chained fallback support and performance improved (#69)
Browse files Browse the repository at this point in the history
* Add more than one fallback with priority support

Example: i18n!("locales", fallback = ["en", "es]);

* Add more than on fallback testing

* Add new proc_macro key! support

* Add new lock-free AtomicStr

* Remove RwLock from locale() and set_locale()

* Use AtomicStr instead of RwLock<String>
* Change locale() return type to Arc<String>

* tests/locales/en.yml: Add lorem ipsum long text item

* bench: Add t_with_threads and t_lorem_ipsum

* bench: Add t_with_args (many)

* Add new function `replace_patterns()` to speed up string replacement

* Use `replace_patterns()` in t! to speed up string replacement

* Change _rust_i18n_translate() return type to Cow<str> to reduce string copy

* Fix some testing compile errors

* Change `t!()` with patterns return type to Cow::Owned(String)

* Fix examples compile error

* Early return in fallback parsing to avoid an 'else' clause

* README.md: Update benchmark results
  • Loading branch information
varphone authored Jan 19, 2024
1 parent 62b6e1d commit 91abcd6
Show file tree
Hide file tree
Showing 16 changed files with 303 additions and 44 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ version = "2.3.1"
once_cell = "1.10.0"
rust-i18n-support = {path = "./crates/support", version = "2.3.0"}
rust-i18n-macro = {path = "./crates/macro", version = "2.3.0"}
smallvec = "1.12.0"

[dev-dependencies]
foo = {path = "examples/foo"}
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ i18n!("locales");
// Use `fallback` option to set fallback locale.
//
// i18n!("locales", fallback = "en");
// Or more than one fallback with priority.
//
// i18n!("locales", fallback = ["en", "es]);
```

Or you can import by use directly:
Expand Down Expand Up @@ -188,7 +192,7 @@ You can use `rust_i18n::set_locale` to set the global locale at runtime, so that
rust_i18n::set_locale("zh-CN");
let locale = rust_i18n::locale();
assert_eq!(locale, "zh-CN");
assert_eq!(*locale, "zh-CN");
```

### Extend Backend
Expand Down Expand Up @@ -370,8 +374,12 @@ $ RUST_I18N_DEBUG=1 cargo build
Benchmark `t!` method, result on Apple M1:

```bash
t time: [100.91 ns 101.06 ns 101.24 ns]
t_with_args time: [495.56 ns 497.88 ns 500.64 ns]
t time: [58.274 ns 60.222 ns 62.390 ns]
t_with_locale time: [55.395 ns 57.106 ns 59.081 ns]
t_with_args time: [167.46 ns 170.94 ns 175.64 ns]
t_with_args (str) time: [164.85 ns 165.91 ns 167.41 ns]
t_with_args (many) time: [444.04 ns 452.17 ns 463.44 ns]
t_with_threads time: [414.26 ns 422.97 ns 433.53 ns]
```

The result `101 ns (0.0001 ms)` means if there have 10K translate texts, it will cost 1ms.
Expand Down
35 changes: 35 additions & 0 deletions benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ fn bench_t(c: &mut Criterion) {

c.bench_function("t_with_locale", |b| b.iter(|| t!("hello", locale = "en")));

c.bench_function("t_with_threads", |b| {
let exit_loop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut handles = Vec::new();
for _ in 0..4 {
let exit_loop = exit_loop.clone();
handles.push(std::thread::spawn(move || {
while !exit_loop.load(std::sync::atomic::Ordering::SeqCst) {
criterion::black_box(t!("hello"));
}
}));
}
b.iter(|| t!("hello"));
exit_loop.store(true, std::sync::atomic::Ordering::SeqCst);
for handle in handles {
handle.join().unwrap();
}
});

c.bench_function("t_lorem_ipsum", |b| b.iter(|| t!("lorem-ipsum")));

// 73.239 ns
c.bench_function("_rust_i18n_translate", |b| {
b.iter(|| crate::_rust_i18n_translate("en", "hello"))
Expand Down Expand Up @@ -44,6 +64,21 @@ fn bench_t(c: &mut Criterion) {
c.bench_function("t_with_args (str)", |b| {
b.iter(|| t!("a.very.nested.message", "name" = "Jason", "msg" = "Bla bla"))
});

c.bench_function("t_with_args (many)", |b| {
b.iter(|| {
t!(
"a.very.nested.response",
id = 123,
name = "Marion",
surname = "Christiansen",
email = "[email protected]",
city = "Litteltown",
zip = 8408,
website = "https://snoopy-napkin.name"
)
})
});
}

criterion_group!(benches, bench_t);
Expand Down
2 changes: 1 addition & 1 deletion crates/macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ rust-i18n-support = { path = "../support", version = "2.3.0" }
serde = "1"
serde_json = "1"
serde_yaml = "0.8"
syn = "2.0.18"
syn = { version = "2.0.18", features = ["full"] }

[dev-dependencies]
rust-i18n = { path = "../.." }
Expand Down
71 changes: 59 additions & 12 deletions crates/macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use syn::{parse_macro_input, Expr, Ident, LitStr, Token};

struct Args {
locales_path: String,
fallback: Option<String>,
fallback: Option<Vec<String>>,
extend: Option<Expr>,
}

Expand All @@ -17,14 +17,40 @@ impl Args {
Ok(())
}

fn consume_fallback(&mut self, input: syn::parse::ParseStream) -> syn::parse::Result<()> {
if let Ok(val) = input.parse::<LitStr>() {
self.fallback = Some(vec![val.value()]);
return Ok(());
}
let val = input.parse::<syn::ExprArray>()?;
let fallback = val
.elems
.into_iter()
.map(|expr| {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = expr
{
Ok(lit_str.value())
} else {
Err(input.error(
"`fallback` must be a string literal or an array of string literals",
))
}
})
.collect::<syn::parse::Result<Vec<String>>>()?;
self.fallback = Some(fallback);
Ok(())
}

fn consume_options(&mut self, input: syn::parse::ParseStream) -> syn::parse::Result<()> {
let ident = input.parse::<Ident>()?.to_string();
input.parse::<Token![=]>()?;

match ident.as_str() {
"fallback" => {
let val = input.parse::<LitStr>()?.value();
self.fallback = Some(val);
self.consume_fallback(input)?;
}
"backend" => {
let val = input.parse::<Expr>()?;
Expand Down Expand Up @@ -56,6 +82,9 @@ impl syn::parse::Parse for Args {
/// # fn v3() {
/// i18n!("locales", fallback = "en");
/// # }
/// # fn v4() {
/// i18n!("locales", fallback = ["en", "es"]);
/// # }
/// ```
///
/// Ref: https://docs.rs/syn/latest/syn/parse/index.html
Expand Down Expand Up @@ -99,6 +128,9 @@ impl syn::parse::Parse for Args {
/// # fn v3() {
/// i18n!("locales", fallback = "en");
/// # }
/// # fn v4() {
/// i18n!("locales", fallback = ["en", "es"]);
/// # }
/// ```
#[proc_macro]
pub fn i18n(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
Expand Down Expand Up @@ -147,7 +179,7 @@ fn generate_code(

let fallback = if let Some(fallback) = args.fallback {
quote! {
Some(#fallback)
Some(&[#(#fallback),*])
}
} else {
quote! {
Expand All @@ -166,6 +198,7 @@ fn generate_code(
// result
quote! {
use rust_i18n::BackendExt;
use std::borrow::Cow;

/// I18n backend instance
///
Expand All @@ -179,7 +212,7 @@ fn generate_code(
Box::new(backend)
});

static _RUST_I18N_FALLBACK_LOCALE: Option<&'static str> = #fallback;
static _RUST_I18N_FALLBACK_LOCALE: Option<&[&'static str]> = #fallback;

/// Lookup fallback locales
///
Expand All @@ -195,29 +228,31 @@ fn generate_code(
/// Get I18n text by locale and key
#[inline]
#[allow(missing_docs)]
pub fn _rust_i18n_translate(locale: &str, key: &str) -> String {
pub fn _rust_i18n_translate<'r>(locale: &str, key: &'r str) -> Cow<'r, str> {
if let Some(value) = _RUST_I18N_BACKEND.translate(locale, key) {
return value.to_string();
return value.into();
}

let mut current_locale = locale;
while let Some(fallback_locale) = _rust_i18n_lookup_fallback(current_locale) {
if let Some(value) = _RUST_I18N_BACKEND.translate(fallback_locale, key) {
return value.to_string();
return value.into();
}
current_locale = fallback_locale;
}

if let Some(fallback) = _RUST_I18N_FALLBACK_LOCALE {
if let Some(value) = _RUST_I18N_BACKEND.translate(fallback, key) {
return value.to_string();
for locale in fallback {
if let Some(value) = _RUST_I18N_BACKEND.translate(locale, key) {
return value.into();
}
}
}

if locale.is_empty() {
return key.to_string();
return key.into();
}
return format!("{}.{}", locale, key);
return format!("{}.{}", locale, key).into();
}

#[allow(missing_docs)]
Expand All @@ -228,3 +263,15 @@ fn generate_code(
}
}
}

#[proc_macro]
pub fn key(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let output = syn::parse::<syn::LitStr>(input.clone())
.map(|str| str.value())
.or(syn::parse::<syn::Ident>(input.clone()).map(|ident| format!("{}", ident)));

match output {
Ok(value) => quote! { #value }.into(),
Err(err) => err.to_compile_error().into(),
}
}
80 changes: 80 additions & 0 deletions crates/support/src/atomic_str.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use std::fmt;
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::Arc;

/// A thread-safe atomically reference-counting string.
pub struct AtomicStr(AtomicPtr<String>);

impl AtomicStr {
/// Create a new `AtomicStr` with the given value.
pub fn new(value: impl Into<String>) -> Self {
let arced = Arc::new(value.into());
Self(AtomicPtr::new(Arc::into_raw(arced) as _))
}

/// Get the string slice.
pub fn as_str(&self) -> &str {
unsafe {
let arced_ptr = self.0.load(Ordering::SeqCst);
assert!(!arced_ptr.is_null());
&*arced_ptr
}
}

/// Get the cloned inner `Arc<String>`.
pub fn clone_string(&self) -> Arc<String> {
unsafe {
let arced_ptr = self.0.load(Ordering::SeqCst);
assert!(!arced_ptr.is_null());
Arc::increment_strong_count(arced_ptr);
Arc::from_raw(arced_ptr)
}
}

/// Replaces the value at self with src, returning the old value, without dropping either.
pub fn replace(&self, src: impl Into<String>) -> Arc<String> {
unsafe {
let arced_new = Arc::new(src.into());
let arced_old_ptr = self.0.swap(Arc::into_raw(arced_new) as _, Ordering::SeqCst);
assert!(!arced_old_ptr.is_null());
Arc::from_raw(arced_old_ptr)
}
}
}

impl Drop for AtomicStr {
fn drop(&mut self) {
unsafe {
let arced_ptr = self.0.swap(std::ptr::null_mut(), Ordering::SeqCst);
assert!(!arced_ptr.is_null());
let _ = Arc::from_raw(arced_ptr);
}
}
}

impl AsRef<str> for AtomicStr {
fn as_ref(&self) -> &str {
self.as_str()
}
}

impl<T> From<T> for AtomicStr
where
T: Into<String>,
{
fn from(value: T) -> Self {
Self::new(value)
}
}

impl From<&AtomicStr> for Arc<String> {
fn from(value: &AtomicStr) -> Self {
value.clone_string()
}
}

impl fmt::Display for AtomicStr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
2 changes: 2 additions & 0 deletions crates/support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::fs::File;
use std::io::prelude::*;
use std::{collections::HashMap, path::Path};

mod atomic_str;
mod backend;
pub use atomic_str::AtomicStr;
pub use backend::{Backend, BackendExt, SimpleBackend};

type Locale = String;
Expand Down
2 changes: 1 addition & 1 deletion examples/app-load-path/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ extern crate rust_i18n;
i18n!("../locales");

fn hello() -> String {
t!("hello")
t!("hello").into()
}

fn main() {
Expand Down
2 changes: 1 addition & 1 deletion examples/app-workspace/app1/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ extern crate rust_i18n;
i18n!("../locales");

fn get_text() -> String {
t!("hello")
t!("hello").into()
}

fn main() {
Expand Down
2 changes: 1 addition & 1 deletion examples/app-workspace/crates/example-base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ extern crate rust_i18n;
i18n!("locales");

pub fn hello(name: &str) -> String {
t!("hello", name = name)
t!("hello", name = name).into()
}
2 changes: 1 addition & 1 deletion examples/foo/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use rust_i18n::t;

#[allow(unused)]
pub fn get_info() -> String {
t!("hello")
t!("hello").into()
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion examples/foo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ mod info;
rust_i18n::i18n!("locales", fallback = "en");

pub fn t(key: &str) -> String {
t!(key)
t!(key).to_string()
}
Loading

0 comments on commit 91abcd6

Please sign in to comment.