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}