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::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.insert(parts);
426            // if with_cookies {
427            //     cx_builder.insert(CookieJarCell::new());
428            // }
429            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}