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::{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.clone());
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.clone());
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 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 std::sync::Arc;
367
368 use crate::LanguageAware;
369
370 use super::*;
371 use http::{header::ACCEPT_LANGUAGE, Request};
372 use topcoat::{
373 context::{ContextMap, CxBuilder},
374 cookie::CookieJarCell,
375 };
376
377 #[derive(Debug, Clone, PartialEq, Eq)]
378 struct LanguageSpec(String);
379
380 impl Default for LanguageSpec {
381 fn default() -> Self {
382 Self::get("en").unwrap()
383 }
384 }
385
386 impl LanguageAware for LanguageSpec {
387 fn language_id(&self) -> &str {
388 &self.0
389 }
390 }
391
392 impl MessageBundle for LanguageSpec {
393 fn get(language_id: &str) -> Option<Self>
394 where
395 Self: Sized,
396 {
397 match language_id {
398 "en" => Some(LanguageSpec("en".to_string())),
399 "de" => Some(LanguageSpec("de".to_string())),
400 "fr" => Some(LanguageSpec("fr".to_string())),
401 _ => None,
402 }
403 }
404
405 fn default_language_id() -> &'static str {
406 "en"
407 }
408
409 fn supported_language_ids() -> &'static [&'static str] {
410 &["de", "en", "fr"]
411 }
412 }
413
414 fn build_cx(
415 config: RequestLanguageConfig,
416 parts: http::request::Parts,
417 with_cookies: bool,
418 ) -> Cx {
419 let mut app_context = ContextMap::new();
420 app_context.insert(config);
421
422 let mut cx_builder = CxBuilder::new(Arc::new(app_context));
423 cx_builder.insert(parts);
424 if with_cookies {
425 cx_builder.insert(CookieJarCell::new());
426 }
427 cx_builder.build()
428 }
429
430 #[test]
431 fn test_language_from_header() {
432 let req = Request::builder()
433 .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
434 .body(String::default())
435 .unwrap();
436
437 let cx = build_cx(RequestLanguageConfig::default(), req.into_parts().0, false);
438
439 assert_eq!(
440 request_language::<LanguageSpec>(&cx),
441 LanguageSpec("en".to_string())
442 );
443 }
444
445 #[test]
446 fn test_language_from_cookie() {
447 let cookie_name = "lang";
448 let cookie_value = "de";
449
450 let req = Request::builder()
451 .header("Cookie", format!("{}={}", cookie_name, cookie_value))
452 .body(String::default())
453 .unwrap();
454
455 let cx = build_cx(
456 RequestLanguageConfig::builder()
457 .ignore_accept_language_header(true)
458 .language_cookie_name(cookie_name)
459 .build(),
460 req.into_parts().0,
461 true,
462 );
463
464 assert_eq!(
465 request_language::<LanguageSpec>(&cx),
466 LanguageSpec("de".to_string())
467 );
468 }
469
470 #[test]
471 fn test_fallback_language() {
472 let req = Request::builder().body(String::default()).unwrap();
473
474 let cx = build_cx(
475 RequestLanguageConfig::builder()
476 .fallback_language_id("fr")
477 .build(),
478 req.into_parts().0,
479 false,
480 );
481
482 assert_eq!(
483 request_language::<LanguageSpec>(&cx),
484 LanguageSpec("fr".to_string())
485 );
486 }
487
488 #[test]
489 fn test_no_language_specified() {
490 let req = Request::builder().body(String::default()).unwrap();
491
492 let cx = build_cx(RequestLanguageConfig::default(), req.into_parts().0, false);
493
494 assert_eq!(
495 request_language::<LanguageSpec>(&cx),
496 LanguageSpec::default()
497 );
498 }
499 }
500}