i18n+filtering fork - fluent-templates v2
at main 257 lines 7.9 kB view raw
1use axum::extract::FromRef; 2use axum::{ 3 extract::FromRequestParts, 4 http::request::Parts, 5 response::{IntoResponse, Response}, 6}; 7use axum_extra::extract::Cached; 8use axum_template::engine::Engine; 9use cookie::Key; 10use hickory_resolver::TokioAsyncResolver; 11use minijinja::context as template_context; 12use std::{ops::Deref, sync::Arc}; 13use unic_langid::LanguageIdentifier; 14 15#[cfg(feature = "reload")] 16use minijinja_autoreload::AutoReloader; 17 18#[cfg(feature = "reload")] 19pub type AppEngine = Engine<AutoReloader>; 20 21#[cfg(feature = "embed")] 22use minijinja::Environment; 23 24use crate::{ 25 config::Config, 26 http::middleware_auth::Auth, 27 http::middleware_i18n::Language, 28 i18n::{get_supported_languages, get_translation, Locales}, 29 storage::handle::model::Handle, 30 storage::{CachePool, StoragePool}, 31}; 32 33#[cfg(feature = "embed")] 34pub type AppEngine = Engine<Environment<'static>>; 35 36pub struct I18nContext { 37 pub supported_languages: Vec<LanguageIdentifier>, 38 pub locales: Locales, 39} 40 41impl I18nContext { 42 pub fn new() -> Self { 43 let supported_languages = get_supported_languages(); 44 let locales = Locales::new(supported_languages.clone()); 45 Self { 46 supported_languages, 47 locales, 48 } 49 } 50 51 /// Get a translation for the given locale and key 52 pub fn get_translation(&self, locale: &LanguageIdentifier, key: &str) -> String { 53 get_translation(locale, key, None) 54 } 55 56 /// Get a translation with arguments 57 pub fn get_translation_with_args( 58 &self, 59 locale: &LanguageIdentifier, 60 key: &str, 61 args: std::collections::HashMap<std::borrow::Cow<'static, str>, fluent::FluentValue<'static>>, 62 ) -> String { 63 // Convert the HashMap to the expected format 64 let converted_args: std::collections::HashMap<String, fluent_templates::fluent_bundle::FluentValue> = args 65 .into_iter() 66 .map(|(k, v)| { 67 let converted_value = match v { 68 fluent::FluentValue::String(s) => fluent_templates::fluent_bundle::FluentValue::String(s), 69 fluent::FluentValue::Number(n) => fluent_templates::fluent_bundle::FluentValue::Number(n), 70 fluent::FluentValue::None => fluent_templates::fluent_bundle::FluentValue::String("".into()), 71 fluent::FluentValue::Error => fluent_templates::fluent_bundle::FluentValue::String("".into()), 72 _ => fluent_templates::fluent_bundle::FluentValue::String("".into()), // Handle any other variants 73 }; 74 (k.to_string(), converted_value) 75 }) 76 .collect(); 77 78 get_translation(locale, key, Some(converted_args)) 79 } 80 81 /// Check if a language is supported 82 pub fn supports_language(&self, locale: &LanguageIdentifier) -> bool { 83 self.supported_languages.contains(locale) 84 } 85} 86 87pub struct InnerWebContext { 88 pub engine: AppEngine, 89 pub http_client: reqwest::Client, 90 pub pool: StoragePool, 91 pub cache_pool: CachePool, 92 pub config: Config, 93 pub i18n_context: I18nContext, 94 pub dns_resolver: hickory_resolver::TokioAsyncResolver, 95} 96 97#[derive(Clone, FromRef)] 98pub struct WebContext(pub Arc<InnerWebContext>); 99 100impl Deref for WebContext { 101 type Target = InnerWebContext; 102 103 fn deref(&self) -> &Self::Target { 104 &self.0 105 } 106} 107 108impl WebContext { 109 pub fn new( 110 pool: StoragePool, 111 cache_pool: CachePool, 112 engine: AppEngine, 113 http_client: &reqwest::Client, 114 config: Config, 115 i18n_context: I18nContext, 116 dns_resolver: TokioAsyncResolver, 117 ) -> Self { 118 Self(Arc::new(InnerWebContext { 119 pool, 120 cache_pool, 121 engine, 122 http_client: http_client.clone(), 123 config, 124 i18n_context, 125 dns_resolver, 126 })) 127 } 128} 129 130impl FromRef<WebContext> for Key { 131 fn from_ref(context: &WebContext) -> Self { 132 context.0.config.http_cookie_key.as_ref().clone() 133 } 134} 135 136// New structs for reducing handler function arguments 137 138/// A context struct specifically for admin handlers 139pub struct AdminRequestContext { 140 pub web_context: WebContext, 141 pub language: Language, 142 pub admin_handle: Handle, 143 pub auth: Auth, 144} 145 146impl<S> FromRequestParts<S> for AdminRequestContext 147where 148 S: Send + Sync, 149 WebContext: FromRef<S>, 150{ 151 type Rejection = Response; 152 153 async fn from_request_parts(parts: &mut Parts, context: &S) -> Result<Self, Self::Rejection> { 154 // Extract the needed components 155 let web_context = WebContext::from_ref(context); 156 let language = Language::from_request_parts(parts, context).await?; 157 let cached_auth = Cached::<Auth>::from_request_parts(parts, context).await?; 158 159 // Validate user is an admin 160 let admin_handle = match cached_auth.0.require_admin(&web_context.config) { 161 Ok(handle) => handle, 162 Err(err) => return Err(err.into_response()), 163 }; 164 165 Ok(Self { 166 web_context, 167 language, 168 admin_handle, 169 auth: cached_auth.0, 170 }) 171 } 172} 173 174/// Helper function to create standard template context for admin views 175pub fn admin_template_context( 176 ctx: &AdminRequestContext, 177 canonical_url: &str, 178) -> minijinja::value::Value { 179 let supported_languages: Vec<String> = ctx.web_context.i18n_context.supported_languages 180 .iter() 181 .map(|lang| lang.to_string()) 182 .collect(); 183 184 template_context! { 185 language => ctx.language.to_string(), 186 locale => ctx.language.to_string(), 187 current_handle => ctx.admin_handle.clone(), 188 canonical_url => canonical_url, 189 supported_languages => supported_languages, 190 } 191} 192 193/// A context struct for regular authenticated user handlers 194pub struct UserRequestContext { 195 pub web_context: WebContext, 196 pub language: Language, 197 pub current_handle: Option<Handle>, 198 pub auth: Auth, 199} 200 201impl<S> FromRequestParts<S> for UserRequestContext 202where 203 S: Send + Sync, 204 WebContext: FromRef<S>, 205{ 206 type Rejection = Response; 207 208 async fn from_request_parts(parts: &mut Parts, context: &S) -> Result<Self, Self::Rejection> { 209 // Extract the needed components 210 let web_context = WebContext::from_ref(context); 211 let language = Language::from_request_parts(parts, context).await?; 212 let cached_auth = Cached::<Auth>::from_request_parts(parts, context).await?; 213 214 Ok(Self { 215 web_context, 216 language, 217 current_handle: cached_auth.0 .0.clone(), 218 auth: cached_auth.0, 219 }) 220 } 221} 222 223/// Helper function to create standard template context for user views 224pub fn user_template_context( 225 ctx: &UserRequestContext, 226 canonical_url: &str, 227) -> minijinja::value::Value { 228 let supported_languages: Vec<String> = ctx.web_context.i18n_context.supported_languages 229 .iter() 230 .map(|lang| lang.to_string()) 231 .collect(); 232 233 template_context! { 234 language => ctx.language.to_string(), 235 locale => ctx.language.to_string(), 236 current_handle => ctx.current_handle.clone(), 237 canonical_url => canonical_url, 238 supported_languages => supported_languages, 239 } 240} 241 242/// Helper function to create basic template context with just locale information 243pub fn basic_template_context( 244 language: &Language, 245 supported_languages: &[LanguageIdentifier], 246) -> minijinja::value::Value { 247 let supported_languages: Vec<String> = supported_languages 248 .iter() 249 .map(|lang| lang.to_string()) 250 .collect(); 251 252 template_context! { 253 language => language.to_string(), 254 locale => language.to_string(), 255 supported_languages => supported_languages, 256 } 257}