use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::RwLock;
type PluginStringsMap = HashMap<String, HashMap<String, HashMap<String, String>>>;
static PLUGIN_STRINGS: Lazy<RwLock<PluginStringsMap>> = Lazy::new(|| RwLock::new(HashMap::new()));
pub fn register_plugin_strings(
plugin_name: &str,
strings: HashMap<String, HashMap<String, String>>,
) {
let mut all_strings = PLUGIN_STRINGS.write().unwrap();
all_strings.insert(plugin_name.to_string(), strings);
}
pub fn translate_plugin_string(
plugin_name: &str,
key: &str,
args: &HashMap<String, String>,
) -> String {
let locale = current_locale();
let all_strings = PLUGIN_STRINGS.read().unwrap();
let plugin_map: &HashMap<String, HashMap<String, String>> = match all_strings.get(plugin_name) {
Some(m) => m,
None => {
tracing::debug!(
"translate_plugin_string: plugin '{}' not found (available: {:?}), returning key '{}'",
plugin_name,
all_strings.keys().collect::<Vec<_>>(),
key
);
return key.to_string();
}
};
let lang_map: Option<&HashMap<String, String>> =
plugin_map.get(&locale).or_else(|| plugin_map.get("en"));
let template: &String = match lang_map.and_then(|m| m.get(key)) {
Some(t) => t,
None => {
tracing::debug!(
"translate_plugin_string: key '{}' not found for plugin '{}' (locale='{}', available keys: {:?})",
key,
plugin_name,
locale,
plugin_map.get(&locale).or_else(|| plugin_map.get("en")).map(|m| m.keys().take(5).collect::<Vec<_>>())
);
return key.to_string();
}
};
let mut result = template.clone();
for (k, v) in args {
result = result.replace(&format!("%{{{}}}", k), v);
}
result
}
pub fn unregister_plugin_strings(plugin_name: &str) {
let mut all_strings = PLUGIN_STRINGS.write().unwrap();
all_strings.remove(plugin_name);
}
pub fn init() {
let locale = detect_locale().unwrap_or_else(|| "en".to_string());
rust_i18n::set_locale(&locale);
}
pub fn init_with_config(config_locale: Option<&str>) {
let locale = if let Some(req_locale) = config_locale {
let supported = available_locales();
let req_lower = req_locale.replace('_', "-").to_lowercase();
let mut matched = None;
for &loc in &supported {
if loc.to_lowercase() == req_lower {
matched = Some(loc.to_string());
break;
}
}
matched.unwrap_or_else(|| req_locale.to_string())
} else {
detect_locale().unwrap_or_else(|| "en".to_string())
};
rust_i18n::set_locale(&locale);
}
fn detect_locale() -> Option<String> {
let env_locale = std::env::var("LC_ALL")
.or_else(|_| std::env::var("LC_MESSAGES"))
.or_else(|_| std::env::var("LANG"))
.ok()?;
if env_locale.is_empty() || env_locale == "C" || env_locale == "POSIX" {
return None;
}
let normalized = env_locale.replace('_', "-").to_lowercase();
let supported = available_locales();
for &loc in &supported {
if normalized.starts_with(&loc.to_lowercase()) {
return Some(loc.to_string());
}
}
let lang = env_locale.split(['_', '-', '.']).next()?;
if lang.is_empty() || lang == "C" || lang == "POSIX" {
None
} else {
Some(lang.to_lowercase())
}
}
pub fn current_locale() -> String {
rust_i18n::locale().to_string()
}
pub fn set_locale(locale: &str) {
rust_i18n::set_locale(locale);
}
pub fn available_locales() -> Vec<&'static str> {
rust_i18n::available_locales!()
}
pub fn locale_display_name(locale: &str) -> Option<(&'static str, &'static str)> {
match locale {
"cs" => Some(("Czech", "Čeština")),
"de" => Some(("German", "Deutsch")),
"en" => Some(("English", "English")),
"es" => Some(("Spanish", "Español")),
"fr" => Some(("French", "Français")),
"it" => Some(("Italian", "Italiano")),
"ja" => Some(("Japanese", "日本語")),
"ko" => Some(("Korean", "한국어")),
"pt-BR" => Some(("Portuguese (Brazil)", "Português (Brasil)")),
"ru" => Some(("Russian", "Русский")),
"th" => Some(("Thai", "ไทย")),
"uk" => Some(("Ukrainian", "Українська")),
"zh-CN" => Some(("Chinese (Simplified)", "简体中文")),
_ => None,
}
}
pub fn switched_to_project_message(path: &str) -> String {
rust_i18n::t!("file.switched_to_project", path = path).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_init_sets_locale() {
init();
let locale = current_locale();
assert!(!locale.is_empty());
}
#[test]
fn test_set_locale() {
set_locale("en");
assert_eq!(current_locale(), "en");
}
#[test]
fn test_locale_changed_interpolation() {
use rust_i18n::t;
set_locale("en");
let locale_name = "es";
let msg = t!("locale.changed", locale_name = locale_name).to_string();
assert_eq!(msg, "Locale changed to es");
}
#[test]
fn test_available_locales_includes_en() {
let locales = available_locales();
assert!(
locales.contains(&"en"),
"English locale should be available"
);
}
#[test]
fn test_all_locales_have_required_keys() {
use std::fs;
use std::path::Path;
let locales_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("locales");
let en_content =
fs::read_to_string(locales_dir.join("en.json")).expect("Failed to read en.json");
let en_json: serde_json::Value =
serde_json::from_str(&en_content).expect("Failed to parse en.json");
let en_keys: HashSet<_> = en_json
.as_object()
.expect("en.json should be an object")
.keys()
.filter(|k| !k.starts_with('_'))
.cloned()
.collect();
let locales = available_locales();
assert!(
locales.len() >= 2,
"Should have at least 2 locales (en and at least one other)"
);
for locale in &locales {
if *locale == "en" {
continue; }
let locale_file = locales_dir.join(format!("{}.json", locale));
let content = fs::read_to_string(&locale_file)
.unwrap_or_else(|_| panic!("Failed to read {}.json", locale));
let json: serde_json::Value = serde_json::from_str(&content)
.unwrap_or_else(|_| panic!("Failed to parse {}.json", locale));
let locale_keys: HashSet<_> = json
.as_object()
.unwrap_or_else(|| panic!("{}.json should be an object", locale))
.keys()
.filter(|k| !k.starts_with('_'))
.cloned()
.collect();
let missing: Vec<_> = en_keys.difference(&locale_keys).collect();
if !missing.is_empty() {
let mut missing_sorted: Vec<_> = missing.into_iter().collect();
missing_sorted.sort();
panic!(
"Locale '{}' is missing {} keys: {:?}",
locale,
missing_sorted.len(),
missing_sorted
);
}
let extra: Vec<_> = locale_keys.difference(&en_keys).collect();
if !extra.is_empty() {
let mut extra_sorted: Vec<_> = extra.into_iter().collect();
extra_sorted.sort();
eprintln!(
"Warning: Locale '{}' has {} extra keys not in English: {:?}",
locale,
extra_sorted.len(),
extra_sorted
);
}
}
}
}