use crate::assets::LocaleAssets;
use std::collections::HashMap;
use std::sync::LazyLock;
pub type LocaleMap = HashMap<&'static str, &'static str>;
static EN: LazyLock<LocaleMap> = LazyLock::new(|| load_locale("en"));
static ZH_CN: LazyLock<LocaleMap> = LazyLock::new(|| load_locale("zh-CN"));
fn load_locale(name: &str) -> LocaleMap {
let path = format!("{}.toml", name);
let file = match LocaleAssets::get(&path) {
Some(f) => f,
None => {
eprintln!("i18n: locale file {} not found in embedded assets", path);
return HashMap::new();
}
};
let content = match std::str::from_utf8(&file.data) {
Ok(s) => s,
Err(e) => {
eprintln!("i18n: invalid UTF-8 in locale {}: {}", path, e);
return HashMap::new();
}
};
let toml: toml::Table = match toml::from_str(content) {
Ok(t) => t,
Err(e) => {
eprintln!("i18n: failed to parse locales/{}.toml: {}", name, e);
return HashMap::new();
}
};
let mut map = HashMap::new();
flatten_toml(&toml, "", &mut map);
map
}
fn flatten_toml(table: &toml::Table, prefix: &str, map: &mut HashMap<&'static str, &'static str>) {
for (key, value) in table {
match value {
toml::Value::Table(t) => {
let new_prefix = if prefix.is_empty() {
format!("{}_", key)
} else {
format!("{}{}_", prefix, key)
};
flatten_toml(t, &new_prefix, map);
}
toml::Value::String(s) => {
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{}{}", prefix, key)
};
map.insert(
Box::leak(full_key.into_boxed_str()),
Box::leak(s.clone().into_boxed_str()),
);
}
_ => {}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Locale {
pub map: &'static LocaleMap,
pub en: &'static LocaleMap,
}
impl Locale {
pub fn from_accept_language(header: Option<&str>) -> Self {
let en = &*EN;
let zh_cn = &*ZH_CN;
let map = match header {
Some(h) => {
let best = best_locale(h);
match best.as_str() {
"zh-CN" => zh_cn,
"en" => en,
_ => zh_cn, }
}
None => zh_cn,
};
Locale { map, en }
}
pub fn t(&self, key: &str) -> &'static str {
self.map
.get(key)
.copied()
.or_else(|| self.en.get(key).copied())
.unwrap_or_else(|| Box::leak(key.to_string().into_boxed_str()))
}
pub fn resolve_flash(&self, raw: &str, param: Option<&str>) -> String {
let translated = self.t(raw);
if translated == raw {
raw.replace('+', " ")
} else if let Some(p) = param {
translated.replace("{0}", p)
} else {
translated.to_string()
}
}
}
fn best_locale(header: &str) -> String {
let mut best_q = 0.0f32;
let mut best_lang = String::from("zh-CN");
for part in header.split(',') {
let part = part.trim();
let (lang_tag, q) = if let Some((tag, q_part)) = part.split_once(';') {
let q = q_part
.trim()
.strip_prefix("q=")
.and_then(|s| s.parse::<f32>().ok())
.unwrap_or(1.0);
(tag.trim(), q)
} else {
(part, 1.0)
};
if q <= best_q {
continue;
}
let lang_lower = lang_tag.to_lowercase();
if lang_lower.starts_with("zh") {
best_q = q;
best_lang = String::from("zh-CN");
} else if lang_lower.starts_with("en") {
best_q = q;
best_lang = String::from("en");
}
}
best_lang
}
pub fn init() {
LazyLock::force(&EN);
LazyLock::force(&ZH_CN);
}