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