cranpose_localization/
preferences.rs1use std::{
2 cell::RefCell,
3 sync::{
4 Arc, Mutex, Weak,
5 atomic::{AtomicU64, Ordering},
6 },
7};
8
9use crate::{Catalog, DeferredMessage, Language, Locale, LocalizationError, Translator};
10
11#[derive(Clone, Debug, Default, PartialEq, Eq)]
13pub enum LanguagePreference {
14 #[default]
16 System,
17 Selected(Language),
19}
20
21impl LanguagePreference {
22 pub fn from_setting(value: Option<&str>, catalog: &Catalog) -> Self {
24 value
25 .and_then(|tag| {
26 catalog
27 .languages()
28 .iter()
29 .find(|language| language.tag() == tag)
30 })
31 .cloned()
32 .map_or(Self::System, Self::Selected)
33 }
34
35 pub fn setting(&self) -> &str {
37 match self {
38 Self::System => "system",
39 Self::Selected(language) => language.tag(),
40 }
41 }
42
43 pub fn translator(&self, catalog: &Catalog, system: &[Locale]) -> Translator {
45 match self {
46 Self::System => catalog.negotiate(system),
47 Self::Selected(language) => catalog.translator(language.locale().clone()),
48 }
49 }
50}
51
52pub trait LanguagePreferenceStore: Send + Sync {
55 fn load(&self) -> Result<Option<String>, LocalizationError>;
57 fn save(&self, value: &str) -> Result<(), LocalizationError>;
59}
60
61struct State {
62 preference: LanguagePreference,
63 system: Vec<Locale>,
64 choice_revision: u64,
65}
66
67struct Data {
68 catalog: Catalog,
69 store: Option<Arc<dyn LanguagePreferenceStore>>,
70 state: Mutex<State>,
71 revision: AtomicU64,
72 writes: Mutex<()>,
73}
74
75#[derive(Clone)]
79pub struct Localization(Arc<Data>);
80
81impl PartialEq for Localization {
82 fn eq(&self, other: &Self) -> bool {
83 Arc::ptr_eq(&self.0, &other.0)
84 }
85}
86
87impl Localization {
88 pub fn new(catalog: Catalog, system: Vec<Locale>) -> Self {
90 Self::create(catalog, system, None)
91 }
92
93 pub fn persistent(
95 catalog: Catalog,
96 system: Vec<Locale>,
97 store: impl LanguagePreferenceStore + 'static,
98 ) -> Self {
99 Self::create(catalog, system, Some(Arc::new(store)))
100 }
101
102 fn create(
103 catalog: Catalog,
104 system: Vec<Locale>,
105 store: Option<Arc<dyn LanguagePreferenceStore>>,
106 ) -> Self {
107 Self(Arc::new(Data {
108 catalog,
109 store,
110 state: Mutex::new(State {
111 preference: LanguagePreference::System,
112 system,
113 choice_revision: 0,
114 }),
115 revision: AtomicU64::new(0),
116 writes: Mutex::new(()),
117 }))
118 }
119
120 pub fn catalog(&self) -> &Catalog {
122 &self.0.catalog
123 }
124
125 pub fn is_persistent(&self) -> bool {
128 self.0.store.is_some()
129 }
130
131 pub fn preference(&self) -> LanguagePreference {
133 self.0
134 .state
135 .lock()
136 .unwrap_or_else(std::sync::PoisonError::into_inner)
137 .preference
138 .clone()
139 }
140
141 pub fn revision(&self) -> u64 {
143 self.0.revision.load(Ordering::Acquire)
144 }
145
146 pub fn load(&self) -> Result<(), LocalizationError> {
149 let Some(store) = &self.0.store else {
150 return Ok(());
151 };
152 let revision = self
153 .0
154 .state
155 .lock()
156 .unwrap_or_else(std::sync::PoisonError::into_inner)
157 .choice_revision;
158 let stored = store.load()?;
159 let preference = LanguagePreference::from_setting(stored.as_deref(), self.catalog());
160 let mut state = self
161 .0
162 .state
163 .lock()
164 .unwrap_or_else(std::sync::PoisonError::into_inner);
165 if state.choice_revision == revision && state.preference != preference {
166 state.preference = preference;
167 self.0.revision.fetch_add(1, Ordering::Release);
168 }
169 Ok(())
170 }
171
172 pub fn select(&self, preference: LanguagePreference) -> Result<(), LocalizationError> {
175 let _write = self
176 .0
177 .writes
178 .lock()
179 .unwrap_or_else(std::sync::PoisonError::into_inner);
180 if let LanguagePreference::Selected(language) = &preference
181 && !self.catalog().languages().contains(language)
182 {
183 return Err(LocalizationError::Preference(
184 "language is not offered by this catalog".into(),
185 ));
186 }
187 if let Some(store) = &self.0.store {
188 store.save(preference.setting())?;
189 }
190 let mut state = self
191 .0
192 .state
193 .lock()
194 .unwrap_or_else(std::sync::PoisonError::into_inner);
195 state.choice_revision = state.choice_revision.wrapping_add(1);
196 if state.preference != preference {
197 state.preference = preference;
198 self.0.revision.fetch_add(1, Ordering::Release);
199 }
200 Ok(())
201 }
202
203 pub fn refresh_system(&self, system: &[Locale]) -> bool {
205 let mut state = self
206 .0
207 .state
208 .lock()
209 .unwrap_or_else(std::sync::PoisonError::into_inner);
210 if state.system == system {
211 return false;
212 }
213 state.system.clear();
214 state.system.extend_from_slice(system);
215 self.0.revision.fetch_add(1, Ordering::Release);
216 true
217 }
218
219 pub fn translator(&self) -> Translator {
221 let state = self
222 .0
223 .state
224 .lock()
225 .unwrap_or_else(std::sync::PoisonError::into_inner);
226 state.preference.translator(self.catalog(), &state.system)
227 }
228
229 pub fn format(&self, message: &DeferredMessage) -> Result<String, LocalizationError> {
231 THREAD_FORMATTER.with(|cache| {
232 let mut cache = cache.borrow_mut();
233 let revision = self.revision();
234 if !cache.as_ref().is_some_and(|cached| {
235 cached.owner.as_ptr() == Arc::as_ptr(&self.0) && cached.revision == revision
236 }) {
237 *cache = Some(Cached {
238 owner: Arc::downgrade(&self.0),
239 revision,
240 translator: self.translator(),
241 });
242 }
243 message.format(cache.as_ref().map(|cached| &cached.translator))
244 })
245 }
246
247 pub fn text(&self, message: &DeferredMessage) -> String {
249 self.format(message)
250 .unwrap_or_else(|_| message.fallback_text())
251 }
252}
253
254struct Cached {
255 owner: Weak<Data>,
256 revision: u64,
257 translator: Translator,
258}
259
260thread_local! {
261 static THREAD_FORMATTER: RefCell<Option<Cached>> = const { RefCell::new(None) };
262}