Skip to main content

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}