Skip to main content

orinium_browser/platform/
locale.rs

1//! System locale discovery shared by HTTP and browser-facing JavaScript APIs.
2
3const FALLBACK_LANGUAGE: &str = "en-US";
4
5/// Returns the user's primary language as a browser-compatible BCP 47 tag.
6pub fn preferred_language() -> String {
7    system_language()
8        .and_then(|language| normalize_language_tag(&language))
9        .unwrap_or_else(|| FALLBACK_LANGUAGE.to_string())
10}
11
12/// Builds a conservative `Accept-Language` value from the primary language.
13pub fn accept_language_header() -> String {
14    let primary = preferred_language();
15    let base = primary.split('-').next().unwrap_or(&primary);
16    if base.eq_ignore_ascii_case(&primary) {
17        primary
18    } else {
19        format!("{primary},{base};q=0.9,en;q=0.8")
20    }
21}
22
23fn normalize_language_tag(language: &str) -> Option<String> {
24    let language = language
25        .split(['.', '@'])
26        .next()
27        .unwrap_or(language)
28        .replace('_', "-");
29    if language.is_empty()
30        || language.eq_ignore_ascii_case("c")
31        || language.eq_ignore_ascii_case("posix")
32        || !language
33            .chars()
34            .all(|character| character.is_ascii_alphanumeric() || character == '-')
35    {
36        return None;
37    }
38    Some(language)
39}
40
41#[cfg(target_os = "windows")]
42fn system_language() -> Option<String> {
43    const LOCALE_NAME_MAX_LENGTH: usize = 85;
44    #[link(name = "kernel32")]
45    unsafe extern "system" {
46        fn GetUserDefaultLocaleName(locale_name: *mut u16, locale_name_count: i32) -> i32;
47    }
48
49    let mut buffer = [0_u16; LOCALE_NAME_MAX_LENGTH];
50    // SAFETY: `buffer` is writable for the reported element count and remains
51    // alive until the returned UTF-16 slice has been copied into a Rust String.
52    let length =
53        unsafe { GetUserDefaultLocaleName(buffer.as_mut_ptr(), LOCALE_NAME_MAX_LENGTH as i32) };
54    (length > 1).then(|| String::from_utf16_lossy(&buffer[..length as usize - 1]))
55}
56
57#[cfg(not(target_os = "windows"))]
58fn system_language() -> Option<String> {
59    ["LC_ALL", "LC_MESSAGES", "LANG"]
60        .into_iter()
61        .find_map(|name| std::env::var(name).ok().filter(|value| !value.is_empty()))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn normalizes_common_platform_locale_forms() {
70        assert_eq!(normalize_language_tag("ja_JP.UTF-8"), Some("ja-JP".into()));
71        assert_eq!(
72            normalize_language_tag("en_US@calendar"),
73            Some("en-US".into())
74        );
75        assert_eq!(normalize_language_tag("C"), None);
76    }
77}