use leptos::prelude::*; use serde::{Deserialize, Serialize}; use shared::{Texts, load_texts_toml}; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Language { English, Spanish, } impl Language { pub fn code(&self) -> &'static str { match self { Language::English => "en", Language::Spanish => "es", } } pub fn display_name(&self) -> &'static str { match self { Language::English => "English", Language::Spanish => "Español", } } pub fn from_code(code: &str) -> Self { match code { "es" => Language::Spanish, _ => Language::English, // Default to English } } pub fn all() -> Vec { vec![Language::English, Language::Spanish] } } impl Default for Language { fn default() -> Self { Language::English } } #[derive(Clone)] pub struct I18nContext { pub language: ReadSignal, pub set_language: WriteSignal, pub texts: Memo, } impl I18nContext { /// Get translated text pub fn t(&self, key: &str, _args: Option<&HashMap<&str, &str>>) -> String { let texts = self.texts.get(); let lang_code = self.language.get().code(); let translations = match lang_code { "es" => &texts.es, _ => &texts.en, }; translations .get(key) .cloned() .unwrap_or_else(|| key.to_string()) } /// Get current language code pub fn current_lang(&self) -> String { self.language.get().code().to_string() } /// Check if current language is specific language pub fn is_language(&self, lang: Language) -> bool { self.language.get() == lang } } #[component] pub fn I18nProvider(children: leptos::prelude::Children) -> impl IntoView { // Initialize language from localStorage or default to English let initial_language = Language::default(); let (language, set_language) = signal(initial_language); // Load texts from embedded resources let texts = Memo::new(move |_| load_texts_toml().unwrap_or_default()); let context = I18nContext { language: language.into(), set_language, texts, }; provide_context(context); view! { {children()} } } #[derive(Clone)] pub struct UseI18n(pub I18nContext); impl UseI18n { pub fn new() -> Self { Self(expect_context::()) } /// Get translated text pub fn t(&self, key: &str) -> String { self.0.t(key, None) } /// Get translated text with arguments pub fn t_with_args(&self, key: &str, args: &HashMap<&str, &str>) -> String { self.0.t(key, Some(args)) } /// Change language pub fn set_language(&self, language: Language) { self.0.set_language.set(language); } /// Get current language pub fn language(&self) -> Language { self.0.language.get() } /// Get current language code pub fn lang_code(&self) -> String { self.0.current_lang() } /// Check if current language is specific language pub fn is_language(&self, lang: Language) -> bool { self.0.is_language(lang) } } /// Hook to use internationalization pub fn use_i18n() -> UseI18n { UseI18n::new() } /// Language selector component #[component] pub fn LanguageSelector(#[prop(optional)] class: Option) -> impl IntoView { let i18n = use_i18n(); let (is_open, set_is_open) = signal(false); view! {
// Click outside to close
} } /// Compact language toggle component #[component] pub fn LanguageToggle(#[prop(optional)] class: Option) -> impl IntoView { let i18n = use_i18n(); view! { } } #[cfg(test)] mod tests { use super::*; #[test] fn test_language_codes() { assert_eq!(Language::English.code(), "en"); assert_eq!(Language::Spanish.code(), "es"); } #[test] fn test_language_from_code() { assert_eq!(Language::from_code("en"), Language::English); assert_eq!(Language::from_code("es"), Language::Spanish); assert_eq!(Language::from_code("invalid"), Language::English); // Default fallback } #[test] fn test_language_display_names() { assert_eq!(Language::English.display_name(), "English"); assert_eq!(Language::Spanish.display_name(), "Español"); } }