euv_ui/hook/i18n/impl.rs
1use super::*;
2
3/// Implements [`HookContextI18nExt`] for [`HookContext`].
4impl HookContextI18nExt for HookContext {
5 /// Returns a fresh [`I18n`] bound to the current component scope.
6 ///
7 /// # Returns
8 ///
9 /// - `I18n` - A `I18n` value.
10 fn i18n() -> I18n {
11 HookContext::use_hook(|| {
12 I18n::new(
13 Signal::create(String::from("en")),
14 Signal::create(String::from("en")),
15 )
16 })
17 }
18}
19
20/// Inherent implementation of [`I18n`].
21impl I18n {
22 /// Sets the active locale to `locale`. Triggers a
23 /// reactive update so any reactive `t(key)` read
24 /// re-evaluates.
25 ///
26 /// Named `change_locale` (not `set_locale`) to avoid
27 /// colliding with the `set_locale` getter generated by
28 /// `#[derive(Data)]` on the struct field.
29 ///
30 /// # Arguments
31 ///
32 /// - `&str` - Shared reference to a `str`.
33 pub fn change_locale(&self, locale: &str) {
34 self.get_locale().set(locale.to_string());
35 }
36
37 /// Sets the fallback locale. Trigger a reactive
38 /// update for any `t(key)` whose key is missing in
39 /// the active locale — they may now resolve to a
40 /// different fallback value.
41 ///
42 /// Named `change_fallback_locale` (not
43 /// `set_fallback_locale`) for the same reason as
44 /// `change_locale`.
45 ///
46 /// # Arguments
47 ///
48 /// - `&str` - Shared reference to a `str`.
49 pub fn change_fallback_locale(&self, locale: &str) {
50 self.get_fallback_locale().set(locale.to_string());
51 }
52
53 /// Adds a batch of `(key, message)` entries to the
54 /// translation table for `locale`. Existing entries
55 /// for that locale are overwritten (last-write-wins).
56 ///
57 /// OPT 22 (tail): writes through the process-wide
58 /// [`I18N_MESSAGES`] lock instead of cloning the whole
59 /// table out of a `Signal` and re-setting it.
60 ///
61 /// # Arguments
62 ///
63 /// - `&str` - Shared reference to a `str`.
64 /// - `&[MessageEntry]` - Shared reference to a `[MessageEntry]`.
65 pub fn add_messages(&self, locale: &str, entries: &[MessageEntry]) {
66 let mut guard: std::sync::RwLockWriteGuard<
67 'static,
68 HashMap<String, HashMap<String, String>>,
69 > = messages_lock().write().unwrap_or_else(
70 |e: std::sync::PoisonError<
71 std::sync::RwLockWriteGuard<'static, HashMap<String, HashMap<String, String>>>,
72 >| e.into_inner(),
73 );
74 let entry_map: &mut HashMap<String, String> = guard.entry(locale.to_string()).or_default();
75 for (key, value) in entries {
76 entry_map.insert((*key).to_string(), (*value).to_string());
77 }
78 }
79
80 /// Removes every entry for `locale`. After this
81 /// call, `t(key)` for any key will skip this locale
82 /// in its lookup chain.
83 ///
84 /// OPT 22 (tail): see `add_messages` — writes through
85 /// the static lock.
86 ///
87 /// # Arguments
88 ///
89 /// - `&str` - Shared reference to a `str`.
90 pub fn remove_locale(&self, locale: &str) {
91 let mut guard: std::sync::RwLockWriteGuard<
92 'static,
93 HashMap<String, HashMap<String, String>>,
94 > = messages_lock().write().unwrap_or_else(
95 |e: std::sync::PoisonError<
96 std::sync::RwLockWriteGuard<'static, HashMap<String, HashMap<String, String>>>,
97 >| e.into_inner(),
98 );
99 guard.remove(locale);
100 }
101
102 /// Removes a single message from a locale. After this
103 /// call, `t(key)` for this key in this locale will
104 /// fall back to `fallback_locale`.
105 ///
106 /// OPT 22 (tail): see `add_messages` — writes through
107 /// the static lock.
108 ///
109 /// # Arguments
110 ///
111 /// - `&str` - Shared reference to a `str`.
112 /// - `&str` - Shared reference to a `str`.
113 pub fn remove_message(&self, locale: &str, key: &str) {
114 let mut guard: std::sync::RwLockWriteGuard<
115 'static,
116 HashMap<String, HashMap<String, String>>,
117 > = messages_lock().write().unwrap_or_else(
118 |e: std::sync::PoisonError<
119 std::sync::RwLockWriteGuard<'static, HashMap<String, HashMap<String, String>>>,
120 >| e.into_inner(),
121 );
122 if let Some(entry_map) = guard.get_mut(locale) {
123 entry_map.remove(key);
124 }
125 }
126
127 /// Translates `key` to a string under the active
128 /// locale, falling back to `fallback_locale` if the
129 /// active locale has no entry. Returns `key` itself
130 /// if neither locale has an entry (debug-friendly).
131 ///
132 /// This is the reactive read — calling it inside a
133 /// render closure subscribes that closure to locale
134 /// changes. The messages table itself is not reactive
135 /// (it lives behind [`I18N_MESSAGES`]).
136 ///
137 /// OPT 22 (tail): previously the read cloned the
138 /// entire translation table out of a `Signal` on every
139 /// call (a `HashMap<String, HashMap<String, String>>`
140 /// per `t()`). Now the read takes the read guard and
141 /// clones at most a single `String` (the message
142 /// itself) before dropping the guard.
143 ///
144 /// # Arguments
145 ///
146 /// - `&str` - Shared reference to a `str`.
147 ///
148 /// # Returns
149 ///
150 /// - `String` - A `String` value.
151 pub fn t(&self, key: &str) -> String {
152 // Borrow both locale signals via `Signal::with` instead of paying
153 // two `String` clones per call (per `t()` per render).
154 self.get_locale().with(|active: &String| {
155 self.get_fallback_locale().with(|fallback: &String| {
156 let guard: std::sync::RwLockReadGuard<
157 'static,
158 HashMap<String, HashMap<String, String>>,
159 > = messages_lock().read().unwrap_or_else(
160 |e: std::sync::PoisonError<
161 std::sync::RwLockReadGuard<
162 'static,
163 HashMap<String, HashMap<String, String>>,
164 >,
165 >| e.into_inner(),
166 );
167 if let Some(message) = guard
168 .get(active.as_str())
169 .and_then(|m: &HashMap<String, String>| m.get(key))
170 {
171 return message.clone();
172 }
173 if let Some(message) = guard
174 .get(fallback.as_str())
175 .and_then(|m: &HashMap<String, String>| m.get(key))
176 {
177 return message.clone();
178 }
179 key.to_string()
180 })
181 })
182 }
183
184 /// Translates `key` and substitutes `{name}`-style
185 /// placeholders from `vars`.
186 ///
187 /// Placeholders that are present in `vars` are
188 /// replaced with their corresponding value.
189 /// Placeholders that are missing from `vars` are left
190 /// as the literal `{name}` token — matching the
191 /// i18next default behavior. No escaping is
192 /// supported; add it when a real use case shows up.
193 ///
194 /// # Arguments
195 ///
196 /// - `&str` - Shared reference to a `str`.
197 /// - `&HashMap<&'static str, &'static str>` - Shared reference to a `HashMap<&'static str, &'static str>`.
198 ///
199 /// # Returns
200 ///
201 /// - `String` - A `String` value.
202 pub fn t_with(&self, key: &str, vars: &HashMap<&'static str, &'static str>) -> String {
203 let template: String = self.t(key);
204 interpolate(&template, vars)
205 }
206
207 /// Returns the number of locales currently registered
208 /// (i.e. the number of distinct keys in the messages
209 /// map's outer level).
210 ///
211 /// OPT 22 (tail): borrows through [`I18N_MESSAGES`]
212 /// read guard instead of cloning the whole table.
213 ///
214 /// # Returns
215 ///
216 /// - `usize` - Count of registered locales.
217 pub fn locale_count(&self) -> usize {
218 let guard: std::sync::RwLockReadGuard<'static, HashMap<String, HashMap<String, String>>> =
219 messages_lock().read().unwrap_or_else(
220 |e: std::sync::PoisonError<
221 std::sync::RwLockReadGuard<'static, HashMap<String, HashMap<String, String>>>,
222 >| e.into_inner(),
223 );
224 guard.len()
225 }
226
227 /// Returns the number of messages registered for the
228 /// active locale.
229 ///
230 /// OPT 22 (tail): borrows through [`I18N_MESSAGES`]
231 /// read guard instead of cloning the whole table.
232 ///
233 /// # Returns
234 ///
235 /// - `usize` - Count of currently-registered messages.
236 pub fn active_message_count(&self) -> usize {
237 self.get_locale().with(|active: &String| {
238 let guard: std::sync::RwLockReadGuard<
239 'static,
240 HashMap<String, HashMap<String, String>>,
241 > = messages_lock().read().unwrap_or_else(
242 |e: std::sync::PoisonError<
243 std::sync::RwLockReadGuard<'static, HashMap<String, HashMap<String, String>>>,
244 >| e.into_inner(),
245 );
246 guard
247 .get(active.as_str())
248 .map(|m: &HashMap<String, String>| m.len())
249 .unwrap_or_default()
250 })
251 }
252}
253
254/// `I18n` is `Copy` because every remaining field is a
255/// `Signal`, which is already `Copy` — the registry hands
256/// out cheap `usize` addresses for any `T: Clone + PartialEq
257/// + 'static`.
258impl Copy for I18n {}