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