cranpose_ui/
language_preferences.rs1use cranpose_core::{
2 CompositionLocal, CompositionLocalProvider, LaunchedEffect, MutableState, compositionLocalOf,
3 launchBlocking, rememberKeyed, rememberMutableStateOf,
4};
5
6use crate::localization::{Catalog, LanguagePreference, Locale, Localization, ProvideTranslator};
7
8#[derive(Clone, Default, PartialEq)]
9struct SelectionState {
10 revision: u64,
11 loading: bool,
12 saving: bool,
13 pending: Option<LanguagePreference>,
14 error: Option<String>,
15}
16
17#[derive(Clone)]
20pub struct LocalizationController {
21 application: Localization,
22 state: MutableState<SelectionState>,
23}
24
25impl PartialEq for LocalizationController {
26 fn eq(&self, other: &Self) -> bool {
27 self.application == other.application
28 }
29}
30
31impl LocalizationController {
32 pub fn preference(&self) -> LanguagePreference {
34 let _revision = self.state.get().revision;
35 self.application.preference()
36 }
37
38 pub fn catalog(&self) -> &Catalog {
40 self.application.catalog()
41 }
42
43 pub fn application(&self) -> &Localization {
45 &self.application
46 }
47
48 pub fn is_busy(&self) -> bool {
50 let state = self.state.get();
51 state.loading || state.saving
52 }
53
54 pub fn error(&self) -> Option<String> {
56 self.state.get().error
57 }
58
59 pub fn select(&self, preference: LanguagePreference) {
62 if !self.application.is_persistent() {
63 let result = self.application.select(preference);
64 self.state.update(|state| {
65 state.revision = self.application.revision();
66 state.error = result.err().map(|error| error.to_string());
67 });
68 return;
69 }
70 self.state.update(|state| {
71 state.pending = Some(preference);
72 state.error = None;
73 });
74 self.save_next();
75 }
76
77 fn save_next(&self) {
78 let mut next = None;
79 self.state.update(|state| {
80 if !state.loading && !state.saving {
81 next = state.pending.take();
82 state.saving = next.is_some();
83 }
84 });
85 let Some(preference) = next else { return };
86 let application = self.application.clone();
87 let controller = self.clone();
88 let started = launchBlocking(
89 move || application.select(preference),
90 move |result| {
91 controller.state.update(|state| {
92 state.saving = false;
93 state.revision = controller.application.revision();
94 state.error = storage_error(result);
95 });
96 controller.save_next();
97 },
98 );
99 if let Err(error) = started {
100 self.state.update(|state| {
101 state.saving = false;
102 state.error = Some(error.to_string());
103 });
104 }
105 }
106}
107
108pub fn local_localization() -> CompositionLocal<Option<LocalizationController>> {
110 crate::environment_locals::cached_local(
111 |locals| &locals.localization_controller,
112 || compositionLocalOf(|| None),
113 )
114}
115
116#[expect(non_snake_case)]
120#[track_caller]
121pub fn ProvideLanguagePreferences(
122 application: Localization,
123 system: &[Locale],
124 content: impl FnOnce(),
125) {
126 application.refresh_system(system);
127 let state = rememberMutableStateOf(|| SelectionState {
128 loading: application.is_persistent(),
129 ..SelectionState::default()
130 });
131 let controller = LocalizationController {
132 application: application.clone(),
133 state,
134 };
135 let loader = controller.clone();
136 LaunchedEffect(application.clone(), move |_| {
137 let application = loader.application.clone();
138 if !application.is_persistent() {
139 return;
140 }
141 let state = loader.state;
142 let started = launchBlocking(
143 move || application.load(),
144 move |result| {
145 loader.state.update(|state| {
146 state.loading = false;
147 state.revision = loader.application.revision();
148 state.error = storage_error(result);
149 });
150 loader.save_next();
151 },
152 );
153 if let Err(error) = started {
154 state.update(|state| {
155 state.loading = false;
156 state.error = Some(error.to_string());
157 });
158 }
159 });
160 let _observed = state.get().revision;
161 let translator = rememberKeyed(
162 (application.clone(), application.revision()),
163 |(application, _)| application.translator(),
164 );
165 CompositionLocalProvider([local_localization().provides(Some(controller))], || {
166 ProvideTranslator(translator, content);
167 });
168}
169
170fn storage_error(
171 result: Result<
172 Result<(), crate::localization::LocalizationError>,
173 cranpose_core::BlockingError,
174 >,
175) -> Option<String> {
176 match result {
177 Ok(Ok(())) => None,
178 Ok(Err(error)) => Some(error.to_string()),
179 Err(error) => Some(error.to_string()),
180 }
181}