use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
use arc_swap::{ArcSwap, Guard};
use rustc_hash::FxHashMap;
use crate::error::LangError;
use crate::intern::intern;
use crate::loader;
type LocaleMap = FxHashMap<&'static str, &'static str>;
struct LangState {
path: &'static str,
active: &'static str,
fallbacks: Arc<[&'static str]>,
locales: FxHashMap<&'static str, Arc<LocaleMap>>,
}
impl LangState {
fn initial() -> Self {
Self {
path: "locales",
active: "en",
fallbacks: Arc::from(["en"].as_slice()),
locales: FxHashMap::default(),
}
}
}
impl Clone for LangState {
fn clone(&self) -> Self {
Self {
path: self.path,
active: self.active,
fallbacks: Arc::clone(&self.fallbacks),
locales: self.locales.clone(),
}
}
}
static STATE: OnceLock<ArcSwap<LangState>> = OnceLock::new();
static WRITE_LOCK: Mutex<()> = Mutex::new(());
fn state() -> &'static ArcSwap<LangState> {
STATE.get_or_init(|| ArcSwap::new(Arc::new(LangState::initial())))
}
fn snapshot() -> Guard<Arc<LangState>> {
state().load()
}
fn with_write<F>(mutate: F)
where
F: FnOnce(&mut LangState),
{
let _guard = WRITE_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
let current = state().load_full();
let mut next = (*current).clone();
mutate(&mut next);
state().store(Arc::new(next));
}
pub struct Lang;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Translator {
locale: &'static str,
}
impl Translator {
#[must_use]
pub fn new(locale: impl AsRef<str>) -> Self {
Self {
locale: intern(locale.as_ref()),
}
}
#[must_use]
pub fn locale(&self) -> &'static str {
self.locale
}
#[must_use]
pub fn translate<'a>(&self, key: &'a str) -> Cow<'a, str> {
Lang::translate(key, Some(self.locale), None)
}
#[must_use]
pub fn translate_with_fallback<'a>(&self, key: &'a str, fallback: &'a str) -> Cow<'a, str> {
Lang::translate(key, Some(self.locale), Some(fallback))
}
}
impl Lang {
pub fn set_path(path: impl AsRef<str>) {
let interned = intern(path.as_ref());
with_write(|state| state.path = interned);
}
#[must_use]
pub fn path() -> &'static str {
snapshot().path
}
pub fn set_locale(locale: impl AsRef<str>) {
let interned = intern(locale.as_ref());
with_write(|state| state.active = interned);
}
#[must_use]
pub fn locale() -> &'static str {
snapshot().active
}
#[allow(clippy::needless_pass_by_value, reason = "preserves 1.0.x signature")]
pub fn set_fallbacks(chain: Vec<String>) {
let interned: Vec<&'static str> = chain.iter().map(|s| intern(s)).collect();
let arc: Arc<[&'static str]> = Arc::from(interned);
with_write(|state| state.fallbacks = Arc::clone(&arc));
}
pub fn load(locale: impl AsRef<str>) -> Result<(), LangError> {
let locale = locale.as_ref();
let path = snapshot().path;
let map = loader::load_file(path, locale)?;
let interned_locale = intern(locale);
let arc_map: Arc<LocaleMap> = Arc::new(map);
with_write(|state| {
let _ = state.locales.insert(interned_locale, Arc::clone(&arc_map));
});
Ok(())
}
pub fn load_from(locale: impl AsRef<str>, path: &str) -> Result<(), LangError> {
let locale = locale.as_ref();
let map = loader::load_file(path, locale)?;
let interned_locale = intern(locale);
let arc_map: Arc<LocaleMap> = Arc::new(map);
with_write(|state| {
let _ = state.locales.insert(interned_locale, Arc::clone(&arc_map));
});
Ok(())
}
#[must_use]
pub fn is_loaded(locale: &str) -> bool {
snapshot().locales.contains_key(locale)
}
#[must_use]
pub fn loaded() -> Vec<&'static str> {
let mut locales: Vec<&'static str> = snapshot().locales.keys().copied().collect();
locales.sort_unstable();
locales
}
pub fn unload(locale: &str) {
with_write(|state| {
let _ = state.locales.remove(locale);
});
}
#[must_use]
pub fn translator(locale: impl AsRef<str>) -> Translator {
Translator::new(locale)
}
#[must_use]
pub fn translate<'a>(
key: &'a str,
locale: Option<&'a str>,
fallback: Option<&'a str>,
) -> Cow<'a, str> {
let state = snapshot();
let target: &str = locale.unwrap_or(state.active);
if let Some(map) = state.locales.get(target) {
if let Some(&val) = map.get(key) {
return Cow::Borrowed(val);
}
}
let mut seen = HashSet::with_capacity(state.fallbacks.len());
for &fb_locale in state.fallbacks.iter() {
if fb_locale == target || !seen.insert(fb_locale) {
continue;
}
if let Some(map) = state.locales.get(fb_locale) {
if let Some(&val) = map.get(key) {
return Cow::Borrowed(val);
}
}
}
if let Some(fb) = fallback {
return Cow::Borrowed(fb);
}
Cow::Borrowed(key)
}
}