use serde_json::{self, Value};
use std::collections::HashMap;
#[cfg(target_arch = "wasm32")]
use web_sys::window;
#[derive(Debug, Clone, PartialEq)]
pub struct I18nConfig {
pub translations: HashMap<&'static str, &'static str>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum StorageType {
#[default]
LocalStorage,
SessionStorage,
}
#[derive(Clone, PartialEq)]
pub struct I18n {
pub config: I18nConfig,
current_language: String,
translations: HashMap<String, Value>,
}
impl I18n {
pub fn new(config: I18nConfig, translations: HashMap<&str, &str>) -> Result<Self, String> {
let translations = Self::load_translations(translations)?;
let languages: Vec<&str> = translations
.keys()
.map(|arg: &String| String::as_str(arg))
.collect();
let current_language = languages
.first()
.cloned()
.ok_or_else(|| "You must add at least one supported language".to_string())?;
Ok(I18n {
config,
current_language: current_language.to_string(),
translations,
})
}
fn load_translations(
translations: HashMap<&str, &str>,
) -> Result<HashMap<String, Value>, String> {
let mut loaded_translations = HashMap::new();
let languages: Vec<&str> = translations.keys().copied().collect();
for language in &languages {
if let Some(json_str) = translations.get(language) {
let json: Value = serde_json::from_str(json_str)
.map_err(|err| format!("Invalid JSON for language {}: {}", language, err))?;
loaded_translations.insert(language.to_string(), json);
} else {
return Err(format!("Translation data for '{}' not found", language));
}
}
Ok(loaded_translations)
}
pub fn set_translation_language(
&mut self,
language: &str,
_storage_type: &StorageType,
_storage_name: &str,
) -> Result<(), String> {
let languages: Vec<&str> = self
.translations
.keys()
.map(|arg: &String| arg.as_str())
.collect();
if !languages.contains(&language) {
return Err(format!("Language '{}' is not supported", language));
}
self.current_language = language.to_string();
#[cfg(target_arch = "wasm32")]
{
let result = match _storage_type {
StorageType::LocalStorage => window()
.ok_or("No window available")?
.local_storage()
.map_err(|_| "Failed to access localStorage".to_string())?
.ok_or("localStorage not available")?
.set_item(_storage_name, language),
StorageType::SessionStorage => window()
.ok_or("No window available")?
.session_storage()
.map_err(|_| "Failed to access sessionStorage".to_string())?
.ok_or("sessionStorage not available")?
.set_item(_storage_name, language),
};
result.map_err(|_| {
format!(
"Failed to write to {}",
match _storage_type {
StorageType::LocalStorage => "LocalStorage",
StorageType::SessionStorage => "SessionStorage",
}
)
})?;
}
#[cfg(not(target_arch = "wasm32"))]
{
}
Ok(())
}
pub fn get_current_language(&self) -> &str {
&self.current_language
}
pub fn t(&self, key: &str) -> String {
let keys: Vec<&str> = key.split('.').collect();
let languages: Vec<&str> = self.config.translations.keys().copied().collect();
let first_language = languages[0];
self.translations
.get(&self.current_language)
.and_then(|language_json| Self::get_nested_value(language_json, &keys))
.or_else(|| {
self.translations
.get(first_language)
.and_then(|default_json| Self::get_nested_value(default_json, &keys))
})
.map_or_else(
|| {
format!(
"Key '{}' not found for language '{}'",
key, self.current_language
)
},
|value| match value {
Value::String(s) => s.clone(),
_ => value.to_string(),
},
)
}
fn get_nested_value<'a>(json: &'a Value, keys: &[&str]) -> Option<&'a Value> {
keys.iter().try_fold(json, |current, key| current.get(key))
}
}