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::{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}