use crate::error::Result;
use crate::http::{Request, Response};
use crate::router::Next;
const ACCEPT_LANGUAGE_HEADER: &str = "accept-language";
pub const DEFAULT_LOCALE: &str = "en";
const MAX_TAG_LEN: usize = 16;
#[derive(Debug, Clone)]
pub struct Locale(pub String);
impl Locale {
pub fn as_str(&self) -> &str {
&self.0
}
}
pub async fn locale(mut req: Request, next: Next) -> Result<Response> {
let tag = parse_accept_language(req.header(ACCEPT_LANGUAGE_HEADER))
.unwrap_or_else(|| DEFAULT_LOCALE.to_string());
req.ctx_mut().insert(Locale(tag));
next.run(req).await
}
pub fn parse_accept_language(header: Option<&str>) -> Option<String> {
let raw = header?.trim();
if raw.is_empty() {
return None;
}
for segment in raw.split(',') {
let tag = segment.split(';').next().unwrap_or(segment).trim();
if is_valid_tag(tag) {
return Some(tag.to_string());
}
}
None
}
fn is_valid_tag(tag: &str) -> bool {
if tag.is_empty() || tag.len() > MAX_TAG_LEN {
return false;
}
tag.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_header_returns_none() {
assert!(parse_accept_language(None).is_none());
}
#[test]
fn empty_header_returns_none() {
assert!(parse_accept_language(Some("")).is_none());
assert!(parse_accept_language(Some(" ")).is_none());
}
#[test]
fn single_tag_returned_as_is() {
assert_eq!(parse_accept_language(Some("en")), Some("en".to_string()));
assert_eq!(
parse_accept_language(Some("en-US")),
Some("en-US".to_string())
);
}
#[test]
fn first_tag_wins_in_comma_separated_list() {
let header = "en-US,en;q=0.9,fr;q=0.8,*;q=0.5";
assert_eq!(
parse_accept_language(Some(header)),
Some("en-US".to_string())
);
}
#[test]
fn q_value_is_stripped_from_the_tag() {
assert_eq!(
parse_accept_language(Some("ar;q=0.9")),
Some("ar".to_string())
);
}
#[test]
fn invalid_first_tag_falls_through_to_next() {
let header = "*,en\nbad,de;q=0.8";
assert_eq!(parse_accept_language(Some(header)), Some("de".to_string()));
}
#[test]
fn oversized_tag_is_rejected() {
let evil = "a".repeat(50);
assert!(parse_accept_language(Some(&evil)).is_none());
}
#[test]
fn header_with_only_invalid_tags_returns_none() {
assert!(parse_accept_language(Some("*,?,!!!")).is_none());
assert!(parse_accept_language(Some(",,,")).is_none());
}
#[test]
fn whitespace_between_tags_is_tolerated() {
assert_eq!(
parse_accept_language(Some("en-US, en;q=0.9, fr;q=0.8")),
Some("en-US".to_string())
);
}
#[test]
fn arabic_and_cjk_tags_are_accepted() {
assert_eq!(
parse_accept_language(Some("ar-EG,ar;q=0.9,en;q=0.6")),
Some("ar-EG".to_string())
);
assert_eq!(
parse_accept_language(Some("zh-Hans-CN,zh;q=0.9")),
Some("zh-Hans-CN".to_string())
);
}
#[test]
fn locale_as_str_round_trips() {
let l = Locale("en-US".to_string());
assert_eq!(l.as_str(), "en-US");
}
}