153 lines
4.4 KiB
Rust
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());
|
|
}
|
|
}
|