use std::{
cell::RefCell,
sync::{
Arc, Mutex, Weak,
atomic::{AtomicU64, Ordering},
},
};
use crate::{Catalog, DeferredMessage, Language, Locale, LocalizationError, Translator};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum LanguagePreference {
#[default]
System,
Selected(Language),
}
impl LanguagePreference {
pub fn from_setting(value: Option<&str>, catalog: &Catalog) -> Self {
value
.and_then(|tag| {
catalog
.languages()
.iter()
.find(|language| language.tag() == tag)
})
.cloned()
.map_or(Self::System, Self::Selected)
}
pub fn setting(&self) -> &str {
match self {
Self::System => "system",
Self::Selected(language) => language.tag(),
}
}
pub fn translator(&self, catalog: &Catalog, system: &[Locale]) -> Translator {
match self {
Self::System => catalog.negotiate(system),
Self::Selected(language) => catalog.translator(language.locale().clone()),
}
}
}
pub trait LanguagePreferenceStore: Send + Sync {
fn load(&self) -> Result<Option<String>, LocalizationError>;
fn save(&self, value: &str) -> Result<(), LocalizationError>;
}
struct State {
preference: LanguagePreference,
system: Vec<Locale>,
choice_revision: u64,
}
struct Data {
catalog: Catalog,
store: Option<Arc<dyn LanguagePreferenceStore>>,
state: Mutex<State>,
revision: AtomicU64,
writes: Mutex<()>,
}
#[derive(Clone)]
pub struct Localization(Arc<Data>);
impl PartialEq for Localization {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Localization {
pub fn new(catalog: Catalog, system: Vec<Locale>) -> Self {
Self::create(catalog, system, None)
}
pub fn persistent(
catalog: Catalog,
system: Vec<Locale>,
store: impl LanguagePreferenceStore + 'static,
) -> Self {
Self::create(catalog, system, Some(Arc::new(store)))
}
fn create(
catalog: Catalog,
system: Vec<Locale>,
store: Option<Arc<dyn LanguagePreferenceStore>>,
) -> Self {
Self(Arc::new(Data {
catalog,
store,
state: Mutex::new(State {
preference: LanguagePreference::System,
system,
choice_revision: 0,
}),
revision: AtomicU64::new(0),
writes: Mutex::new(()),
}))
}
pub fn catalog(&self) -> &Catalog {
&self.0.catalog
}
pub fn is_persistent(&self) -> bool {
self.0.store.is_some()
}
pub fn preference(&self) -> LanguagePreference {
self.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.preference
.clone()
}
pub fn revision(&self) -> u64 {
self.0.revision.load(Ordering::Acquire)
}
pub fn load(&self) -> Result<(), LocalizationError> {
let Some(store) = &self.0.store else {
return Ok(());
};
let revision = self
.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.choice_revision;
let stored = store.load()?;
let preference = LanguagePreference::from_setting(stored.as_deref(), self.catalog());
let mut state = self
.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.choice_revision == revision && state.preference != preference {
state.preference = preference;
self.0.revision.fetch_add(1, Ordering::Release);
}
Ok(())
}
pub fn select(&self, preference: LanguagePreference) -> Result<(), LocalizationError> {
let _write = self
.0
.writes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let LanguagePreference::Selected(language) = &preference
&& !self.catalog().languages().contains(language)
{
return Err(LocalizationError::Preference(
"language is not offered by this catalog".into(),
));
}
if let Some(store) = &self.0.store {
store.save(preference.setting())?;
}
let mut state = self
.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.choice_revision = state.choice_revision.wrapping_add(1);
if state.preference != preference {
state.preference = preference;
self.0.revision.fetch_add(1, Ordering::Release);
}
Ok(())
}
pub fn refresh_system(&self, system: &[Locale]) -> bool {
let mut state = self
.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.system == system {
return false;
}
state.system.clear();
state.system.extend_from_slice(system);
self.0.revision.fetch_add(1, Ordering::Release);
true
}
pub fn translator(&self) -> Translator {
let state = self
.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.preference.translator(self.catalog(), &state.system)
}
pub fn format(&self, message: &DeferredMessage) -> Result<String, LocalizationError> {
THREAD_FORMATTER.with(|cache| {
let mut cache = cache.borrow_mut();
let revision = self.revision();
if !cache.as_ref().is_some_and(|cached| {
cached.owner.as_ptr() == Arc::as_ptr(&self.0) && cached.revision == revision
}) {
*cache = Some(Cached {
owner: Arc::downgrade(&self.0),
revision,
translator: self.translator(),
});
}
message.format(cache.as_ref().map(|cached| &cached.translator))
})
}
pub fn text(&self, message: &DeferredMessage) -> String {
self.format(message)
.unwrap_or_else(|_| message.fallback_text())
}
}
struct Cached {
owner: Weak<Data>,
revision: u64,
translator: Translator,
}
thread_local! {
static THREAD_FORMATTER: RefCell<Option<Cached>> = const { RefCell::new(None) };
}