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(feature = "axum")]
13pub mod axum {
14    use std::{future::Future, sync::Arc};
15
16    use accept_language;
17    use axum_core::extract::FromRequestParts;
18    use axum_extra::extract::CookieJar;
19    use http::{header::ACCEPT_LANGUAGE, request::Parts, StatusCode};
20
21    use crate::MessageBundle;
22
23    pub struct RequestLanguage<T: MessageBundle>(pub T);
24
25    #[derive(Debug, Default)]
26    struct RequestLanguageConfigInner {
27        skip_language_header: bool,
28        language_cookie: Option<String>,
29        default_language: Option<String>,
30    }
31
32    #[derive(Debug, Clone, Default)]
33    pub struct RequestLanguageConfig {
34        inner: Arc<RequestLanguageConfigInner>,
35    }
36
37    impl RequestLanguageConfig {
38        pub fn builder() -> RequestLanguageConfigBuilder {
39            RequestLanguageConfigBuilder {
40                inner: RequestLanguageConfigInner::default(),
41            }
42        }
43
44        pub fn ignore_language_header(&self) -> bool {
45            self.inner.skip_language_header
46        }
47
48        pub fn language_cookie_name(&self) -> Option<&str> {
49            self.inner.language_cookie.as_deref()
50        }
51
52        pub fn fallback_language_id(&self) -> Option<&str> {
53            self.inner.default_language.as_deref()
54        }
55    }
56
57    pub struct RequestLanguageConfigBuilder {
58        inner: RequestLanguageConfigInner,
59    }
60
61    impl RequestLanguageConfigBuilder {
62        pub fn ignore_accept_language_header(mut self, value: bool) -> Self {
63            self.inner.skip_language_header = value;
64            self
65        }
66
67        pub fn language_cookie_name(mut self, value: &str) -> Self {
68            self.inner.language_cookie = Some(value.to_string());
69            self
70        }
71
72        pub fn fallback_language_id(mut self, value: &str) -> Self {
73            self.inner.default_language = Some(value.to_string());
74            self
75        }
76
77        pub fn build(self) -> RequestLanguageConfig {
78            RequestLanguageConfig {
79                inner: Arc::new(self.inner),
80            }
81        }
82    }
83
84    impl<T: MessageBundle, S> FromRequestParts<S> for RequestLanguage<T>
85    where
86        S: Send + Sync,
87    {
88        type Rejection = (StatusCode, &'static str);
89
90        fn from_request_parts(
91            parts: &mut Parts,
92            _state: &S,
93        ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
94            async move {
95                let cfg = parts
96                    .extensions
97                    .get::<RequestLanguageConfig>()
98                    .map(|cfg| cfg.clone())
99                    .unwrap_or_default();
100
101                if let Some(cookie_name) = cfg.language_cookie_name().as_ref() {
102                    if let Some(bundle) = CookieJar::from_request_parts(parts, _state)
103                        .await
104                        .ok()
105                        .and_then(|jar| {
106                            jar.get(cookie_name)
107                                .map(|cookie| cookie.value_trimmed())
108                                .and_then(T::get)
109                        })
110                    {
111                        return Ok(Self(bundle));
112                    };
113                };
114
115                let bundle = if !cfg.ignore_language_header() {
116                    parts
117                        .headers
118                        .get(ACCEPT_LANGUAGE)
119                        .and_then(|v| v.to_str().ok())
120                        .and_then(|value| {
121                            accept_language::intersection_ordered(
122                                value,
123                                T::supported_language_ids(),
124                            )
125                            .first()
126                            .and_then(|lang| T::get(lang))
127                        })
128                } else {
129                    None
130                };
131
132                Ok(Self(
133                    bundle
134                        .or_else(|| cfg.fallback_language_id().and_then(T::get))
135                        .unwrap_or_default(),
136                ))
137            }
138        }
139    }
140
141    #[cfg(test)]
142    mod tests {
143
144        use crate::LanguageAware;
145
146        use super::*;
147        use http::Request;
148
149        #[derive(Debug, Clone, PartialEq, Eq)]
150        struct LanguageSpec(String);
151
152        impl Default for LanguageSpec {
153            fn default() -> Self {
154                Self::get("en").unwrap()
155            }
156        }
157
158        impl LanguageAware for LanguageSpec {
159            fn language_id(&self) -> &str {
160                &self.0
161            }
162        }
163
164        impl MessageBundle for LanguageSpec {
165            fn get(language_id: &str) -> Option<Self>
166            where
167                Self: Sized,
168            {
169                match language_id {
170                    "en" => Some(LanguageSpec("en".to_string())),
171                    "de" => Some(LanguageSpec("de".to_string())),
172                    "fr" => Some(LanguageSpec("fr".to_string())),
173                    _ => None,
174                }
175            }
176
177            fn default_language_id() -> &'static str {
178                "en"
179            }
180
181            fn supported_language_ids() -> &'static [&'static str] {
182                &["de", "en", "fr"]
183            }
184        }
185
186        #[tokio::test]
187        async fn test_language_from_header() {
188            let req = Request::builder()
189                .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
190                .body(String::default())
191                .unwrap();
192
193            let parts = &mut req.into_parts().0;
194            parts.extensions.insert(RequestLanguageConfig::default());
195
196            assert_eq!(
197                RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
198                    .await
199                    .unwrap()
200                    .0,
201                LanguageSpec("en".to_string())
202            );
203        }
204
205        #[tokio::test]
206        async fn test_language_from_cookie() {
207            let cookie_name = "lang";
208            let cookie_value = "de";
209
210            // Create a fake request with the specific cookie set
211            let mut req = Request::builder()
212                .header("Cookie", format!("{}={}", cookie_name, cookie_value))
213                .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
214                .body(String::default())
215                .unwrap();
216            req.extensions_mut().insert(
217                RequestLanguageConfig::builder()
218                    .ignore_accept_language_header(true)
219                    .language_cookie_name(cookie_name)
220                    .build(),
221            );
222
223            let parts = &mut req.into_parts().0;
224            assert_eq!(
225                RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
226                    .await
227                    .unwrap()
228                    .0,
229                LanguageSpec("de".to_string())
230            );
231        }
232
233        #[tokio::test]
234        async fn test_default_language() {
235            let mut req = Request::builder().body(String::default()).unwrap();
236
237            req.extensions_mut().insert(
238                RequestLanguageConfig::builder()
239                    .fallback_language_id("fr")
240                    .build(),
241            );
242
243            let parts = &mut req.into_parts().0;
244
245            assert_eq!(
246                RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
247                    .await
248                    .unwrap()
249                    .0,
250                LanguageSpec("fr".to_string())
251            );
252        }
253
254        #[tokio::test]
255        async fn test_no_language_specified() {
256            let mut req = Request::builder().body(String::default()).unwrap();
257
258            req.extensions_mut()
259                .insert(RequestLanguageConfig::default());
260
261            let parts = &mut req.into_parts().0;
262
263            assert_eq!(
264                RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
265                    .await
266                    .unwrap()
267                    .0,
268                LanguageSpec::default()
269            );
270        }
271    }
272}