use std::sync::OnceLock;
const ENV_VARS: [&str; 6] = [
"JSCD_LANG",
"LC_ALL",
"LC_MESSAGES",
"LANGUAGE",
"LANG",
"LC_CTYPE",
];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Lang {
En,
Zh,
}
impl Lang {
fn from_value(v: &str) -> Option<Lang> {
let v = v.trim().to_ascii_lowercase();
if v.is_empty() {
return None;
}
if v.starts_with("zh") {
return Some(Lang::Zh);
}
if v.starts_with("c") || v.starts_with("posix") {
return None;
}
Some(Lang::En)
}
fn from_env(mut get: impl FnMut(&str) -> Option<String>) -> Option<Lang> {
ENV_VARS
.iter()
.find_map(|var| get(var).and_then(|v| Lang::from_value(&v)))
}
fn detect() -> Lang {
Lang::from_env(|k| std::env::var(k).ok())
.or_else(|| os_locale().and_then(|v| Lang::from_value(&v)))
.unwrap_or(Lang::En)
}
}
#[cfg(windows)]
fn os_locale() -> Option<String> {
use std::panic::{catch_unwind, AssertUnwindSafe};
use windows_sys::Win32::Globalization::GetUserDefaultLocaleName;
catch_unwind(AssertUnwindSafe(|| -> Option<String> {
let mut buf = [0u16; 85]; let n = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) };
let n = usize::try_from(n).ok().filter(|n| *n > 1)?;
let n = n.min(buf.len());
String::from_utf16(&buf[..n - 1]).ok()
}))
.ok()
.flatten()
}
#[cfg(not(windows))]
fn os_locale() -> Option<String> {
None
}
pub fn lang() -> Lang {
static LANG: OnceLock<Lang> = OnceLock::new();
*LANG.get_or_init(Lang::detect)
}
pub fn pick(en: &'static str, zh: &'static str) -> &'static str {
match lang() {
Lang::En => en,
Lang::Zh => zh,
}
}
pub fn pick_owned(en: String, zh: String) -> String {
match lang() {
Lang::En => en,
Lang::Zh => zh,
}
}
#[macro_export]
macro_rules! bi {
($en:expr, $zh:expr) => {
$crate::lang::pick($en, $zh)
};
}
#[macro_export]
macro_rules! bif {
($en:literal, $zh:literal) => {
$crate::lang::pick($en, $zh)
};
($en:literal, $zh:literal; $($arg:tt)*) => {
match $crate::lang::lang() {
$crate::lang::Lang::Zh => format!($zh, $($arg)*),
$crate::lang::Lang::En => format!($en, $($arg)*),
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zh_values_all_count_as_chinese() {
for v in [
"zh",
"zh_CN",
"zh_CN.UTF-8",
"zh_TW",
"zh_TW.UTF-8",
"zh_HK",
"zh_MO",
"zh_SG",
"ZH_cn",
] {
assert_eq!(Lang::from_value(v), Some(Lang::Zh), "{v}");
}
}
#[test]
fn other_values_are_english_or_unset() {
assert_eq!(Lang::from_value("en_US.UTF-8"), Some(Lang::En));
assert_eq!(Lang::from_value("fr_FR"), Some(Lang::En));
assert_eq!(Lang::from_value("ja_JP.UTF-8"), Some(Lang::En));
assert_eq!(Lang::from_value("C"), None);
assert_eq!(Lang::from_value("POSIX"), None);
assert_eq!(Lang::from_value(""), None);
}
#[test]
fn env_priority_and_fallthrough() {
fn env(pairs: &[(&str, &str)]) -> Option<Lang> {
Lang::from_env(|k| {
pairs
.iter()
.find(|(name, _)| *name == k)
.map(|(_, v)| (*v).to_string())
})
}
assert_eq!(env(&[("LANG", "zh_CN.UTF-8")]), Some(Lang::Zh));
assert_eq!(
env(&[("JSCD_LANG", "en"), ("LANG", "zh_CN.UTF-8")]),
Some(Lang::En)
);
assert_eq!(env(&[("LC_ALL", "C"), ("LANG", "zh_TW")]), Some(Lang::Zh));
assert_eq!(env(&[("LC_CTYPE", "zh_CN.UTF-8")]), Some(Lang::Zh));
assert_eq!(env(&[]), None);
}
}