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::request,
291 view::{AttributeValueViewParts, 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 impl NodeViewParts for &Message {
303 fn into_view_parts(self, _: &Cx, parts: &mut PartsWriter<'_>) {
304 parts.push_str(&self.0);
305 }
306 }
307
308 impl AttributeValueViewParts for Message {
309 fn attribute_present(&self) -> bool {
310 true
311 }
312
313 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
314 parts.push_str(&self.0);
315 }
316 }
317
318 impl AttributeValueViewParts for &Message {
319 fn attribute_present(&self) -> bool {
320 true
321 }
322
323 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
324 parts.push_str(&self.0);
325 }
326 }
327
328 pub fn request_language<T>(cx: &Cx) -> T
329 where
330 T: MessageBundle,
331 {
332 let cfg = try_app_context::<RequestLanguageConfig>(cx)
333 .cloned()
334 .unwrap_or_default();
335
336 if let Some(cookie_name) = cfg.language_cookie_name() {
337 if let Some(bundle) = cookies(cx)
338 .get(cookie_name)
339 .map(|cookie| cookie.value().to_owned())
340 .and_then(|val| T::get(&val))
341 {
342 return bundle;
343 }
344 }
345
346 let bundle = if !cfg.ignore_language_header() {
347 request::headers(cx)
348 .get(ACCEPT_LANGUAGE)
349 .and_then(|v| v.to_str().ok())
350 .and_then(|value| {
351 accept_language::intersection_ordered(value, T::supported_language_ids())
352 .first()
353 .and_then(|lang| T::get(lang))
354 })
355 } else {
356 None
357 };
358
359 bundle
360 .or_else(|| cfg.fallback_language_id().and_then(T::get))
361 .unwrap_or_default()
362 }
363
364 #[cfg(test)]
365 mod tests {
366 use crate::LanguageAware;
367
368 use super::*;
369 use http::{header::ACCEPT_LANGUAGE, Request};
370 use topcoat::{
371 context::{Cx, CxTestBuilder},
372 cookie::CookieJarCell,
373 };
374
375 #[derive(Debug, Clone, PartialEq, Eq)]
376 struct LanguageSpec(String);
377
378 impl Default for LanguageSpec {
379 fn default() -> Self {
380 Self::get("en").unwrap()
381 }
382 }
383
384 impl LanguageAware for LanguageSpec {
385 fn language_id(&self) -> &str {
386 &self.0
387 }
388 }
389
390 impl MessageBundle for LanguageSpec {
391 fn get(language_id: &str) -> Option<Self>
392 where
393 Self: Sized,
394 {
395 match language_id {
396 "en" => Some(LanguageSpec("en".to_string())),
397 "de" => Some(LanguageSpec("de".to_string())),
398 "fr" => Some(LanguageSpec("fr".to_string())),
399 _ => None,
400 }
401 }
402
403 fn default_language_id() -> &'static str {
404 "en"
405 }
406
407 fn supported_language_ids() -> &'static [&'static str] {
408 &["de", "en", "fr"]
409 }
410 }
411
412 fn build_cx(
413 config: RequestLanguageConfig,
414 parts: http::request::Parts,
415 with_cookies: bool,
416 ) -> Cx {
417 let cx_builder = CxTestBuilder::new()
418 .app_context(config)
419 .request_context(parts);
420 let cx_builder = if with_cookies {
421 cx_builder.request_context(CookieJarCell::new())
422 } else {
423 cx_builder
424 };
425 cx_builder.build()
430 }
431
432 #[test]
433 fn test_language_from_header() {
434 let req = Request::builder()
435 .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
436 .body(String::default())
437 .unwrap();
438
439 let cx = build_cx(RequestLanguageConfig::default(), req.into_parts().0, false);
440
441 assert_eq!(
442 request_language::<LanguageSpec>(&cx),
443 LanguageSpec("en".to_string())
444 );
445 }
446
447 #[test]
448 fn test_language_from_cookie() {
449 let cookie_name = "lang";
450 let cookie_value = "de";
451
452 let req = Request::builder()
453 .header("Cookie", format!("{}={}", cookie_name, cookie_value))
454 .body(String::default())
455 .unwrap();
456
457 let cx = build_cx(
458 RequestLanguageConfig::builder()
459 .ignore_accept_language_header(true)
460 .language_cookie_name(cookie_name)
461 .build(),
462 req.into_parts().0,
463 true,
464 );
465
466 assert_eq!(
467 request_language::<LanguageSpec>(&cx),
468 LanguageSpec("de".to_string())
469 );
470 }
471
472 #[test]
473 fn test_fallback_language() {
474 let req = Request::builder().body(String::default()).unwrap();
475
476 let cx = build_cx(
477 RequestLanguageConfig::builder()
478 .fallback_language_id("fr")
479 .build(),
480 req.into_parts().0,
481 false,
482 );
483
484 assert_eq!(
485 request_language::<LanguageSpec>(&cx),
486 LanguageSpec("fr".to_string())
487 );
488 }
489
490 #[test]
491 fn test_no_language_specified() {
492 let req = Request::builder().body(String::default()).unwrap();
493
494 let cx = build_cx(RequestLanguageConfig::default(), req.into_parts().0, false);
495
496 assert_eq!(
497 request_language::<LanguageSpec>(&cx),
498 LanguageSpec::default()
499 );
500 }
501 }
502}