actix_web/http/header/
accept_language.rs

1use language_tags::LanguageTag;
2
3use super::{common_header, Preference, Quality, QualityItem};
4use crate::http::header;
5
6common_header! {
7    /// `Accept-Language` header, defined
8    /// in [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5)
9    ///
10    /// The `Accept-Language` header field can be used by user agents to indicate the set of natural
11    /// languages that are preferred in the response.
12    ///
13    /// The `Accept-Language` header is defined in
14    /// [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) using language
15    /// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1).
16    ///
17    /// # ABNF
18    /// ```plain
19    /// Accept-Language = 1#( language-range [ weight ] )
20    /// language-range  = (1*8ALPHA *("-" 1*8alphanum)) / "*"
21    /// alphanum        = ALPHA / DIGIT
22    /// weight          = OWS ";" OWS "q=" qvalue
23    /// qvalue          = ( "0" [ "." 0*3DIGIT ] )
24    ///                 / ( "1" [ "." 0*3("0") ] )
25    /// ```
26    ///
27    /// # Example Values
28    /// - `da, en-gb;q=0.8, en;q=0.7`
29    /// - `en-us;q=1.0, en;q=0.5, fr`
30    /// - `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5`
31    ///
32    /// # Examples
33    /// ```
34    /// use actix_web::HttpResponse;
35    /// use actix_web::http::header::{AcceptLanguage, QualityItem};
36    ///
37    /// let mut builder = HttpResponse::Ok();
38    /// builder.insert_header(
39    ///     AcceptLanguage(vec![
40    ///         "en-US".parse().unwrap(),
41    ///     ])
42    /// );
43    /// ```
44    ///
45    /// ```
46    /// use actix_web::HttpResponse;
47    /// use actix_web::http::header::{AcceptLanguage, QualityItem, q};
48    ///
49    /// let mut builder = HttpResponse::Ok();
50    /// builder.insert_header(
51    ///     AcceptLanguage(vec![
52    ///         "da".parse().unwrap(),
53    ///         "en-GB;q=0.8".parse().unwrap(),
54    ///         "en;q=0.7".parse().unwrap(),
55    ///     ])
56    /// );
57    /// ```
58    (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem<Preference<LanguageTag>>)*
59
60    test_parse_and_format {
61        common_header_test!(no_headers, [b""; 0], Some(AcceptLanguage(vec![])));
62
63        common_header_test!(empty_header, [b""; 1], Some(AcceptLanguage(vec![])));
64
65        common_header_test!(
66            example_from_rfc,
67            [b"da, en-gb;q=0.8, en;q=0.7"]
68        );
69
70
71        common_header_test!(
72            not_ordered_by_weight,
73            [b"en-US, en; q=0.5, fr"],
74            Some(AcceptLanguage(vec![
75                QualityItem::max("en-US".parse().unwrap()),
76                QualityItem::new("en".parse().unwrap(), q(0.5)),
77                QualityItem::max("fr".parse().unwrap()),
78            ]))
79        );
80
81        common_header_test!(
82            has_wildcard,
83            [b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"],
84            Some(AcceptLanguage(vec![
85                QualityItem::max("fr-CH".parse().unwrap()),
86                QualityItem::new("fr".parse().unwrap(), q(0.9)),
87                QualityItem::new("en".parse().unwrap(), q(0.8)),
88                QualityItem::new("de".parse().unwrap(), q(0.7)),
89                QualityItem::new("*".parse().unwrap(), q(0.5)),
90            ]))
91        );
92    }
93}
94
95impl AcceptLanguage {
96    /// Extracts the most preferable language, accounting for [q-factor weighting].
97    ///
98    /// If no q-factors are provided, the first language is chosen. Note that items without
99    /// q-factors are given the maximum preference value.
100    ///
101    /// As per the spec, returns [`Preference::Any`] if contained list is empty.
102    ///
103    /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
104    pub fn preference(&self) -> Preference<LanguageTag> {
105        let mut max_item = None;
106        let mut max_pref = Quality::ZERO;
107
108        // uses manual max lookup loop since we want the first occurrence in the case of same
109        // preference but `Iterator::max_by_key` would give us the last occurrence
110
111        for pref in &self.0 {
112            // only change if strictly greater
113            // equal items, even while unsorted, still have higher preference if they appear first
114            if pref.quality > max_pref {
115                max_pref = pref.quality;
116                max_item = Some(pref.item.clone());
117            }
118        }
119
120        max_item.unwrap_or(Preference::Any)
121    }
122
123    /// Returns a sorted list of languages from highest to lowest precedence, accounting
124    /// for [q-factor weighting].
125    ///
126    /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
127    pub fn ranked(&self) -> Vec<Preference<LanguageTag>> {
128        if self.0.is_empty() {
129            return vec![];
130        }
131
132        let mut types = self.0.clone();
133
134        // use stable sort so items with equal q-factor retain listed order
135        types.sort_by(|a, b| {
136            // sort by q-factor descending
137            b.quality.cmp(&a.quality)
138        });
139
140        types.into_iter().map(|q_item| q_item.item).collect()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::http::header::*;
148
149    #[test]
150    fn ranking_precedence() {
151        let test = AcceptLanguage(vec![]);
152        assert!(test.ranked().is_empty());
153
154        let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
155        assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]);
156
157        let test = AcceptLanguage(vec![
158            QualityItem::new("fr".parse().unwrap(), q(0.900)),
159            QualityItem::new("fr-CH".parse().unwrap(), q(1.0)),
160            QualityItem::new("en".parse().unwrap(), q(0.800)),
161            QualityItem::new("*".parse().unwrap(), q(0.500)),
162            QualityItem::new("de".parse().unwrap(), q(0.700)),
163        ]);
164        assert_eq!(
165            test.ranked(),
166            vec![
167                "fr-CH".parse().unwrap(),
168                "fr".parse().unwrap(),
169                "en".parse().unwrap(),
170                "de".parse().unwrap(),
171                "*".parse().unwrap(),
172            ]
173        );
174
175        let test = AcceptLanguage(vec![
176            QualityItem::max("fr".parse().unwrap()),
177            QualityItem::max("fr-CH".parse().unwrap()),
178            QualityItem::max("en".parse().unwrap()),
179            QualityItem::max("*".parse().unwrap()),
180            QualityItem::max("de".parse().unwrap()),
181        ]);
182        assert_eq!(
183            test.ranked(),
184            vec![
185                "fr".parse().unwrap(),
186                "fr-CH".parse().unwrap(),
187                "en".parse().unwrap(),
188                "*".parse().unwrap(),
189                "de".parse().unwrap(),
190            ]
191        );
192    }
193
194    #[test]
195    fn preference_selection() {
196        let test = AcceptLanguage(vec![
197            QualityItem::new("fr".parse().unwrap(), q(0.900)),
198            QualityItem::new("fr-CH".parse().unwrap(), q(1.0)),
199            QualityItem::new("en".parse().unwrap(), q(0.800)),
200            QualityItem::new("*".parse().unwrap(), q(0.500)),
201            QualityItem::new("de".parse().unwrap(), q(0.700)),
202        ]);
203        assert_eq!(
204            test.preference(),
205            Preference::Specific("fr-CH".parse().unwrap())
206        );
207
208        let test = AcceptLanguage(vec![
209            QualityItem::max("fr".parse().unwrap()),
210            QualityItem::max("fr-CH".parse().unwrap()),
211            QualityItem::max("en".parse().unwrap()),
212            QualityItem::max("*".parse().unwrap()),
213            QualityItem::max("de".parse().unwrap()),
214        ]);
215        assert_eq!(
216            test.preference(),
217            Preference::Specific("fr".parse().unwrap())
218        );
219
220        let test = AcceptLanguage(vec![]);
221        assert_eq!(test.preference(), Preference::Any);
222    }
223}