Files
feresaria/crates/ferese-config/src/locale.rs
T

153 lines
4.4 KiB
Rust

//! Ferese interface language; independent of keyboard layout and system locale.
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::{
OnceLock,
atomic::{AtomicU8, Ordering},
};
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
pub enum Language {
#[default]
#[serde(rename = "auto")]
Auto,
#[serde(rename = "en", alias = "en-US", alias = "en_US")]
English,
#[serde(rename = "zh-CN", alias = "zh_CN", alias = "zh-Hans", alias = "zh")]
Chinese,
}
impl Language {
#[must_use]
pub const fn key(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::English => "en",
Self::Chinese => "zh-CN",
}
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct Settings {
pub language: Language,
}
static LANGUAGE: AtomicU8 = AtomicU8::new(0);
pub fn set(language: Language) {
LANGUAGE.store(
match language {
Language::Auto => 0,
Language::English => 1,
Language::Chinese => 2,
},
Ordering::Relaxed,
);
}
#[must_use]
pub fn configured() -> Language {
match LANGUAGE.load(Ordering::Relaxed) {
1 => Language::English,
2 => Language::Chinese,
_ => Language::Auto,
}
}
#[must_use]
pub fn language_for_locale(locale: &str) -> Language {
if locale.to_ascii_lowercase().starts_with("zh") {
Language::Chinese
} else {
Language::English
}
}
#[must_use]
pub fn resolved() -> Language {
match configured() {
Language::Auto => {
static SYSTEM: OnceLock<Language> = OnceLock::new();
*SYSTEM.get_or_init(|| {
let locale = ["LC_ALL", "LC_MESSAGES", "LANGUAGE", "LANG"]
.into_iter()
.find_map(|key| std::env::var(key).ok().filter(|value| !value.is_empty()))
.unwrap_or_else(|| "en".into());
language_for_locale(&locale)
})
}
language => language,
}
}
pub fn from_document(document: &crate::Document) -> Language {
document
.get("ui.language")
.cloned()
.and_then(|value| serde_json::from_value(value).ok())
.unwrap_or_default()
}
pub fn load(path: Option<&std::path::Path>) {
let language = path
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|source| crate::Document::parse(&source).ok())
.map(|doc| from_document(&doc))
.unwrap_or_default();
set(language);
}
fn chinese() -> &'static BTreeMap<String, String> {
static CATALOG: OnceLock<BTreeMap<String, String>> = OnceLock::new();
CATALOG.get_or_init(|| {
serde_json::from_str(include_str!("../locales/zh-CN.json"))
.expect("valid built-in Chinese catalog")
})
}
#[must_use]
pub fn in_language<'a>(language: Language, key: &'a str) -> &'a str {
if language == Language::Chinese {
chinese().get(key).map_or(key, String::as_str)
} else {
key
}
}
#[must_use]
pub fn translate(key: &str) -> String {
in_language(resolved(), key).to_owned()
}
#[must_use]
pub fn text(key: &'static str) -> &'static str {
in_language(resolved(), key)
}
#[must_use]
pub fn format(key: &str, arguments: &[(&str, String)]) -> String {
let mut result = translate(key);
for (name, value) in arguments {
result = result.replace(&format!("{{{name}}}"), value);
}
result
}
#[macro_export]
macro_rules! tr {
($key:literal) => {
$crate::locale::text($key)
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locale_choice_does_not_rewrite_identifiers_or_unknown_values() {
assert_eq!(language_for_locale("zh_CN.UTF-8"), Language::Chinese);
assert_eq!(language_for_locale("en_US.UTF-8"), Language::English);
assert_eq!(language_for_locale("C.UTF-8"), Language::English);
assert_eq!(in_language(Language::Chinese, "Appearance"), "外观与设置");
assert_eq!(in_language(Language::English, "Appearance"), "Appearance");
assert_eq!(
in_language(Language::Chinese, "org.example.CustomApp"),
"org.example.CustomApp"
);
assert!(serde_json::from_str::<Language>("\"unsupported\"").is_err());
}
}