pub(crate) fn system_accept_language_list() -> Option<String> {
let languages = preferred_languages();
let list = normalize_language_list(languages.iter().map(String::as_str));
(!list.is_empty()).then(|| list.join(","))
}
fn normalize_language_list<'a>(languages: impl Iterator<Item = &'a str>) -> Vec<String> {
let mut normalized: Vec<String> = Vec::new();
for language in languages {
let Some(tag) = normalize_language(language) else {
continue;
};
if !normalized.iter().any(|existing| existing == &tag) {
normalized.push(tag);
}
}
normalized
}
fn normalize_language(language: &str) -> Option<String> {
let tag = language
.split(['.', '@'])
.next()
.unwrap_or_default()
.trim()
.replace('_', "-");
if tag.is_empty() || tag.eq_ignore_ascii_case("C") || tag.eq_ignore_ascii_case("POSIX") {
return None;
}
let mut parts = tag.split('-');
let language = parts.next()?;
if language.len() < 2 || language.len() > 8 || !language.chars().all(|c| c.is_ascii_alphabetic())
{
return None;
}
if !parts.all(|part| {
!part.is_empty() && part.len() <= 8 && part.chars().all(|c| c.is_ascii_alphanumeric())
}) {
return None;
}
Some(tag)
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn preferred_languages() -> Vec<String> {
if let Some(languages) = std::env::var_os("LANGUAGE")
.map(|value| value.to_string_lossy().into_owned())
.filter(|value| !value.is_empty())
{
let languages: Vec<String> = languages.split(':').map(ToString::to_string).collect();
if !normalize_language_list(languages.iter().map(String::as_str)).is_empty() {
return languages;
}
}
for variable in ["LC_ALL", "LC_MESSAGES", "LANG"] {
if let Some(value) = std::env::var_os(variable)
.map(|value| value.to_string_lossy().into_owned())
.filter(|value| !value.is_empty())
{
return vec![value];
}
}
Vec::new()
}
#[cfg(target_os = "macos")]
fn preferred_languages() -> Vec<String> {
objc2_foundation::NSLocale::preferredLanguages()
.iter()
.map(|language| language.to_string())
.collect()
}
#[cfg(windows)]
fn preferred_languages() -> Vec<String> {
use windows::Win32::Globalization::GetUserDefaultLocaleName;
let mut buffer = [0u16; 85];
let written = unsafe { GetUserDefaultLocaleName(&mut buffer) };
if written <= 0 {
return Vec::new();
}
let name = String::from_utf16_lossy(&buffer[..(written as usize).saturating_sub(1)]);
if name.is_empty() {
Vec::new()
} else {
vec![name]
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "macos",
windows
)))]
fn preferred_languages() -> Vec<String> {
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
fn normalize(languages: &[&str]) -> Vec<String> {
normalize_language_list(languages.iter().copied())
}
#[test]
fn posix_locale_names_become_language_tags() {
assert_eq!(normalize(&["pt_BR.UTF-8"]), ["pt-BR"]);
assert_eq!(normalize(&["de_DE@euro"]), ["de-DE"]);
assert_eq!(normalize(&["fr_CA.ISO-8859-1@currency"]), ["fr-CA"]);
}
#[test]
fn tags_that_are_already_bcp47_are_left_alone() {
assert_eq!(
normalize(&["pt-BR", "zh-Hans-CN", "en"]),
["pt-BR", "zh-Hans-CN", "en"]
);
}
#[test]
fn placeholder_locales_are_dropped() {
assert!(normalize(&["C"]).is_empty());
assert!(normalize(&["POSIX"]).is_empty());
assert!(normalize(&["C.UTF-8"]).is_empty());
assert_eq!(normalize(&["C", "pt_BR"]), ["pt-BR"]);
}
#[test]
fn preference_order_is_kept_and_duplicates_dropped() {
assert_eq!(
normalize(&["pt_BR.UTF-8", "pt-BR", "en_US.UTF-8"]),
["pt-BR", "en-US"]
);
}
#[test]
fn malformed_tags_never_reach_the_accept_language_header() {
assert!(normalize(&[""]).is_empty());
assert!(normalize(&[" "]).is_empty());
assert!(
normalize(&["e"]).is_empty(),
"a one-letter language is not one"
);
assert!(
normalize(&["en-"]).is_empty(),
"a trailing separator is malformed"
);
assert!(normalize(&["en-US-"]).is_empty());
assert!(normalize(&["1234"]).is_empty());
assert!(
normalize(&["en US"]).is_empty(),
"a space is not a separator"
);
}
#[test]
fn nothing_reported_produces_no_list() {
assert!(normalize(&[]).is_empty());
assert!(normalize(&["C", "POSIX"]).is_empty());
}
}