1#[cfg(feature = "maud")]
2pub mod maud {
3 use crate::Message;
4
5 impl maud::Render for Message {
6 fn render_to(&self, buffer: &mut String) {
7 str::render_to(&self.0, buffer);
8 }
9 }
10}
11
12#[cfg(any(feature = "axum", feature = "topcoat"))]
13mod http {
14 use std::sync::Arc;
15
16 #[derive(Debug, Default)]
17 pub struct RequestLanguageConfigInner {
18 pub skip_language_header: bool,
19 pub language_cookie: Option<String>,
20 pub default_language: Option<String>,
21 }
22
23 #[derive(Debug, Clone, Default)]
24 pub struct RequestLanguageConfig {
25 pub inner: Arc<RequestLanguageConfigInner>,
26 }
27
28 impl RequestLanguageConfig {
29 pub fn builder() -> RequestLanguageConfigBuilder {
30 RequestLanguageConfigBuilder {
31 inner: RequestLanguageConfigInner::default(),
32 }
33 }
34
35 pub fn ignore_language_header(&self) -> bool {
36 self.inner.skip_language_header
37 }
38
39 pub fn language_cookie_name(&self) -> Option<&str> {
40 self.inner.language_cookie.as_deref()
41 }
42
43 pub fn fallback_language_id(&self) -> Option<&str> {
44 self.inner.default_language.as_deref()
45 }
46 }
47
48 pub struct RequestLanguageConfigBuilder {
49 pub inner: RequestLanguageConfigInner,
50 }
51
52 impl RequestLanguageConfigBuilder {
53 pub fn ignore_accept_language_header(mut self, value: bool) -> Self {
54 self.inner.skip_language_header = value;
55 self
56 }
57
58 pub fn language_cookie_name(mut self, value: &str) -> Self {
59 self.inner.language_cookie = Some(value.to_string());
60 self
61 }
62
63 pub fn fallback_language_id(mut self, value: &str) -> Self {
64 self.inner.default_language = Some(value.to_string());
65 self
66 }
67
68 pub fn build(self) -> RequestLanguageConfig {
69 RequestLanguageConfig {
70 inner: Arc::new(self.inner),
71 }
72 }
73 }
74}
75
76#[cfg(feature = "axum")]
77pub mod axum {
78 use std::future::Future;
79
80 use accept_language;
81 use axum_core::extract::FromRequestParts;
82 use axum_extra::extract::CookieJar;
83 use http::{header::ACCEPT_LANGUAGE, request::Parts, StatusCode};
84
85 use crate::MessageBundle;
86
87 pub use super::http::{RequestLanguageConfig, RequestLanguageConfigBuilder};
88
89 pub struct RequestLanguage<T: MessageBundle>(pub T);
90
91 impl<T: MessageBundle, S> FromRequestParts<S> for RequestLanguage<T>
92 where
93 S: Send + Sync,
94 {
95 type Rejection = (StatusCode, &'static str);
96
97 fn from_request_parts(
98 parts: &mut Parts,
99 _state: &S,
100 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
101 async move {
102 let cfg = parts
103 .extensions
104 .get::<RequestLanguageConfig>()
105 .map(|cfg| cfg.clone())
106 .unwrap_or_default();
107
108 if let Some(cookie_name) = cfg.language_cookie_name().as_ref() {
109 if let Some(bundle) = CookieJar::from_request_parts(parts, _state)
110 .await
111 .ok()
112 .and_then(|jar| {
113 jar.get(cookie_name)
114 .map(|cookie| cookie.value_trimmed())
115 .and_then(T::get)
116 })
117 {
118 return Ok(Self(bundle));
119 };
120 };
121
122 let bundle = if !cfg.ignore_language_header() {
123 parts
124 .headers
125 .get(ACCEPT_LANGUAGE)
126 .and_then(|v| v.to_str().ok())
127 .and_then(|value| {
128 accept_language::intersection_ordered(
129 value,
130 T::supported_language_ids(),
131 )
132 .first()
133 .and_then(|lang| T::get(lang))
134 })
135 } else {
136 None
137 };
138
139 Ok(Self(
140 bundle
141 .or_else(|| cfg.fallback_language_id().and_then(T::get))
142 .unwrap_or_default(),
143 ))
144 }
145 }
146 }
147
148 #[cfg(test)]
149 mod tests {
150
151 use crate::LanguageAware;
152
153 use super::*;
154 use http::Request;
155
156 #[derive(Debug, Clone, PartialEq, Eq)]
157 struct LanguageSpec(String);
158
159 impl Default for LanguageSpec {
160 fn default() -> Self {
161 Self::get("en").unwrap()
162 }
163 }
164
165 impl LanguageAware for LanguageSpec {
166 fn language_id(&self) -> &str {
167 &self.0
168 }
169 }
170
171 impl MessageBundle for LanguageSpec {
172 fn get(language_id: &str) -> Option<Self>
173 where
174 Self: Sized,
175 {
176 match language_id {
177 "en" => Some(LanguageSpec("en".to_string())),
178 "de" => Some(LanguageSpec("de".to_string())),
179 "fr" => Some(LanguageSpec("fr".to_string())),
180 _ => None,
181 }
182 }
183
184 fn default_language_id() -> &'static str {
185 "en"
186 }
187
188 fn supported_language_ids() -> &'static [&'static str] {
189 &["de", "en", "fr"]
190 }
191 }
192
193 #[tokio::test]
194 async fn test_language_from_header() {
195 let req = Request::builder()
196 .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
197 .body(String::default())
198 .unwrap();
199
200 let parts = &mut req.into_parts().0;
201 parts.extensions.insert(RequestLanguageConfig::default());
202
203 assert_eq!(
204 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
205 .await
206 .unwrap()
207 .0,
208 LanguageSpec("en".to_string())
209 );
210 }
211
212 #[tokio::test]
213 async fn test_language_from_cookie() {
214 let cookie_name = "lang";
215 let cookie_value = "de";
216
217 let mut req = Request::builder()
219 .header("Cookie", format!("{}={}", cookie_name, cookie_value))
220 .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
221 .body(String::default())
222 .unwrap();
223 req.extensions_mut().insert(
224 RequestLanguageConfig::builder()
225 .ignore_accept_language_header(true)
226 .language_cookie_name(cookie_name)
227 .build(),
228 );
229
230 let parts = &mut req.into_parts().0;
231 assert_eq!(
232 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
233 .await
234 .unwrap()
235 .0,
236 LanguageSpec("de".to_string())
237 );
238 }
239
240 #[tokio::test]
241 async fn test_default_language() {
242 let mut req = Request::builder().body(String::default()).unwrap();
243
244 req.extensions_mut().insert(
245 RequestLanguageConfig::builder()
246 .fallback_language_id("fr")
247 .build(),
248 );
249
250 let parts = &mut req.into_parts().0;
251
252 assert_eq!(
253 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
254 .await
255 .unwrap()
256 .0,
257 LanguageSpec("fr".to_string())
258 );
259 }
260
261 #[tokio::test]
262 async fn test_no_language_specified() {
263 let mut req = Request::builder().body(String::default()).unwrap();
264
265 req.extensions_mut()
266 .insert(RequestLanguageConfig::default());
267
268 let parts = &mut req.into_parts().0;
269
270 assert_eq!(
271 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
272 .await
273 .unwrap()
274 .0,
275 LanguageSpec::default()
276 );
277 }
278 }
279}
280
281#[cfg(feature = "topcoat")]
282pub mod topcoat {
283 use crate::{Message, MessageBundle};
284
285 use accept_language;
286 use http::header::ACCEPT_LANGUAGE;
287 use topcoat::{
288 context::{try_app_context, Cx},
289 cookie::{cookies, Cookies},
290 router::headers,
291 view::{NodeViewParts, PartsWriter},
292 };
293
294 pub use super::http::{RequestLanguageConfig, RequestLanguageConfigBuilder};
295
296 impl NodeViewParts for Message {
297 fn into_view_parts(self, _: &Cx, parts: &mut PartsWriter<'_>) {
298 parts.push_str(self.0);
299 }
300 }
301
302 pub fn request_language<T>(cx: &Cx) -> T
303 where
304 T: MessageBundle,
305 {
306 let cfg = try_app_context::<RequestLanguageConfig>(cx)
307 .cloned()
308 .unwrap_or_default();
309
310 if let Some(cookie_name) = cfg.language_cookie_name() {
311 if let Some(bundle) = cookies(cx)
312 .get(cookie_name)
313 .map(|cookie| cookie.value().to_owned())
314 .and_then(|val| T::get(&val))
315 {
316 return bundle;
317 }
318 }
319
320 let bundle = if !cfg.ignore_language_header() {
321 headers(cx)
322 .get(ACCEPT_LANGUAGE)
323 .and_then(|v| v.to_str().ok())
324 .and_then(|value| {
325 accept_language::intersection_ordered(value, T::supported_language_ids())
326 .first()
327 .and_then(|lang| T::get(lang))
328 })
329 } else {
330 None
331 };
332
333 bundle
334 .or_else(|| cfg.fallback_language_id().and_then(T::get))
335 .unwrap_or_default()
336 }
337
338 #[cfg(test)]
339 mod tests {
340 use std::sync::Arc;
341
342 use crate::LanguageAware;
343
344 use super::*;
345 use http::{header::ACCEPT_LANGUAGE, Request};
346 use topcoat::{
347 context::{ContextMap, CxBuilder},
348 cookie::CookieJarCell,
349 };
350
351 #[derive(Debug, Clone, PartialEq, Eq)]
352 struct LanguageSpec(String);
353
354 impl Default for LanguageSpec {
355 fn default() -> Self {
356 Self::get("en").unwrap()
357 }
358 }
359
360 impl LanguageAware for LanguageSpec {
361 fn language_id(&self) -> &str {
362 &self.0
363 }
364 }
365
366 impl MessageBundle for LanguageSpec {
367 fn get(language_id: &str) -> Option<Self>
368 where
369 Self: Sized,
370 {
371 match language_id {
372 "en" => Some(LanguageSpec("en".to_string())),
373 "de" => Some(LanguageSpec("de".to_string())),
374 "fr" => Some(LanguageSpec("fr".to_string())),
375 _ => None,
376 }
377 }
378
379 fn default_language_id() -> &'static str {
380 "en"
381 }
382
383 fn supported_language_ids() -> &'static [&'static str] {
384 &["de", "en", "fr"]
385 }
386 }
387
388 fn build_cx(
389 config: RequestLanguageConfig,
390 parts: http::request::Parts,
391 with_cookies: bool,
392 ) -> Cx {
393 let mut app_context = ContextMap::new();
394 app_context.insert(config);
395
396 let mut cx_builder = CxBuilder::new(Arc::new(app_context));
397 cx_builder.insert(parts);
398 if with_cookies {
399 cx_builder.insert(CookieJarCell::new());
400 }
401 cx_builder.build()
402 }
403
404 #[test]
405 fn test_language_from_header() {
406 let req = Request::builder()
407 .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
408 .body(String::default())
409 .unwrap();
410
411 let cx = build_cx(RequestLanguageConfig::default(), req.into_parts().0, false);
412
413 assert_eq!(
414 request_language::<LanguageSpec>(&cx),
415 LanguageSpec("en".to_string())
416 );
417 }
418
419 #[test]
420 fn test_language_from_cookie() {
421 let cookie_name = "lang";
422 let cookie_value = "de";
423
424 let req = Request::builder()
425 .header("Cookie", format!("{}={}", cookie_name, cookie_value))
426 .body(String::default())
427 .unwrap();
428
429 let cx = build_cx(
430 RequestLanguageConfig::builder()
431 .ignore_accept_language_header(true)
432 .language_cookie_name(cookie_name)
433 .build(),
434 req.into_parts().0,
435 true,
436 );
437
438 assert_eq!(
439 request_language::<LanguageSpec>(&cx),
440 LanguageSpec("de".to_string())
441 );
442 }
443
444 #[test]
445 fn test_fallback_language() {
446 let req = Request::builder().body(String::default()).unwrap();
447
448 let cx = build_cx(
449 RequestLanguageConfig::builder()
450 .fallback_language_id("fr")
451 .build(),
452 req.into_parts().0,
453 false,
454 );
455
456 assert_eq!(
457 request_language::<LanguageSpec>(&cx),
458 LanguageSpec("fr".to_string())
459 );
460 }
461
462 #[test]
463 fn test_no_language_specified() {
464 let req = Request::builder().body(String::default()).unwrap();
465
466 let cx = build_cx(RequestLanguageConfig::default(), req.into_parts().0, false);
467
468 assert_eq!(
469 request_language::<LanguageSpec>(&cx),
470 LanguageSpec::default()
471 );
472 }
473 }
474}