1fn parse_quality(param: &str) -> Option<u16> {
2 let (_, value) = param.split_once('=')?;
3 let value = value.trim();
4
5 if value == "1" {
6 return Some(1000);
7 }
8
9 if value == "0" {
10 return Some(0);
11 }
12
13 if let Some(fraction) = value.strip_prefix("1.") {
14 if fraction.chars().all(|ch| ch == '0') {
15 return Some(1000);
16 }
17
18 return None;
19 }
20
21 let fraction = value.strip_prefix("0.")?;
22 if fraction.is_empty() || !fraction.chars().all(|ch| ch.is_ascii_digit()) {
23 return None;
24 }
25
26 let mut padded = fraction.to_string();
27 padded.truncate(3);
28 while padded.len() < 3 {
29 padded.push('0');
30 }
31
32 padded.parse().ok()
33}
34
35fn parse_weighted_language(token: &str) -> Option<(String, u16)> {
36 let mut parts = token.split(';');
37 let language = parts.next()?.trim();
38 if language.is_empty() {
39 return None;
40 }
41
42 let mut quality = 1000;
43 for part in parts {
44 let part = part.trim();
45 if part.starts_with("q=") {
46 quality = parse_quality(part)?;
47 break;
48 }
49 }
50
51 Some((language.to_ascii_lowercase(), quality))
52}
53
54fn primary_subtag(locale: &str) -> &str {
55 locale.split(['-', '_']).next().unwrap_or(locale)
56}
57
58fn match_score(requested: &str, supported: &str) -> u8 {
59 let supported = supported.to_ascii_lowercase();
60
61 if requested == supported {
62 return 2;
63 }
64
65 if primary_subtag(requested) == primary_subtag(&supported) {
66 return 1;
67 }
68
69 0
70}
71
72fn resolve_accept_language_impl<'a>(
73 header: &str,
74 supported_locales: &[&'a str],
75 default_locale: &'a str,
76) -> &'a str {
77 let mut best_match: Option<(&'a str, u16, u8, usize)> = None;
78
79 for (order, token) in header.split(',').enumerate() {
80 let Some((language, quality)) = parse_weighted_language(token) else {
81 continue;
82 };
83
84 for &supported in supported_locales {
85 let score = match_score(&language, supported);
86 if score == 0 {
87 continue;
88 }
89
90 let is_better = match best_match {
91 None => true,
92 Some((_, best_quality, best_score, best_order)) => {
93 quality > best_quality
94 || (quality == best_quality
95 && (score > best_score || (score == best_score && order < best_order)))
96 }
97 };
98
99 if is_better {
100 best_match = Some((supported, quality, score, order));
101 }
102 }
103 }
104
105 best_match
106 .map(|(locale, _, _, _)| locale)
107 .unwrap_or(default_locale)
108}
109
110pub fn resolve_accept_language<'a>(
133 header: &str,
134 supported_locales: &[&'a str],
135 default_locale: &'a str,
136) -> &'a str {
137 resolve_accept_language_impl(header, supported_locales, default_locale)
138}
139
140pub fn resolve_accept_language_owned<S>(
161 header: &str,
162 supported_locales: &[S],
163 default_locale: &str,
164) -> String
165where
166 S: AsRef<str>,
167{
168 let supported_refs: Vec<&str> = supported_locales.iter().map(AsRef::as_ref).collect();
169 resolve_accept_language_impl(header, &supported_refs, default_locale).to_string()
170}