Skip to content

Commit 751c83d

Browse files
committed
chore(i18n): vendor i18n-rs to unblock Yew upgrade
1 parent 27d0548 commit 751c83d

5 files changed

Lines changed: 559 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ If you need help, join the [Discord server][discord-invite] and ask for assistan
4040

4141
## License
4242

43-
This project is licensed under the [AGPLv3 License](https://github.com/Rustmail/rustmail/blob/main/LICENSE).
43+
This project is licensed under the [AGPLv3 License](https://github.com/Rustmail/rustmail/blob/main/LICENSE).
44+
45+
> **Note:** The `rustmail-panel` i18n module includes code derived from [i18n-rs](https://github.com/opensass/i18n-rs),
46+
> which is licensed under the MIT License.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Open SASS Core Maintainers
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

rustmail_panel/src/i18n/config.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// ---------------------------------------------------------------------------- //
2+
// Code integrated from: https://github.com/opensass/i18n-rs //
3+
// Original Author: opensass //
4+
// License: MIT //
5+
// Note: Internalized to allow Yew updates. //
6+
// ---------------------------------------------------------------------------- //
7+
8+
use serde_json::{self, Value};
9+
use std::collections::HashMap;
10+
#[cfg(target_arch = "wasm32")]
11+
use web_sys::window;
12+
13+
/// Configuration for the I18n module, specifying supported translations.
14+
#[derive(Debug, Clone, PartialEq)]
15+
pub struct I18nConfig {
16+
/// Mapping of language codes to raw JSON strings representing translation data.
17+
/// Example: `HashMap::from([("en", "{...}"), ("fr", "{...}")])`.
18+
pub translations: HashMap<&'static str, &'static str>,
19+
}
20+
21+
/// Enum representing browser storage options for persisting the selected language.
22+
#[derive(Debug, Clone, PartialEq, Default)]
23+
pub enum StorageType {
24+
/// Use the browser's `LocalStorage` for persisting data.
25+
#[default]
26+
LocalStorage,
27+
/// Use the browser's `SessionStorage` for persisting data.
28+
#[allow(dead_code)]
29+
SessionStorage,
30+
}
31+
32+
/// This struct represents the state and methods for managing internationalization.
33+
#[derive(Clone, PartialEq)]
34+
pub struct I18n {
35+
/// Configuration for I18n, specifying supported translations.
36+
pub config: I18nConfig,
37+
/// The current language code being used for translations.
38+
current_language: String,
39+
/// Translations loaded for each supported language, represented as a mapping from
40+
/// language codes to JSON structures (`serde_json::Value`).
41+
translations: HashMap<String, Value>,
42+
}
43+
44+
impl I18n {
45+
/// Initializes an `I18n` instance with a configuration and translations.
46+
///
47+
/// # Arguments
48+
/// - `config`: The `I18nConfig` containing supported translations map.
49+
/// - `translations`: A `HashMap` containing language codes as keys and JSON strings as values.
50+
///
51+
/// # Returns
52+
/// - `Ok(I18n)` if initialization is successful.
53+
/// - `Err(String)` if there is an error, such as missing translations or invalid JSON.
54+
pub fn new(config: I18nConfig, translations: HashMap<&str, &str>) -> Result<Self, String> {
55+
let translations = Self::load_translations(translations)?;
56+
57+
let languages: Vec<&str> = translations
58+
.keys()
59+
.map(|arg: &String| String::as_str(arg))
60+
.collect();
61+
62+
let current_language = languages
63+
.first()
64+
.cloned()
65+
.ok_or_else(|| "You must add at least one supported language".to_string())?;
66+
67+
Ok(I18n {
68+
config,
69+
current_language: current_language.to_string(),
70+
translations,
71+
})
72+
}
73+
74+
/// Loads translations for the given languages from a `HashMap` of raw JSON strings.
75+
///
76+
/// # Arguments
77+
/// - `translations`: A `HashMap` containing language codes as keys and JSON strings as values.
78+
///
79+
/// # Returns
80+
/// - `Ok(HashMap<String, Value>)` if all translations are valid.
81+
/// - `Err(String)` if any translation is missing or invalid.
82+
fn load_translations(
83+
translations: HashMap<&str, &str>,
84+
) -> Result<HashMap<String, Value>, String> {
85+
let mut loaded_translations = HashMap::new();
86+
let languages: Vec<&str> = translations.keys().copied().collect();
87+
88+
for language in &languages {
89+
if let Some(json_str) = translations.get(language) {
90+
let json: Value = serde_json::from_str(json_str)
91+
.map_err(|err| format!("Invalid JSON for language {}: {}", language, err))?;
92+
loaded_translations.insert(language.to_string(), json);
93+
} else {
94+
return Err(format!("Translation data for '{}' not found", language));
95+
}
96+
}
97+
98+
Ok(loaded_translations)
99+
}
100+
101+
/// Sets the translation language and stores it in the browser's storage.
102+
///
103+
/// # Arguments
104+
/// - `language`: The language code to set (e.g., `"en"`).
105+
/// - `storage_type`: The type of browser storage to use (`StorageType::LocalStorage` or `StorageType::SessionStorage`).
106+
/// - `storage_name`: The key to use for storing the selected language.
107+
///
108+
/// # Returns
109+
/// - `Ok(())` if the language was successfully set.
110+
/// - `Err(String)` if the language is not supported or storage fails.
111+
pub fn set_translation_language(
112+
&mut self,
113+
language: &str,
114+
_storage_type: &StorageType,
115+
_storage_name: &str,
116+
) -> Result<(), String> {
117+
let languages: Vec<&str> = self
118+
.translations
119+
.keys()
120+
.map(|arg: &String| arg.as_str())
121+
.collect();
122+
123+
if !languages.contains(&language) {
124+
return Err(format!("Language '{}' is not supported", language));
125+
}
126+
127+
self.current_language = language.to_string();
128+
129+
#[cfg(target_arch = "wasm32")]
130+
{
131+
let result = match _storage_type {
132+
StorageType::LocalStorage => window()
133+
.ok_or("No window available")?
134+
.local_storage()
135+
.map_err(|_| "Failed to access localStorage".to_string())?
136+
.ok_or("localStorage not available")?
137+
.set_item(_storage_name, language),
138+
StorageType::SessionStorage => window()
139+
.ok_or("No window available")?
140+
.session_storage()
141+
.map_err(|_| "Failed to access sessionStorage".to_string())?
142+
.ok_or("sessionStorage not available")?
143+
.set_item(_storage_name, language),
144+
};
145+
146+
result.map_err(|_| {
147+
format!(
148+
"Failed to write to {}",
149+
match _storage_type {
150+
StorageType::LocalStorage => "LocalStorage",
151+
StorageType::SessionStorage => "SessionStorage",
152+
}
153+
)
154+
})?;
155+
}
156+
157+
#[cfg(not(target_arch = "wasm32"))]
158+
{
159+
// TODO: Add support for native
160+
}
161+
162+
Ok(())
163+
}
164+
165+
/// Retrieves the current language code.
166+
///
167+
/// # Returns
168+
/// - A reference to the current language code as a `&str`.
169+
pub fn get_current_language(&self) -> &str {
170+
&self.current_language
171+
}
172+
173+
/// Translates a given key using the current language.
174+
///
175+
/// # Arguments
176+
/// - `key`: The translation key to retrieve (e.g., `"menu.file.open"`).
177+
///
178+
/// # Returns
179+
/// - The translated string if the key exists.
180+
/// - A fallback message if the key or translation does not exist.
181+
pub fn t(&self, key: &str) -> String {
182+
let keys: Vec<&str> = key.split('.').collect();
183+
let languages: Vec<&str> = self.config.translations.keys().copied().collect();
184+
185+
let first_language = languages[0];
186+
187+
self.translations
188+
.get(&self.current_language)
189+
.and_then(|language_json| Self::get_nested_value(language_json, &keys))
190+
.or_else(|| {
191+
self.translations
192+
.get(first_language)
193+
.and_then(|default_json| Self::get_nested_value(default_json, &keys))
194+
})
195+
.map_or_else(
196+
|| {
197+
format!(
198+
"Key '{}' not found for language '{}'",
199+
key, self.current_language
200+
)
201+
},
202+
|value| match value {
203+
Value::String(s) => s.clone(),
204+
_ => value.to_string(),
205+
},
206+
)
207+
}
208+
209+
/// Retrieves a nested value from a JSON object using a sequence of keys.
210+
///
211+
/// # Arguments
212+
/// - `json`: The root `serde_json::Value` object to search within.
213+
/// - `keys`: A slice of keys representing the path to the desired value.
214+
///
215+
/// # Returns
216+
/// - `Some(&Value)` if the value exists at the specified path.
217+
/// - `None` if the path does not exist.
218+
fn get_nested_value<'a>(json: &'a Value, keys: &[&str]) -> Option<&'a Value> {
219+
keys.iter().try_fold(json, |current, key| current.get(key))
220+
}
221+
}

rustmail_panel/src/i18n/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// ---------------------------------------------------------------------------- //
2+
// Code integrated from: https://github.com/opensass/i18n-rs //
3+
// Original Author: opensass //
4+
// License: MIT //
5+
// Note: Internalized to allow Yew updates. //
6+
// ---------------------------------------------------------------------------- //
7+
8+
pub mod yew;
9+
10+
pub mod config;

0 commit comments

Comments
 (0)