Archived
210 lines
5.9 KiB
Rust
210 lines
5.9 KiB
Rust
/// The t() translation function and LocaleContext.
|
|
///
|
|
/// Components call `ctx.t("key")` or `ctx.t_plural("key", count)`.
|
|
/// The context flows down from the experience root and carries the active bundle.
|
|
|
|
use std::collections::HashMap;
|
|
use crate::bundle::LocaleBundle;
|
|
use crate::locale::Locale;
|
|
|
|
/// The active localization context.
|
|
///
|
|
/// Holds the current locale and its translation bundle. Passed down
|
|
/// through the component tree. When the locale changes, the context
|
|
/// is updated and all components that used it re-render.
|
|
#[derive(Debug, Clone)]
|
|
pub struct LocaleContext {
|
|
pub locale: Locale,
|
|
bundle: LocaleBundle,
|
|
/// Fallback bundle (typically English) used when a key is missing.
|
|
fallback: Option<LocaleBundle>,
|
|
}
|
|
|
|
impl LocaleContext {
|
|
/// Create a context with a locale and its bundle.
|
|
pub fn new(locale: Locale, bundle: LocaleBundle) -> Self {
|
|
Self {
|
|
locale,
|
|
bundle,
|
|
fallback: None,
|
|
}
|
|
}
|
|
|
|
/// Set a fallback bundle for missing keys.
|
|
pub fn with_fallback(mut self, fallback: LocaleBundle) -> Self {
|
|
self.fallback = Some(fallback);
|
|
self
|
|
}
|
|
|
|
/// Translate a key with optional variable interpolation.
|
|
///
|
|
/// Returns the key itself if not found (never panics).
|
|
pub fn t(&self, key: &str) -> String {
|
|
self.t_vars(key, &HashMap::new())
|
|
}
|
|
|
|
/// Translate a key with variables.
|
|
pub fn t_vars(&self, key: &str, vars: &HashMap<&str, String>) -> String {
|
|
// Try primary bundle
|
|
if let Some(s) = self.bundle.translate(key, vars) {
|
|
return s;
|
|
}
|
|
// Try fallback bundle
|
|
if let Some(ref fb) = self.fallback {
|
|
if let Some(s) = fb.translate(key, vars) {
|
|
return s;
|
|
}
|
|
}
|
|
// Return the key itself — visible but never panics
|
|
key.to_string()
|
|
}
|
|
|
|
/// Translate a plural key with a count.
|
|
pub fn t_plural(&self, key: &str, count: i64) -> String {
|
|
self.t_plural_vars(key, count, &HashMap::new())
|
|
}
|
|
|
|
/// Translate a plural key with a count and extra variables.
|
|
pub fn t_plural_vars(
|
|
&self,
|
|
key: &str,
|
|
count: i64,
|
|
vars: &HashMap<&str, String>,
|
|
) -> String {
|
|
if let Some(s) = self.bundle.translate_plural(key, count, vars) {
|
|
return s;
|
|
}
|
|
if let Some(ref fb) = self.fallback {
|
|
if let Some(s) = fb.translate_plural(key, count, vars) {
|
|
return s;
|
|
}
|
|
}
|
|
key.to_string()
|
|
}
|
|
|
|
/// The current locale tag (e.g. "en-US").
|
|
pub fn locale_tag(&self) -> String {
|
|
self.locale.tag()
|
|
}
|
|
|
|
/// Whether the current locale is RTL.
|
|
pub fn is_rtl(&self) -> bool {
|
|
self.locale.is_rtl()
|
|
}
|
|
}
|
|
|
|
/// Convenience macro for translation calls.
|
|
///
|
|
/// ```ignore
|
|
/// // Simple
|
|
/// let text = t!(ctx, "profile.follow");
|
|
///
|
|
/// // With variables
|
|
/// let text = t!(ctx, "greeting", name => "Alice");
|
|
///
|
|
/// // Plural
|
|
/// let text = t_n!(ctx, "profile.followers", count);
|
|
/// ```
|
|
#[macro_export]
|
|
macro_rules! t {
|
|
($ctx:expr, $key:expr) => {
|
|
$ctx.t($key)
|
|
};
|
|
($ctx:expr, $key:expr, $($var:ident => $val:expr),+) => {{
|
|
let mut vars = std::collections::HashMap::new();
|
|
$(vars.insert(stringify!($var), $val.to_string());)+
|
|
$ctx.t_vars($key, &vars)
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! t_n {
|
|
($ctx:expr, $key:expr, $count:expr) => {
|
|
$ctx.t_plural($key, $count as i64)
|
|
};
|
|
($ctx:expr, $key:expr, $count:expr, $($var:ident => $val:expr),+) => {{
|
|
let mut vars = std::collections::HashMap::new();
|
|
$(vars.insert(stringify!($var), $val.to_string());)+
|
|
$ctx.t_plural_vars($key, $count as i64, &vars)
|
|
}};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::bundle::LocaleBundle;
|
|
|
|
fn make_ctx() -> LocaleContext {
|
|
let mut bundle = LocaleBundle::new(Locale::en_us());
|
|
bundle.insert("profile.follow", "Follow");
|
|
bundle.insert("greeting", "Hello, {name}!");
|
|
let mut forms = HashMap::new();
|
|
forms.insert("one".to_string(), "{n} Follower".to_string());
|
|
forms.insert("other".to_string(), "{n} Followers".to_string());
|
|
bundle.insert_plural("profile.followers", forms);
|
|
LocaleContext::new(Locale::en_us(), bundle)
|
|
}
|
|
|
|
#[test]
|
|
fn t_simple() {
|
|
let ctx = make_ctx();
|
|
assert_eq!(ctx.t("profile.follow"), "Follow");
|
|
}
|
|
|
|
#[test]
|
|
fn t_missing_returns_key() {
|
|
let ctx = make_ctx();
|
|
assert_eq!(ctx.t("no.such.key"), "no.such.key");
|
|
}
|
|
|
|
#[test]
|
|
fn t_vars_interpolation() {
|
|
let ctx = make_ctx();
|
|
let mut vars = HashMap::new();
|
|
vars.insert("name", "Bob".to_string());
|
|
assert_eq!(ctx.t_vars("greeting", &vars), "Hello, Bob!");
|
|
}
|
|
|
|
#[test]
|
|
fn t_plural_one() {
|
|
let ctx = make_ctx();
|
|
assert_eq!(ctx.t_plural("profile.followers", 1), "1 Follower");
|
|
}
|
|
|
|
#[test]
|
|
fn t_plural_many() {
|
|
let ctx = make_ctx();
|
|
assert_eq!(ctx.t_plural("profile.followers", 42), "42 Followers");
|
|
}
|
|
|
|
#[test]
|
|
fn is_rtl_english() {
|
|
let ctx = make_ctx();
|
|
assert!(!ctx.is_rtl());
|
|
}
|
|
|
|
#[test]
|
|
fn is_rtl_arabic() {
|
|
let bundle = LocaleBundle::new(Locale::ar());
|
|
let ctx = LocaleContext::new(Locale::ar(), bundle);
|
|
assert!(ctx.is_rtl());
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_bundle_used() {
|
|
let primary_bundle = LocaleBundle::new(Locale::new("fr"));
|
|
// Key not in French bundle
|
|
let mut fallback = LocaleBundle::new(Locale::en_us());
|
|
fallback.insert("app.name", "My App");
|
|
let ctx = LocaleContext::new(Locale::new("fr"), primary_bundle)
|
|
.with_fallback(fallback);
|
|
assert_eq!(ctx.t("app.name"), "My App");
|
|
}
|
|
|
|
#[test]
|
|
fn locale_tag() {
|
|
let ctx = make_ctx();
|
|
assert_eq!(ctx.locale_tag(), "en-US");
|
|
}
|
|
}
|