use axum::response::{IntoResponse, Response}; use axum_template::{RenderHtml, TemplateEngine}; use minijinja::context as template_context; use unic_langid::LanguageIdentifier; use crate::{ http::{ context::WebContext, middleware_i18n::Language, }, i18n::gender::Gender, storage::handle::model::Handle, }; /// Enhanced template renderer that handles i18n, HTMX, and gender context pub struct TemplateRenderer { pub web_context: WebContext, pub locale: LanguageIdentifier, pub user_gender: Option, pub hx_boosted: bool, pub hx_request: bool, } impl TemplateRenderer { /// Create a new template renderer with all context pub fn new( web_context: WebContext, language: Language, user_gender: Option, hx_boosted: bool, hx_request: bool, ) -> Self { Self { web_context, locale: language.0, user_gender, hx_boosted, hx_request, } } /// Render a template with automatic template selection and i18n context pub fn render_template( &self, template_base: &str, context: minijinja::Value, current_handle: Option<&Handle>, canonical_url: &str, ) -> Response { let template_name = self.select_template(template_base); let enhanced_context = self.enhance_context(context, current_handle, canonical_url); RenderHtml( &template_name, self.web_context.engine.clone(), enhanced_context, ) .into_response() } /// Render an error template with proper error handling pub fn render_error( &self, error: impl std::fmt::Display, base_context: minijinja::Value, ) -> Response { let template_name = self.select_template("alert"); let (err_bare, err_partial) = crate::errors::expand_error(error.to_string()); tracing::warn!(error = %error, "rendering error template"); let error_message = crate::i18n::fluent_loader::format_error( &Language(self.locale.clone()), &err_bare, &err_partial ); // Normalize locale to lowercase for consistent template naming let normalized_locale = self.locale.to_string().to_lowercase(); // Create error context using proper minijinja context merging let error_context = template_context! { message => error_message, locale => self.locale.to_string(), current_locale => normalized_locale, has_gender => self.user_gender.is_some(), user_gender => self.user_gender.as_ref().map(|g| g.to_string()).unwrap_or_default(), is_htmx => self.hx_request, is_boosted => self.hx_boosted, }; // Merge with base context if it contains useful data let final_context = if base_context.kind() == minijinja::value::ValueKind::Map { minijinja::value::merge_maps([base_context, error_context]) } else { error_context }; RenderHtml( &template_name, self.web_context.engine.clone(), final_context, ) .into_response() } /// Try to render a template, falling back to error on failure pub fn try_render_template( &self, template_base: &str, context: minijinja::Value, current_handle: Option<&Handle>, canonical_url: &str, ) -> Result { // Validate locale is supported if !self.web_context.i18n_context.supports_language(&self.locale) { tracing::warn!( locale = %self.locale, "unsupported locale requested, falling back to English" ); } Ok(self.render_template(template_base, context, current_handle, canonical_url)) } /// Select the appropriate template based on HTMX state and locale fn select_template(&self, base_name: &str) -> String { // Use the same fallback logic as the original select_template! macro select_template_with_fallback( base_name, &self.locale, self.hx_boosted, self.hx_request, ) } /// Enhance template context with i18n and system variables fn enhance_context( &self, base_context: minijinja::Value, current_handle: Option<&Handle>, canonical_url: &str, ) -> minijinja::Value { // Normalize locale to lowercase for consistent template naming let normalized_locale = self.locale.to_string().to_lowercase(); // Create enhancement context let enhancement_context = template_context! { language => self.locale.to_string(), locale => self.locale.to_string(), current_locale => normalized_locale, current_handle => current_handle, has_gender => self.user_gender.is_some(), user_gender => self.user_gender.as_ref().map(|g| g.to_string()).unwrap_or_default(), is_htmx => self.hx_request, is_boosted => self.hx_boosted, canonical_url => canonical_url, site_base => format!("https://{}", self.web_context.config.external_base), features => template_context! { htmx_enabled => true, i18n_enabled => true, gender_support => self.user_gender.is_some(), }, }; // Merge contexts if base_context is a map, otherwise use enhancement context if base_context.kind() == minijinja::value::ValueKind::Map { minijinja::value::merge_maps([base_context, enhancement_context]) } else { enhancement_context } } /// Validate locale and provide fallback if needed pub fn validate_locale(&self) -> LanguageIdentifier { if self.web_context.i18n_context.supports_language(&self.locale) { self.locale.clone() } else { // Fall back to English US "en-US".parse().unwrap_or_else(|_| self.locale.clone()) } } /// Get available locales for template use pub fn get_available_locales(&self) -> Vec { self.web_context .i18n_context .supported_languages .iter() .map(|lang| lang.to_string()) .collect() } /// Check if current locale has gender support pub fn has_gender_support(&self) -> bool { // For now, assume all locales support gender // This can be enhanced with locale-specific gender support checks true } } /// Helper trait for extracting common template rendering parameters from request context pub trait TemplateContext { fn current_handle(&self) -> Option<&Handle>; fn canonical_url(&self, base: &str) -> String; fn language(&self) -> &Language; fn user_gender(&self) -> Option { None } } /// Macro for easier template rendering with automatic context enhancement #[macro_export] macro_rules! render_template { ($renderer:expr, $template:expr, $context:expr, $handle:expr, $url:expr) => { $renderer.render_template($template, $context, $handle, $url) }; ($renderer:expr, $template:expr, $context:expr) => { $renderer.render_template($template, $context, None, "") }; } /// Macro for error template rendering with proper context #[macro_export] macro_rules! render_error { ($renderer:expr, $error:expr, $context:expr) => { Ok($renderer.render_error($error, $context)) }; ($renderer:expr, $error:expr) => { Ok($renderer.render_error($error, minijinja::context!{})) }; } /// Advanced template selection with fallback support pub fn select_template_with_fallback( base_name: &str, locale: &LanguageIdentifier, hx_boosted: bool, hx_request: bool, ) -> String { let locale_str = locale.to_string().to_lowercase(); let fallback_locale = "en-us"; let template_variant = if hx_boosted { "bare.html" } else if hx_request { "partial.html" } else { "html" }; // Handle dotted template names (e.g., "settings.language" -> "settings.en-us.language.html") let primary_template = if base_name.contains('.') { let parts: Vec<&str> = base_name.split('.').collect(); if parts.len() == 2 { format!("{}.{}.{}.{}", parts[0], locale_str, parts[1], template_variant) } else { // For more complex dotted names, just append locale at the end as before format!("{}.{}.{}", base_name, locale_str, template_variant) } } else { format!("{}.{}.{}", base_name, locale_str, template_variant) }; // If the requested language is already the fallback, return it directly if locale_str == fallback_locale { return primary_template; } // Check if the language-specific template exists by checking the file system let template_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("templates") .join(&primary_template); if template_path.exists() { tracing::debug!(template = %primary_template, "using language-specific template"); primary_template } else { // Fall back to English template with the same logic let fallback_template = if base_name.contains('.') { let parts: Vec<&str> = base_name.split('.').collect(); if parts.len() == 2 { format!("{}.{}.{}.{}", parts[0], fallback_locale, parts[1], template_variant) } else { format!("{}.{}.{}", base_name, fallback_locale, template_variant) } } else { format!("{}.{}.{}", base_name, fallback_locale, template_variant) }; tracing::debug!( requested_template = %primary_template, fallback_template = %fallback_template, "language-specific template not found, falling back to English" ); fallback_template } } /// Utility for rendering HTMX partial responses pub fn render_htmx_partial( engine: E, template_name: &str, context: minijinja::Value, ) -> Response { RenderHtml(template_name, engine, context).into_response() } /// Utility for rendering full page responses with SEO metadata pub fn render_full_page( engine: E, template_name: &str, context: minijinja::Value, page_title: &str, page_description: &str, ) -> Response { let enhancement_context = template_context! { page_title => page_title, page_description => page_description, meta => template_context! { title => page_title, description => page_description, }, }; let final_context = if context.kind() == minijinja::value::ValueKind::Map { minijinja::value::merge_maps([context, enhancement_context]) } else { enhancement_context }; RenderHtml(template_name, engine, final_context).into_response() } #[cfg(test)] mod tests { use super::*; use crate::i18n::gender::Gender; use unic_langid::langid; #[test] fn test_template_selection() { // Test basic template selection let locale = langid!("en-US"); let full_template = select_template_with_fallback("home", &locale, false, false); assert_eq!(full_template, "home.en-us.html"); let partial_template = select_template_with_fallback("home", &locale, false, true); assert_eq!(partial_template, "home.en-us.partial.html"); let bare_template = select_template_with_fallback("home", &locale, true, false); assert_eq!(bare_template, "home.en-us.bare.html"); } #[test] fn test_template_selection_with_dotted_names() { let locale = langid!("en-US"); // Test dotted template names like "settings.language" let settings_template = select_template_with_fallback("settings.language", &locale, false, false); assert_eq!(settings_template, "settings.en-us.language.html"); let settings_partial = select_template_with_fallback("settings.language", &locale, false, true); assert_eq!(settings_partial, "settings.en-us.language.partial.html"); // Test with French locale let fr_locale = langid!("fr-CA"); let fr_settings = select_template_with_fallback("settings.language", &fr_locale, false, false); assert_eq!(fr_settings, "settings.fr-ca.language.html"); } #[test] fn test_template_selection_with_different_locale() { let locale = langid!("fr-FR"); let template = select_template_with_fallback("profile", &locale, false, false); // Should fall back to English since French templates don't exist assert_eq!(template, "profile.en-us.html"); } #[test] fn test_gender_context() { let gender = Some(Gender::Female); assert!(gender.is_some()); let gender_str = gender.as_ref().map(|g| g.to_string()).unwrap_or_default(); assert_eq!(gender_str, "female"); } }