Skip to main content

rust_expect/auto_config/
locale.rs

1//! Locale detection and configuration.
2
3use std::collections::HashMap;
4
5/// Locale information.
6#[derive(Debug, Clone, Default)]
7pub struct LocaleInfo {
8    /// Language code (e.g., "en").
9    pub language: Option<String>,
10    /// Territory/country (e.g., "US").
11    pub territory: Option<String>,
12    /// Codeset (e.g., "UTF-8").
13    pub codeset: Option<String>,
14    /// Modifier (e.g., "euro").
15    pub modifier: Option<String>,
16}
17
18impl LocaleInfo {
19    /// Parse a locale string (e.g., "en_US.UTF-8").
20    #[must_use]
21    pub fn parse(locale: &str) -> Self {
22        let mut info = Self::default();
23
24        // Handle empty or "C"/"POSIX" locale
25        if locale.is_empty() || locale == "C" || locale == "POSIX" {
26            info.language = Some("C".to_string());
27            return info;
28        }
29
30        let mut remaining = locale;
31
32        // Extract modifier (@modifier)
33        if let Some(at_pos) = remaining.rfind('@') {
34            info.modifier = Some(remaining[at_pos + 1..].to_string());
35            remaining = &remaining[..at_pos];
36        }
37
38        // Extract codeset (.codeset)
39        if let Some(dot_pos) = remaining.rfind('.') {
40            info.codeset = Some(remaining[dot_pos + 1..].to_string());
41            remaining = &remaining[..dot_pos];
42        }
43
44        // Extract territory (_territory)
45        if let Some(under_pos) = remaining.rfind('_') {
46            info.territory = Some(remaining[under_pos + 1..].to_string());
47            remaining = &remaining[..under_pos];
48        }
49
50        // Remaining is the language
51        if !remaining.is_empty() {
52            info.language = Some(remaining.to_string());
53        }
54
55        info
56    }
57
58    /// Check if this is a UTF-8 locale.
59    #[must_use]
60    pub fn is_utf8(&self) -> bool {
61        self.codeset.as_ref().is_some_and(|c| {
62            let c = c.to_lowercase().replace('-', "");
63            c == "utf8"
64        })
65    }
66}
67
68impl std::fmt::Display for LocaleInfo {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        if let Some(ref lang) = self.language {
71            write!(f, "{lang}")?;
72        }
73        if let Some(ref territory) = self.territory {
74            write!(f, "_{territory}")?;
75        }
76        if let Some(ref codeset) = self.codeset {
77            write!(f, ".{codeset}")?;
78        }
79        if let Some(ref modifier) = self.modifier {
80            write!(f, "@{modifier}")?;
81        }
82        Ok(())
83    }
84}
85
86/// Detect current locale from environment.
87#[must_use]
88pub fn detect_locale() -> LocaleInfo {
89    // Check LC_ALL first, then LANG
90    let locale = std::env::var("LC_ALL")
91        .or_else(|_| std::env::var("LANG"))
92        .unwrap_or_default();
93
94    LocaleInfo::parse(&locale)
95}
96
97/// Get all locale-related environment variables.
98#[must_use]
99pub fn locale_env() -> HashMap<String, String> {
100    let vars = [
101        "LANG",
102        "LC_ALL",
103        "LC_CTYPE",
104        "LC_NUMERIC",
105        "LC_TIME",
106        "LC_COLLATE",
107        "LC_MONETARY",
108        "LC_MESSAGES",
109        "LC_PAPER",
110        "LC_NAME",
111        "LC_ADDRESS",
112        "LC_TELEPHONE",
113        "LC_MEASUREMENT",
114        "LC_IDENTIFICATION",
115    ];
116
117    let mut result = HashMap::new();
118    for var in vars {
119        if let Ok(value) = std::env::var(var) {
120            result.insert(var.to_string(), value);
121        }
122    }
123    result
124}
125
126/// Check if the environment supports UTF-8.
127#[must_use]
128pub fn is_utf8_environment() -> bool {
129    detect_locale().is_utf8()
130}
131
132/// Recommended environment for UTF-8 support.
133#[must_use]
134pub fn utf8_environment() -> HashMap<String, String> {
135    let mut env = HashMap::new();
136    env.insert("LANG".to_string(), "en_US.UTF-8".to_string());
137    env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
138    env
139}
140
141/// Force UTF-8 locale environment.
142#[must_use]
143pub fn force_utf8_env() -> HashMap<String, String> {
144    let mut env = locale_env();
145    env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
146    env
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn parse_full_locale() {
155        let info = LocaleInfo::parse("en_US.UTF-8");
156        assert_eq!(info.language, Some("en".to_string()));
157        assert_eq!(info.territory, Some("US".to_string()));
158        assert_eq!(info.codeset, Some("UTF-8".to_string()));
159        assert!(info.is_utf8());
160    }
161
162    #[test]
163    fn parse_locale_with_modifier() {
164        let info = LocaleInfo::parse("de_DE.UTF-8@euro");
165        assert_eq!(info.language, Some("de".to_string()));
166        assert_eq!(info.territory, Some("DE".to_string()));
167        assert_eq!(info.modifier, Some("euro".to_string()));
168    }
169
170    #[test]
171    fn parse_c_locale() {
172        let info = LocaleInfo::parse("C");
173        assert_eq!(info.language, Some("C".to_string()));
174        assert!(!info.is_utf8());
175    }
176
177    #[test]
178    fn locale_to_string() {
179        let info = LocaleInfo {
180            language: Some("en".to_string()),
181            territory: Some("US".to_string()),
182            codeset: Some("UTF-8".to_string()),
183            modifier: None,
184        };
185        assert_eq!(info.to_string(), "en_US.UTF-8");
186    }
187}