use std::collections::{HashMap, HashSet};
use std::sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::error::LangError;
use crate::loader;
struct LangState {
path: String,
active: String,
fallbacks: Vec<String>,
locales: HashMap<String, HashMap<String, String>>,
}
impl LangState {
fn new() -> Self {
Self {
path: "locales".to_string(),
active: "en".to_string(),
fallbacks: vec!["en".to_string()],
locales: HashMap::new(),
}
}
}
static STATE: OnceLock<RwLock<LangState>> = OnceLock::new();
fn state() -> &'static RwLock<LangState> {
STATE.get_or_init(|| RwLock::new(LangState::new()))
}
fn read_state() -> RwLockReadGuard<'static, LangState> {
state()
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn write_state() -> RwLockWriteGuard<'static, LangState> {
state()
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub struct Lang;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Translator {
locale: String,
}
impl Translator {
pub fn new(locale: impl Into<String>) -> Self {
Self {
locale: locale.into(),
}
}
pub fn locale(&self) -> &str {
&self.locale
}
pub fn translate(&self, key: &str) -> String {
Lang::translate(key, Some(self.locale.as_str()), None)
}
pub fn translate_with_fallback(&self, key: &str, fallback: &str) -> String {
Lang::translate(key, Some(self.locale.as_str()), Some(fallback))
}
}
impl Lang {
pub fn set_path(path: impl Into<String>) {
write_state().path = path.into();
}
pub fn path() -> String {
read_state().path.clone()
}
pub fn set_locale(locale: impl Into<String>) {
write_state().active = locale.into();
}
pub fn locale() -> String {
read_state().active.clone()
}
pub fn set_fallbacks(chain: Vec<String>) {
write_state().fallbacks = chain;
}
pub fn load(locale: impl Into<String>) -> Result<(), LangError> {
let locale = locale.into();
let path = read_state().path.clone();
let map = loader::load_file(&path, &locale)?;
write_state().locales.insert(locale, map);
Ok(())
}
pub fn load_from(locale: impl Into<String>, path: &str) -> Result<(), LangError> {
let locale = locale.into();
let map = loader::load_file(path, &locale)?;
write_state().locales.insert(locale, map);
Ok(())
}
pub fn is_loaded(locale: &str) -> bool {
read_state().locales.contains_key(locale)
}
pub fn loaded() -> Vec<String> {
let mut locales: Vec<_> = read_state().locales.keys().cloned().collect();
locales.sort_unstable();
locales
}
pub fn unload(locale: &str) {
write_state().locales.remove(locale);
}
pub fn translator(locale: impl Into<String>) -> Translator {
Translator::new(locale)
}
pub fn translate(key: &str, locale: Option<&str>, fallback: Option<&str>) -> String {
let state = read_state();
let target = locale.unwrap_or(&state.active);
if let Some(map) = state.locales.get(target) {
if let Some(val) = map.get(key) {
return val.clone();
}
}
let mut seen = HashSet::with_capacity(state.fallbacks.len());
for fb_locale in &state.fallbacks {
if fb_locale == target || !seen.insert(fb_locale.as_str()) {
continue;
}
if let Some(map) = state.locales.get(fb_locale.as_str()) {
if let Some(val) = map.get(key) {
return val.clone();
}
}
}
if let Some(fb) = fallback {
return fb.to_string();
}
key.to_string()
}
}