use std::cmp::Ordering;
use axum::extract::Request;
use axum::middleware::Next;
use axum::response::Response;
tokio::task_local! {
static CURRENT_LOCALE: String;
}
#[must_use]
pub fn current_locale() -> String {
CURRENT_LOCALE
.try_with(std::clone::Clone::clone)
.unwrap_or_else(|_| "en".to_string())
}
pub fn detect_locale(req: &Request) -> String {
if let Some(lang) = req.uri().query().and_then(|q| {
q.split('&')
.filter_map(|pair| pair.split_once('='))
.find(|(k, _)| *k == "lang")
.map(|(_, v)| v.to_string())
}) {
let lang = lang.to_lowercase();
if ["zh-cn", "zh-tw", "zh", "en", "ja", "ko"].contains(&lang.as_str()) {
return normalize_locale(&lang);
}
}
req.headers()
.get("accept-language")
.and_then(|v| v.to_str().ok())
.and_then(parse_accept_language)
.unwrap_or_else(|| "en".to_string())
}
fn parse_accept_language(header: &str) -> Option<String> {
header
.split(',')
.filter_map(|part| {
let (lang, quality) = if let Some((l, q)) = part.trim().split_once(';') {
let quality = q
.trim()
.strip_prefix("q=")
.and_then(|v| v.parse::<f32>().ok())
.unwrap_or(1.0);
(l.trim().to_lowercase(), quality)
} else {
(part.trim().to_lowercase(), 1.0)
};
if lang.is_empty() {
None
} else {
Some((normalize_locale(&lang), quality))
}
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal))
.map(|(lang, _)| lang)
}
fn normalize_locale(lang: &str) -> String {
match lang {
"zh" | "zh-cn" | "zh-hans" => "zh-CN".to_string(),
"zh-tw" | "zh-hant" => "zh-TW".to_string(),
"en" | "en-us" | "en-gb" => "en".to_string(),
"ja" => "ja".to_string(),
"ko" => "ko".to_string(),
other => other.to_string(),
}
}
pub async fn locale_middleware(req: Request, next: Next) -> Response {
let locale = detect_locale(&req);
rust_i18n::set_locale(&locale);
CURRENT_LOCALE.scope(locale, next.run(req)).await
}