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