use std::str::FromStr;
use std::sync::{OnceLock, RwLock};
use unic_langid::LanguageIdentifier;
use super::I18nError;
static GLOBAL_LOCALE: OnceLock<LanguageIdentifier> = OnceLock::new();
static OVERRIDE_LOCALE: RwLock<Option<LanguageIdentifier>> = RwLock::new(None);
fn normalize(raw: &str) -> Option<LanguageIdentifier> {
let s = raw
.split('@')
.next()?
.trim()
.split('.')
.next()?
.replace('_', "-");
if matches!(s.as_str(), "C" | "POSIX") {
return None;
}
let langid = LanguageIdentifier::from_str(&s).ok()?;
match langid.language.as_str() {
"zh" => Some(LanguageIdentifier::from_str("zh").expect("'zh' is a valid langid")),
"en" => Some(LanguageIdentifier::from_str("en").expect("'en' is a valid langid")),
_ => None,
}
}
fn detect_from(
get_env: &dyn Fn(&str) -> Option<String>,
get_sys: &dyn Fn() -> Option<String>,
) -> LanguageIdentifier {
if let Some(lang) = get_env("LIMITERON_LANG")
&& !lang.trim().is_empty()
&& let Some(l) = normalize(&lang)
{
return l;
}
for key in ["LC_ALL", "LC_MESSAGES", "LANG"] {
if let Some(lang) = get_env(key)
&& !lang.trim().is_empty()
&& let Some(l) = normalize(&lang)
{
return l;
}
}
if let Some(sys) = get_sys()
&& let Some(l) = normalize(&sys)
{
return l;
}
LanguageIdentifier::from_str("en").expect("'en' is a valid language identifier")
}
fn detect_locale() -> LanguageIdentifier {
detect_from(&|key| std::env::var(key).ok(), &sys_locale::get_locale)
}
pub fn init() {
let _ = current_locale();
}
pub fn current_locale() -> LanguageIdentifier {
if let Ok(guard) = OVERRIDE_LOCALE.read()
&& let Some(locale) = guard.as_ref()
{
return locale.clone();
}
GLOBAL_LOCALE.get_or_init(detect_locale).clone()
}
pub fn set_locale(locale: &str) -> Result<(), I18nError> {
let parsed = LanguageIdentifier::from_str(locale).map_err(|e| I18nError::InvalidLocale {
input: locale.to_string(),
reason: e.to_string(),
})?;
*OVERRIDE_LOCALE.write().unwrap_or_else(|e| e.into_inner()) = Some(parsed);
Ok(())
}
pub fn clear_locale_override() {
*OVERRIDE_LOCALE.write().unwrap_or_else(|e| e.into_inner()) = None;
}
pub fn detected_locale() -> LanguageIdentifier {
GLOBAL_LOCALE.get_or_init(detect_locale).clone()
}
#[cfg(test)]
mod tests {
use super::*;
fn env_of<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |key| {
pairs
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| v.to_string())
}
}
fn chain(pairs: &[(&str, &str)], sys: Option<&str>) -> String {
detect_from(&env_of(pairs), &|| sys.map(str::to_string))
.language
.as_str()
.to_string()
}
#[test]
fn test_detect_chain_zh_variants() {
assert_eq!(chain(&[("LC_ALL", "zh_CN.UTF-8")], None), "zh");
assert_eq!(chain(&[("LC_MESSAGES", "zh_TW")], None), "zh");
assert_eq!(chain(&[("LANG", "zh_SG.UTF-8")], None), "zh");
assert_eq!(chain(&[], Some("zh-Hans-CN")), "zh");
}
#[test]
fn test_detect_chain_en_and_unsupported() {
assert_eq!(chain(&[("LC_ALL", "en_US.UTF-8")], None), "en");
assert_eq!(chain(&[("LANG", "fr_FR.UTF-8")], None), "en");
assert_eq!(chain(&[("LC_ALL", "ja_JP.UTF-8")], None), "en");
assert_eq!(chain(&[("LC_ALL", "C")], None), "en");
assert_eq!(chain(&[("LC_ALL", "POSIX")], None), "en");
assert_eq!(chain(&[], None), "en");
assert_eq!(chain(&[("LC_ALL", "")], Some("garbage@@##")), "en");
}
#[test]
fn test_detect_chain_priority() {
assert_eq!(
chain(&[("LIMITERON_LANG", "zh_CN"), ("LC_ALL", "en_US")], None),
"zh"
);
assert_eq!(
chain(&[("LIMITERON_LANG", "fr_FR"), ("LC_ALL", "zh_CN")], None),
"zh"
);
assert_eq!(
chain(&[("LIMITERON_LANG", " "), ("LC_ALL", "en_US")], None),
"en"
);
assert_eq!(
chain(
&[
("LC_ALL", "en_US"),
("LC_MESSAGES", "zh_CN"),
("LANG", "zh_TW")
],
None
),
"en"
);
}
#[test]
fn test_normalize_rejects_and_collapses() {
assert_eq!(normalize("zh_CN.UTF-8").unwrap().language.as_str(), "zh");
assert_eq!(normalize("en-US@euro").unwrap().language.as_str(), "en");
assert!(normalize("C").is_none());
assert!(normalize("POSIX").is_none());
assert!(normalize("").is_none());
assert!(normalize("not a locale!!").is_none());
assert!(
normalize("fr_FR").is_none(),
"unsupported → chain continues"
);
}
#[test]
fn test_set_locale_override_and_clear() {
clear_locale_override();
set_locale("zh-CN").expect("zh-CN is valid");
assert_eq!(current_locale().language.as_str(), "zh");
assert!(set_locale("not-a-valid-locale!!!").is_err());
clear_locale_override();
clear_locale_override(); }
}