Skip to main content

fluent_static/
support.rs

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            // Create a fake request with the specific cookie set
218            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}