//! # RUSTELO Shared //! //!
//! RUSTELO //!
//! //! Shared types, utilities, and functionality for the RUSTELO web application framework. //! //! ## Overview //! //! The shared crate contains common types, utilities, and functionality that are used across //! both the client and server components of RUSTELO applications. This includes authentication //! types, content management structures, internationalization support, and configuration utilities. //! //! ## Features //! //! - **🔐 Authentication Types** - Shared auth structures and utilities //! - **📄 Content Management** - Common content types and processing //! - **🌐 Internationalization** - Multi-language support with Fluent //! - **⚙️ Configuration** - Shared configuration management //! - **🎨 Menu System** - Navigation and menu configuration //! - **📋 Type Safety** - Strongly typed interfaces for client-server communication //! //! ## Architecture //! //! The shared crate is organized into several key modules: //! //! - [`auth`] - Authentication types and utilities //! - [`content`] - Content management types and processing //! //! Additional functionality includes: //! - Menu configuration and internationalization //! - Fluent resource management //! - Content file loading utilities //! - Type-safe configuration structures //! //! ## Quick Start //! //! ### Menu Configuration //! //! ```rust //! use shared::{MenuConfig, load_menu_toml}; //! //! // Load menu from TOML file //! let menu = load_menu_toml().unwrap_or_default(); //! //! // Access menu items //! for item in menu.menu { //! println!("Route: {}, Label (EN): {}", item.route, item.label.en); //! } //! ``` //! //! ### Internationalization //! //! ```rust //! use shared::{get_bundle, t}; //! use std::collections::HashMap; //! //! // Get localization bundle //! let bundle = get_bundle("en").expect("Failed to load English bundle"); //! //! // Translate text //! let welcome_msg = t(&bundle, "welcome", None); //! println!("{}", welcome_msg); //! //! // Translate with arguments //! let mut args = HashMap::new(); //! args.insert("name", "RUSTELO"); //! let greeting = t(&bundle, "greeting", Some(&args)); //! ``` //! //! ## Type Definitions //! //! ### Menu System //! //! ```rust //! use shared::{MenuConfig, MenuItem, MenuLabel}; //! //! let menu_item = MenuItem { //! route: "/about".to_string(), //! label: MenuLabel { //! en: "About".to_string(), //! es: "Acerca de".to_string(), //! }, //! }; //! ``` //! //! ### Text Localization //! //! ```rust //! use shared::Texts; //! use std::collections::HashMap; //! //! let mut texts = Texts::default(); //! texts.en.insert("welcome".to_string(), "Welcome".to_string()); //! texts.es.insert("welcome".to_string(), "Bienvenido".to_string()); //! ``` //! //! ## Internationalization Support //! //! RUSTELO uses [Fluent](https://projectfluent.org/) for internationalization: //! //! - **Resource Loading** - Automatic loading of .ftl files //! - **Language Fallback** - Graceful fallback to English //! - **Parameter Substitution** - Dynamic text with variables //! - **Pluralization** - Proper plural forms for different languages //! //! ### Supported Languages //! //! - **English (en)** - Primary language //! - **Spanish (es)** - Secondary language //! - **Extensible** - Easy to add more languages //! //! ## Configuration Management //! //! The shared crate provides utilities for loading configuration from various sources: //! //! - **TOML Files** - Structured configuration files //! - **Environment Variables** - Runtime configuration //! - **Fallback Defaults** - Graceful degradation //! //! ## Error Handling //! //! All functions return `Result` types for proper error handling: //! //! ```rust //! use shared::load_menu_toml; //! //! match load_menu_toml() { //! Ok(menu) => println!("Loaded {} menu items", menu.menu.len()), //! Err(e) => eprintln!("Failed to load menu: {}", e), //! } //! ``` //! //! ## Cross-Platform Support //! //! The shared crate is designed to work across different targets: //! //! - **Server** - Native Rust environments //! - **Client** - WebAssembly (WASM) environments //! - **Testing** - Development and CI environments //! //! ## Performance Considerations //! //! - **Lazy Loading** - Resources loaded on demand //! - **Caching** - Efficient resource reuse //! - **Memory Management** - Careful memory usage for WASM //! - **Bundle Size** - Optimized for small WASM bundles //! //! ## Examples //! //! ### Creating a Multi-language Menu //! //! ```rust //! use shared::{MenuConfig, MenuItem, MenuLabel}; //! //! let menu = MenuConfig { //! menu: vec![ //! MenuItem { //! route: "/".to_string(), //! label: MenuLabel { //! en: "Home".to_string(), //! es: "Inicio".to_string(), //! }, //! }, //! MenuItem { //! route: "/about".to_string(), //! label: MenuLabel { //! en: "About".to_string(), //! es: "Acerca de".to_string(), //! }, //! }, //! ], //! }; //! ``` //! //! ### Loading and Using Fluent Resources //! //! ```rust //! use shared::{get_bundle, t}; //! use std::collections::HashMap; //! //! // Load Spanish bundle //! let bundle = get_bundle("es").expect("Failed to load Spanish bundle"); //! //! // Simple translation //! let app_title = t(&bundle, "app-title", None); //! //! // Translation with variables //! let mut args = HashMap::new(); //! args.insert("user", "María"); //! let welcome = t(&bundle, "welcome-user", Some(&args)); //! ``` //! //! ## Contributing //! //! When adding new shared functionality: //! //! 1. **Keep it Generic** - Ensure it's useful for both client and server //! 2. **Document Types** - Add comprehensive documentation //! 3. **Handle Errors** - Use proper error types and handling //! 4. **Test Thoroughly** - Add tests for all platforms //! 5. **Consider Performance** - Optimize for WASM environments //! //! ## License //! //! This project is licensed under the MIT License - see the [LICENSE](https://github.com/yourusername/rustelo/blob/main/LICENSE) file for details. #![allow(unused_imports)] #![allow(dead_code)] pub mod auth; pub mod content; use fluent::{FluentBundle, FluentResource}; use fluent_bundle::FluentArgs; use serde::Deserialize; use std::borrow::Cow; use std::collections::HashMap; use unic_langid::LanguageIdentifier; #[derive(Debug, Clone, Deserialize)] pub struct MenuLabel { pub en: String, pub es: String, } impl Default for MenuLabel { fn default() -> Self { Self { en: "Menu".to_string(), es: "Menú".to_string(), } } } #[derive(Debug, Clone, Deserialize)] pub struct MenuItem { pub route: String, pub label: MenuLabel, } impl Default for MenuItem { fn default() -> Self { Self { route: "/".to_string(), label: MenuLabel::default(), } } } #[derive(Debug, Clone, Deserialize)] pub struct MenuConfig { pub menu: Vec, } impl Default for MenuConfig { fn default() -> Self { Self { menu: vec![ MenuItem { route: "/".to_string(), label: MenuLabel { en: "Home".to_string(), es: "Inicio".to_string(), }, }, MenuItem { route: "/about".to_string(), label: MenuLabel { en: "About".to_string(), es: "Acerca de".to_string(), }, }, ], } } } #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct Texts { pub en: std::collections::HashMap, pub es: std::collections::HashMap, } impl Default for Texts { fn default() -> Self { Self { en: std::collections::HashMap::new(), es: std::collections::HashMap::new(), } } } // Load FTL resources from files instead of hardcoded content fn load_en_ftl() -> &'static str { Box::leak( std::fs::read_to_string( get_content_path("en.ftl") .unwrap_or_else(|_| std::path::PathBuf::from("content/en.ftl")), ) .unwrap_or_else(|_| "app-title = Rustelo App\nwelcome = Welcome".to_string()) .into_boxed_str(), ) } fn load_es_ftl() -> &'static str { Box::leak( std::fs::read_to_string( get_content_path("es.ftl") .unwrap_or_else(|_| std::path::PathBuf::from("content/es.ftl")), ) .unwrap_or_else(|_| "app-title = Aplicación Rustelo\nwelcome = Bienvenido".to_string()) .into_boxed_str(), ) } // Dynamic FTL resources loaded from files static EN_FTL: std::sync::OnceLock<&str> = std::sync::OnceLock::new(); static ES_FTL: std::sync::OnceLock<&str> = std::sync::OnceLock::new(); fn get_en_ftl() -> &'static str { EN_FTL.get_or_init(|| load_en_ftl()) } fn get_es_ftl() -> &'static str { ES_FTL.get_or_init(|| load_es_ftl()) } // Content loading utilities use std::path::PathBuf; pub fn get_content_path(filename: &str) -> Result> { // Try to get root path from environment or use current directory let root_path = std::env::var("ROOT_PATH") .map(PathBuf::from) .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); let content_path = root_path.join("content").join(filename); // If the file exists, return the path if content_path.exists() { Ok(content_path) } else { // Fallback to relative path let fallback_path = PathBuf::from("content").join(filename); if fallback_path.exists() { Ok(fallback_path) } else { Err(format!("Content file not found: {}", filename).into()) } } } pub fn get_bundle(lang: &str) -> Result, Box> { let langid: LanguageIdentifier = lang.parse().unwrap_or_else(|_| { "en".parse().unwrap_or_else(|e| { eprintln!( "Critical error: Default language 'en' failed to parse: {}", e ); // This should never happen, but we'll create a minimal fallback LanguageIdentifier::from_parts( unic_langid::subtags::Language::from_bytes(b"en").unwrap_or_else(|e| { eprintln!("Critical error: failed to create 'en' language: {}", e); // Fallback to creating a new language identifier from scratch match "en".parse::() { Ok(lang) => lang, Err(_) => { // If even this fails, we'll use the default language eprintln!("Using default language as final fallback"); unic_langid::subtags::Language::default() } } }), None, None, &[], ) }) }); let ftl_str = match lang { "es" => get_es_ftl(), _ => get_en_ftl(), }; let res = FluentResource::try_new(ftl_str.to_string()) .map_err(|e| format!("Failed to parse FTL resource: {:?}", e))?; let mut bundle = FluentBundle::new(vec![langid]); bundle .add_resource(res) .map_err(|e| format!("Failed to add FTL resource to bundle: {:?}", e))?; Ok(bundle) } pub fn t( bundle: &FluentBundle, key: &str, args: Option<&HashMap<&str, &str>>, ) -> String { let msg = bundle.get_message(key).and_then(|m| m.value()); if let Some(msg) = msg { let mut errors = vec![]; let fargs = args.map(|a| { let mut f = FluentArgs::new(); for (k, v) in a.iter() { f.set(*k, *v); } f }); bundle .format_pattern(msg, fargs.as_ref(), &mut errors) .to_string() } else { key.to_string() } } pub fn load_menu_toml() -> Result> { // Try to load from file system first match get_content_path("menu.toml") { Ok(path) => { let content = std::fs::read_to_string(&path) .map_err(|e| format!("Failed to read menu.toml from {}: {}", path.display(), e))?; toml::from_str(&content).map_err(|e| format!("Failed to parse menu.toml: {}", e).into()) } Err(_) => { // Return default menu if file not found Ok(MenuConfig { menu: vec![] }) } } } pub fn load_texts_toml() -> Result> { // Try to load from file system first match get_content_path("texts.toml") { Ok(path) => { let content = std::fs::read_to_string(&path) .map_err(|e| format!("Failed to read texts.toml from {}: {}", path.display(), e))?; toml::from_str(&content) .map_err(|e| format!("Failed to parse texts.toml: {}", e).into()) } Err(_) => { // Return default texts if file not found Ok(Texts { en: std::collections::HashMap::new(), es: std::collections::HashMap::new(), }) } } }