forked from
smokesignal.events/smokesignal
i18n+filtering fork - fluent-templates v2
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}