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