Skip to main content

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 {}