pub mod context;
pub mod loader;
pub mod translate;
pub use context::{I18nContext, I18nLayer, CURRENT_I18N};
pub use loader::{load_dir, load_from_pairs, LocaleStore};
pub fn translate(key: &str, args: &[(&str, String)]) -> String {
CURRENT_I18N
.try_with(|ctx| {
translate::do_translate(&ctx.store, &ctx.locale, &ctx.default_locale, key, args)
})
.unwrap_or_else(|_| key.to_string())
}
pub fn translate_plural(key: &str, count: u64, args: &[(&str, String)]) -> String {
CURRENT_I18N
.try_with(|ctx| {
translate::do_translate_plural(
&ctx.store,
&ctx.locale,
&ctx.default_locale,
key,
count,
args,
)
})
.unwrap_or_else(|_| key.to_string())
}
pub fn try_translate(key: &str, args: &[(&str, String)]) -> Option<String> {
CURRENT_I18N
.try_with(|ctx| {
translate::do_translate(&ctx.store, &ctx.locale, &ctx.default_locale, key, args)
})
.ok()
}
pub fn current_locale() -> String {
CURRENT_I18N
.try_with(|ctx| ctx.locale.clone())
.unwrap_or_else(|_| "en".to_string())
}
#[macro_export]
macro_rules! t {
($key:expr) => {
$crate::i18n::translate($key, &[])
};
($key:expr, count = $n:expr $(,)?) => {
$crate::i18n::translate_plural($key, ($n) as u64, &[])
};
($key:expr, count = $n:expr, $($k:ident = $v:expr),+ $(,)?) => {
$crate::i18n::translate_plural(
$key,
($n) as u64,
&[$( (stringify!($k), ($v).to_string()) ),+],
)
};
($key:expr, $($k:ident = $v:expr),+ $(,)?) => {
$crate::i18n::translate($key, &[$( (stringify!($k), ($v).to_string()) ),+])
};
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::sync::Arc;
fn make_ctx(
locale: &str,
entries: impl IntoIterator<Item = (&'static str, serde_json::Value)>,
) -> Arc<I18nContext> {
Arc::new(I18nContext {
locale: locale.to_string(),
default_locale: "en".to_string(),
store: load_from_pairs(entries),
})
}
#[tokio::test]
async fn translate_simple_key() {
let ctx = make_ctx("en", [("en", json!({ "hello": "Hello!" }))]);
let result = CURRENT_I18N
.scope(ctx, async { translate("hello", &[]) })
.await;
assert_eq!(result, "Hello!");
}
#[tokio::test]
async fn translate_with_interpolation() {
let ctx = make_ctx("en", [("en", json!({ "welcome": "Welcome, {name}!" }))]);
let result = CURRENT_I18N
.scope(ctx, async {
translate("welcome", &[("name", "Alice".to_string())])
})
.await;
assert_eq!(result, "Welcome, Alice!");
}
#[tokio::test]
async fn translate_falls_back_to_default_locale() {
let ctx = Arc::new(I18nContext {
locale: "fr".to_string(),
default_locale: "en".to_string(),
store: load_from_pairs([("en", json!({ "greeting": "Hello" }))]),
});
let result = CURRENT_I18N
.scope(ctx, async { translate("greeting", &[]) })
.await;
assert_eq!(result, "Hello");
}
#[tokio::test]
async fn translate_falls_back_to_key_when_missing_everywhere() {
let ctx = make_ctx("en", [("en", json!({}))]);
let result = CURRENT_I18N
.scope(ctx, async { translate("missing.key", &[]) })
.await;
assert_eq!(result, "missing.key");
}
#[tokio::test]
async fn translate_outside_scope_returns_key() {
let result = translate("some.key", &[]);
assert_eq!(result, "some.key");
}
#[tokio::test]
async fn try_translate_outside_scope_returns_none() {
assert!(try_translate("k", &[]).is_none());
}
#[tokio::test]
async fn try_translate_inside_scope_returns_some() {
let ctx = make_ctx("en", [("en", json!({ "k": "v" }))]);
let result = CURRENT_I18N
.scope(ctx, async { try_translate("k", &[]) })
.await;
assert_eq!(result, Some("v".to_string()));
}
#[tokio::test]
async fn plural_selects_one_form() {
let ctx = make_ctx(
"en",
[(
"en",
json!({ "items": { "one": "1 item", "other": "{count} items" } }),
)],
);
let result = CURRENT_I18N
.scope(ctx, async { translate_plural("items", 1, &[]) })
.await;
assert_eq!(result, "1 item");
}
#[tokio::test]
async fn plural_selects_other_form() {
let ctx = make_ctx(
"en",
[(
"en",
json!({ "items": { "one": "1 item", "other": "{count} items" } }),
)],
);
let result = CURRENT_I18N
.scope(ctx, async { translate_plural("items", 5, &[]) })
.await;
assert_eq!(result, "5 items");
}
#[tokio::test]
async fn plural_injects_count_automatically() {
let ctx = make_ctx(
"en",
[(
"en",
json!({ "n": { "one": "one", "other": "{count} things" } }),
)],
);
let result = CURRENT_I18N
.scope(ctx, async { translate_plural("n", 42, &[]) })
.await;
assert_eq!(result, "42 things");
}
#[tokio::test]
async fn plural_falls_back_to_default_locale() {
let ctx = Arc::new(I18nContext {
locale: "de".to_string(),
default_locale: "en".to_string(),
store: load_from_pairs([(
"en",
json!({ "msgs": { "one": "1 message", "other": "{count} messages" } }),
)]),
});
let result = CURRENT_I18N
.scope(ctx, async { translate_plural("msgs", 3, &[]) })
.await;
assert_eq!(result, "3 messages");
}
#[tokio::test]
async fn current_locale_returns_active_locale() {
let ctx = make_ctx("fr", []);
let result = CURRENT_I18N.scope(ctx, async { current_locale() }).await;
assert_eq!(result, "fr");
}
#[tokio::test]
async fn current_locale_outside_scope_returns_en() {
assert_eq!(current_locale(), "en");
}
#[tokio::test]
async fn macro_simple() {
let ctx = make_ctx("en", [("en", json!({ "hi": "Hi!" }))]);
let result = CURRENT_I18N.scope(ctx, async { t!("hi") }).await;
assert_eq!(result, "Hi!");
}
#[tokio::test]
async fn macro_with_variable() {
let ctx = make_ctx("en", [("en", json!({ "greet": "Hello, {name}!" }))]);
let name = "Bob";
let result = CURRENT_I18N
.scope(ctx, async { t!("greet", name = name) })
.await;
assert_eq!(result, "Hello, Bob!");
}
#[tokio::test]
async fn macro_plural() {
let ctx = make_ctx(
"en",
[(
"en",
json!({ "files": { "one": "1 file", "other": "{count} files" } }),
)],
);
let result = CURRENT_I18N
.scope(ctx, async { t!("files", count = 7u64) })
.await;
assert_eq!(result, "7 files");
}
#[tokio::test]
async fn macro_plural_with_extra_var() {
let ctx = make_ctx(
"en",
[(
"en",
json!({ "dl": { "one": "1 {kind}", "other": "{count} {kind}s" } }),
)],
);
let result = CURRENT_I18N
.scope(ctx, async { t!("dl", count = 3u64, kind = "file") })
.await;
assert_eq!(result, "3 files");
}
#[test]
fn detect_locale_from_query_param() {
let store = load_from_pairs([
("en", json!({ "k": "english" })),
("fr", json!({ "k": "french" })),
]);
assert!(store.contains_key("en"));
assert!(store.contains_key("fr"));
}
}