// ABOUTME: Locale detection and string lookup for the power menu UI. // ABOUTME: Reads system locale (LANG or /etc/locale.conf) and provides DE or EN strings. use std::env; use std::fs; use std::path::Path; const DEFAULT_LOCALE_CONF: &str = "/etc/locale.conf"; /// All user-visible strings for the power menu UI. #[derive(Debug, Clone)] pub struct Strings { // Button labels pub lock_label: &'static str, pub logout_label: &'static str, pub hibernate_label: &'static str, pub reboot_label: &'static str, pub shutdown_label: &'static str, // Confirmation prompts pub logout_confirm: &'static str, pub hibernate_confirm: &'static str, pub reboot_confirm: &'static str, pub shutdown_confirm: &'static str, // Confirmation buttons pub confirm_yes: &'static str, pub confirm_no: &'static str, // Error messages pub lock_failed: &'static str, pub logout_failed: &'static str, pub hibernate_failed: &'static str, pub reboot_failed: &'static str, pub shutdown_failed: &'static str, } const STRINGS_DE: Strings = Strings { lock_label: "Sperren", logout_label: "Abmelden", hibernate_label: "Ruhezustand", reboot_label: "Neustart", shutdown_label: "Herunterfahren", logout_confirm: "Wirklich abmelden?", hibernate_confirm: "Wirklich in den Ruhezustand?", reboot_confirm: "Wirklich neu starten?", shutdown_confirm: "Wirklich herunterfahren?", confirm_yes: "Ja", confirm_no: "Abbrechen", lock_failed: "Sperren fehlgeschlagen", logout_failed: "Abmelden fehlgeschlagen", hibernate_failed: "Ruhezustand fehlgeschlagen", reboot_failed: "Neustart fehlgeschlagen", shutdown_failed: "Herunterfahren fehlgeschlagen", }; const STRINGS_EN: Strings = Strings { lock_label: "Lock", logout_label: "Log out", hibernate_label: "Hibernate", reboot_label: "Reboot", shutdown_label: "Shut down", logout_confirm: "Really log out?", hibernate_confirm: "Really hibernate?", reboot_confirm: "Really reboot?", shutdown_confirm: "Really shut down?", confirm_yes: "Yes", confirm_no: "Cancel", lock_failed: "Lock failed", logout_failed: "Log out failed", hibernate_failed: "Hibernate failed", reboot_failed: "Reboot failed", shutdown_failed: "Shutdown failed", }; /// Extract the language prefix from a LANG value like "de_DE.UTF-8" → "de". /// Returns "en" for empty, "C", or "POSIX" values. fn parse_lang_prefix(lang: &str) -> String { if lang.is_empty() || lang == "C" || lang == "POSIX" { return "en".to_string(); } let prefix = lang .split('_') .next() .unwrap_or(lang) .split('.') .next() .unwrap_or(lang) .to_lowercase(); if prefix.chars().all(|c| c.is_ascii_alphabetic()) && !prefix.is_empty() { prefix } else { "en".to_string() } } /// Read the LANG= value from a locale.conf file. fn read_lang_from_conf(path: &Path) -> Option { let content = fs::read_to_string(path).ok()?; for line in content.lines() { if let Some(value) = line.strip_prefix("LANG=") { let value = value.trim(); if !value.is_empty() { return Some(value.to_string()); } } } None } /// Determine the system language from LANG env var or /etc/locale.conf. pub fn detect_locale() -> String { let lang = env::var("LANG") .ok() .filter(|s| !s.is_empty()) .or_else(|| read_lang_from_conf(Path::new(DEFAULT_LOCALE_CONF))); match lang { Some(l) => parse_lang_prefix(&l), None => "en".to_string(), } } /// Return the string table for the given locale, defaulting to English. pub fn load_strings(locale: Option<&str>) -> &'static Strings { let locale = match locale { Some(l) => l.to_string(), None => detect_locale(), }; match locale.as_str() { "de" => &STRINGS_DE, _ => &STRINGS_EN, } } #[cfg(test)] mod tests { use super::*; use std::io::Write; // -- parse_lang_prefix tests (no env manipulation needed) -- #[test] fn parse_german_locale() { assert_eq!(parse_lang_prefix("de_DE.UTF-8"), "de"); } #[test] fn parse_english_locale() { assert_eq!(parse_lang_prefix("en_US.UTF-8"), "en"); } #[test] fn parse_c_falls_back_to_english() { assert_eq!(parse_lang_prefix("C"), "en"); } #[test] fn parse_posix_falls_back_to_english() { assert_eq!(parse_lang_prefix("POSIX"), "en"); } #[test] fn parse_empty_falls_back_to_english() { assert_eq!(parse_lang_prefix(""), "en"); } #[test] fn parse_unsupported_returns_prefix() { assert_eq!(parse_lang_prefix("fr_FR.UTF-8"), "fr"); } #[test] fn parse_bare_language_code() { assert_eq!(parse_lang_prefix("de"), "de"); } // -- read_lang_from_conf tests -- #[test] fn read_conf_extracts_lang() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("locale.conf"); let mut f = fs::File::create(&conf).unwrap(); writeln!(f, "LANG=de_DE.UTF-8").unwrap(); assert_eq!(read_lang_from_conf(&conf), Some("de_DE.UTF-8".to_string())); } #[test] fn read_conf_returns_none_for_missing_file() { assert_eq!(read_lang_from_conf(Path::new("/nonexistent/locale.conf")), None); } #[test] fn read_conf_returns_none_for_empty_lang() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("locale.conf"); let mut f = fs::File::create(&conf).unwrap(); writeln!(f, "LANG=").unwrap(); assert_eq!(read_lang_from_conf(&conf), None); } #[test] fn read_conf_skips_non_lang_lines() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("locale.conf"); let mut f = fs::File::create(&conf).unwrap(); writeln!(f, "LC_ALL=en_US.UTF-8").unwrap(); writeln!(f, "LANG=de_DE.UTF-8").unwrap(); assert_eq!(read_lang_from_conf(&conf), Some("de_DE.UTF-8".to_string())); } // -- load_strings tests -- #[test] fn load_strings_german() { let strings = load_strings(Some("de")); assert_eq!(strings.lock_label, "Sperren"); assert_eq!(strings.confirm_yes, "Ja"); assert_eq!(strings.confirm_no, "Abbrechen"); } #[test] fn load_strings_english() { let strings = load_strings(Some("en")); assert_eq!(strings.lock_label, "Lock"); assert_eq!(strings.confirm_yes, "Yes"); } #[test] fn load_strings_unknown_falls_back_to_english() { let strings = load_strings(Some("fr")); assert_eq!(strings.lock_label, "Lock"); } #[test] fn all_string_fields_nonempty() { for locale in &["de", "en"] { let s = load_strings(Some(locale)); assert!(!s.lock_label.is_empty(), "{locale}: lock_label empty"); assert!(!s.logout_label.is_empty(), "{locale}: logout_label empty"); assert!(!s.hibernate_label.is_empty(), "{locale}: hibernate_label empty"); assert!(!s.reboot_label.is_empty(), "{locale}: reboot_label empty"); assert!(!s.shutdown_label.is_empty(), "{locale}: shutdown_label empty"); assert!(!s.logout_confirm.is_empty(), "{locale}: logout_confirm empty"); assert!(!s.hibernate_confirm.is_empty(), "{locale}: hibernate_confirm empty"); assert!(!s.reboot_confirm.is_empty(), "{locale}: reboot_confirm empty"); assert!(!s.shutdown_confirm.is_empty(), "{locale}: shutdown_confirm empty"); assert!(!s.confirm_yes.is_empty(), "{locale}: confirm_yes empty"); assert!(!s.confirm_no.is_empty(), "{locale}: confirm_no empty"); assert!(!s.lock_failed.is_empty(), "{locale}: lock_failed empty"); assert!(!s.logout_failed.is_empty(), "{locale}: logout_failed empty"); assert!(!s.hibernate_failed.is_empty(), "{locale}: hibernate_failed empty"); assert!(!s.reboot_failed.is_empty(), "{locale}: reboot_failed empty"); assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed empty"); } } #[test] fn error_messages_contain_failed() { let s = load_strings(Some("en")); assert!(s.lock_failed.to_lowercase().contains("failed")); assert!(s.logout_failed.to_lowercase().contains("failed")); assert!(s.hibernate_failed.to_lowercase().contains("failed")); assert!(s.reboot_failed.to_lowercase().contains("failed")); assert!(s.shutdown_failed.to_lowercase().contains("failed")); } }