use rustc_hash::FxHashMap;
use std::sync::OnceLock;
static LOCALE: OnceLock<LocaleMessages> = OnceLock::new();
#[derive(Debug, Default)]
pub struct LocaleMessages {
messages: FxHashMap<u32, String>,
locale_id: String,
}
impl LocaleMessages {
pub fn load(locale_id: &str) -> Option<Self> {
let normalized = normalize_locale(locale_id)?;
let json_content = get_locale_content(normalized)?;
let messages = parse_locale_json(json_content)?;
Some(Self {
messages,
locale_id: normalized.to_string(),
})
}
pub fn get_message<'a>(&self, code: u32, fallback: &'a str) -> &'a str {
if self.messages.contains_key(&code) {
fallback
} else {
fallback
}
}
pub fn get_message_owned(&self, code: u32, fallback: &str) -> String {
self.messages
.get(&code)
.cloned()
.unwrap_or_else(|| fallback.to_string())
}
pub fn has_translation(&self, code: u32) -> bool {
self.messages.contains_key(&code)
}
pub fn locale_id(&self) -> &str {
&self.locale_id
}
pub const fn is_default(&self) -> bool {
self.locale_id.is_empty()
}
}
pub fn init_locale(locale_id: Option<&str>) {
let locale = locale_id.and_then(LocaleMessages::load).unwrap_or_default();
let _ = LOCALE.set(locale);
}
pub fn get_locale() -> &'static LocaleMessages {
LOCALE.get_or_init(LocaleMessages::default)
}
pub fn translate(code: u32, fallback: &str) -> String {
let locale = get_locale();
if locale.is_default() || !locale.has_translation(code) {
return fallback.to_string();
}
let template = locale.get_message_owned(code, fallback);
if !template.contains("{0}") {
return template;
}
substitute_params_from_english(code, &template, fallback)
}
fn substitute_params_from_english(_code: u32, template: &str, formatted_english: &str) -> String {
let params = extract_quoted_strings(formatted_english);
let mut result = template.to_string();
for (i, param) in params.iter().enumerate() {
let placeholder = format!("{{{i}}}");
result = result.replace(&placeholder, param);
}
result
}
fn extract_quoted_strings(message: &str) -> Vec<&str> {
let mut params = Vec::new();
let mut chars = message.char_indices().peekable();
while let Some((idx, ch)) = chars.next() {
if ch == '\'' {
let content_start = idx + 1;
while let Some((pos, c)) = chars.next() {
if c == '\'' {
if let Some((_, next)) = chars.peek()
&& *next == '\''
{
chars.next();
continue;
}
if content_start < pos {
params.push(&message[content_start..pos]);
}
break;
}
}
}
}
params
}
fn normalize_locale(locale: &str) -> Option<&'static str> {
let lower = locale.to_lowercase();
match lower.as_str() {
"cs" | "cs-cz" | "czech" => Some("cs"),
"de" | "de-de" | "de-at" | "de-ch" | "german" => Some("de"),
"es" | "es-es" | "es-mx" | "spanish" => Some("es"),
"fr" | "fr-fr" | "fr-ca" | "french" => Some("fr"),
"it" | "it-it" | "italian" => Some("it"),
"ja" | "ja-jp" | "japanese" => Some("ja"),
"ko" | "ko-kr" | "korean" => Some("ko"),
"pl" | "pl-pl" | "polish" => Some("pl"),
"pt-br" | "pt" | "portuguese" => Some("pt-br"),
"ru" | "ru-ru" | "russian" => Some("ru"),
"tr" | "tr-tr" | "turkish" => Some("tr"),
"zh-cn" | "zh-hans" | "zh" | "chinese" => Some("zh-cn"),
"zh-tw" | "zh-hant" => Some("zh-tw"),
_ => None,
}
}
fn get_locale_content(locale: &str) -> Option<&'static str> {
match locale {
"cs" => Some(include_str!("locales/cs.json")),
"de" => Some(include_str!("locales/de.json")),
"es" => Some(include_str!("locales/es.json")),
"fr" => Some(include_str!("locales/fr.json")),
"it" => Some(include_str!("locales/it.json")),
"ja" => Some(include_str!("locales/ja.json")),
"ko" => Some(include_str!("locales/ko.json")),
"pl" => Some(include_str!("locales/pl.json")),
"pt-br" => Some(include_str!("locales/pt-br.json")),
"ru" => Some(include_str!("locales/ru.json")),
"tr" => Some(include_str!("locales/tr.json")),
"zh-cn" => Some(include_str!("locales/zh-cn.json")),
"zh-tw" => Some(include_str!("locales/zh-tw.json")),
_ => None,
}
}
fn parse_locale_json(json: &str) -> Option<FxHashMap<u32, String>> {
let parsed: serde_json::Value = serde_json::from_str(json).ok()?;
let obj = parsed.as_object()?;
let mut messages = FxHashMap::default();
for (key, value) in obj {
if let Some(code) = extract_code_from_key(key)
&& let Some(msg) = value.as_str()
{
messages.insert(code, msg.to_string());
}
}
Some(messages)
}
fn extract_code_from_key(key: &str) -> Option<u32> {
let last_underscore = key.rfind('_')?;
let code_str = &key[last_underscore + 1..];
code_str.parse().ok()
}
pub const fn supported_locales() -> &'static [&'static str] {
&[
"cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-br", "ru", "tr", "zh-cn", "zh-tw",
]
}
#[cfg(test)]
#[path = "locale_tests.rs"]
mod tests;