/// 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, } 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"); } }