ciphern 0.2.1

Enterprise-grade cryptographic library
Documentation
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::fs;
use std::sync::{Mutex, RwLock};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum I18nError {
    #[error("Translation key not found: {key}")]
    KeyNotFound { key: String },
    #[error("Invalid locale: {locale}")]
    InvalidLocale { locale: String },
    #[error("Missing translation for key: {key} in locale: {locale}")]
    MissingTranslation { key: String, locale: String },
    #[error("Failed to load translation file: {source}")]
    LoadError { source: std::io::Error },
    #[error("Failed to parse translation file: {source}")]
    ParseError { source: toml::de::Error },
}

type TranslationMap = HashMap<String, String>;
type LocaleData = HashMap<String, TranslationMap>;
type AllTranslations = HashMap<String, LocaleData>;

thread_local! {
    static CURRENT_LOCALE: std::cell::RefCell<String> = std::cell::RefCell::new("en".to_string());
}

static TRANSLATIONS: Lazy<RwLock<AllTranslations>> = Lazy::new(|| RwLock::new(HashMap::new()));

static TEST_MUTEX: Mutex<()> = Mutex::new(());

#[cfg(test)]
fn with_test_mutex<F, R>(f: F) -> R
where
    F: FnOnce() -> R,
{
    let _guard = TEST_MUTEX.lock().unwrap();
    f()
}

#[cfg(not(test))]
fn with_test_mutex<F, R>(f: F) -> R
where
    F: FnOnce() -> R,
{
    f()
}

fn find_locale_path() -> Option<std::path::PathBuf> {
    let possible_paths = vec![
        std::path::PathBuf::from("locales"),
        std::path::PathBuf::from("../locales"),
    ];

    if let Ok(cwd) = std::env::current_dir() {
        possible_paths
            .iter()
            .chain(std::iter::once(&cwd.join("locales")))
            .find(|path| path.exists() && path.is_dir())
            .cloned()
    } else {
        possible_paths
            .into_iter()
            .find(|path| path.exists() && path.is_dir())
    }
}

fn load_locale_file(locale: &str) -> Result<(), I18nError> {
    let locale_path = find_locale_path().ok_or_else(|| I18nError::LoadError {
        source: std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "Locales directory not found".to_string(),
        ),
    })?;

    let path = locale_path.join(format!("{}.toml", locale));
    if !path.exists() {
        return Err(I18nError::LoadError {
            source: std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("File not found: {:?}", path),
            ),
        });
    }

    let content = fs::read_to_string(&path).map_err(|e| I18nError::LoadError { source: e })?;

    let toml_value: toml::Value =
        toml::from_str(&content).map_err(|e| I18nError::ParseError { source: e })?;

    let mut locale_data = HashMap::<String, HashMap<String, String>>::new();

    if let toml::Value::Table(table) = toml_value {
        for (section, value) in table {
            if let toml::Value::Table(fields) = value {
                let mut section_map = HashMap::<String, String>::new();
                for (key, val) in fields {
                    let s = match val {
                        toml::Value::String(s) => s,
                        toml::Value::Integer(i) => i.to_string(),
                        toml::Value::Float(f) => f.to_string(),
                        toml::Value::Boolean(b) => b.to_string(),
                        _ => continue,
                    };
                    section_map.insert(key, s);
                }
                locale_data.insert(section, section_map);
            }
        }
    }

    let mut translations_guard = TRANSLATIONS.write().unwrap();
    translations_guard.insert(locale.to_string(), locale_data);

    Ok(())
}

fn ensure_locale_loaded(locale: &str) {
    let translations = TRANSLATIONS.read().unwrap();
    if !translations.contains_key(locale) {
        drop(translations);
        let _ = load_locale_file(locale);
    }
}

pub fn set_locale(locale: &str) {
    with_test_mutex(|| {
        let locale_to_set = if ["en", "zh"].contains(&locale) {
            locale.to_string()
        } else {
            "en".to_string()
        };

        ensure_locale_loaded(&locale_to_set);

        CURRENT_LOCALE.with(|cell| {
            *cell.borrow_mut() = locale_to_set;
        });
    })
}

pub fn get_locale() -> String {
    CURRENT_LOCALE.with(|cell| cell.borrow().clone())
}

pub fn translate(key: &str) -> String {
    let locale = get_locale();
    translate_with_locale(key, &locale).unwrap_or_else(|_| format!("[{}]", key))
}

pub fn translate_with_locale(key: &str, locale: &str) -> Result<String, I18nError> {
    with_test_mutex(|| {
        ensure_locale_loaded(locale);

        let translations = TRANSLATIONS.read().unwrap();
        let locale_data = translations
            .get(locale)
            .ok_or_else(|| I18nError::InvalidLocale {
                locale: locale.to_string(),
            })?;

        let parts: Vec<&str> = key.split('.').collect();

        if parts.len() == 2 {
            let section = parts[0];
            let field = parts[1];
            if let Some(section_map) = locale_data.get(section) {
                if let Some(translation) = section_map.get(field) {
                    return Ok(translation.clone());
                }
            }
        }

        Err(I18nError::KeyNotFound {
            key: key.to_string(),
        })
    })
}

pub fn translate_with_args(key: &str, args: &[(&str, &str)]) -> String {
    let locale = get_locale();
    let result = translate_with_locale(key, &locale);
    match result {
        Ok(mut translation) => {
            for &(name, value) in args {
                translation = translation.replace(&format!("{{{}}}", name), value);
            }
            translation
        }
        Err(_) => format!("[{}]", key),
    }
}

pub fn translate_safe(key: &str) -> Result<String, I18nError> {
    let locale = get_locale();
    let translation = translate_with_locale(key, &locale)?;
    if translation.starts_with("[") && translation.ends_with("]") {
        Err(I18nError::MissingTranslation {
            key: key.to_string(),
            locale,
        })
    } else {
        Ok(translation)
    }
}

pub fn is_locale_supported(locale: &str) -> bool {
    ["en", "zh"].contains(&locale)
}

pub fn get_supported_locales() -> Vec<&'static str> {
    vec!["en", "zh"]
}

#[allow(dead_code)]
pub fn preload_all_locales() {
    for &locale in get_supported_locales().iter() {
        ensure_locale_loaded(locale);
    }
}

pub fn reset_for_testing() {
    let _guard = TEST_MUTEX.lock().unwrap();

    CURRENT_LOCALE.with(|cell| {
        *cell.borrow_mut() = "en".to_string();
    });

    let mut translations = TRANSLATIONS.write().unwrap();
    translations.clear();

    drop(translations);
    for &locale in get_supported_locales().iter() {
        let _ = load_locale_file(locale);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_set_locale() {
        set_locale("en");
        assert_eq!(get_locale(), "en");
        set_locale("zh");
        assert_eq!(get_locale(), "zh");
    }

    #[test]
    fn test_set_invalid_locale_falls_back_to_en() {
        set_locale("invalid");
        assert_eq!(get_locale(), "en");
    }

    #[test]
    fn test_is_locale_supported() {
        assert!(is_locale_supported("en"));
        assert!(is_locale_supported("zh"));
        assert!(!is_locale_supported("ja"));
        assert!(!is_locale_supported("fr"));
    }

    #[test]
    fn test_get_supported_locales() {
        let locales = get_supported_locales();
        assert_eq!(locales.len(), 2);
        assert!(locales.contains(&"en"));
        assert!(locales.contains(&"zh"));
    }

    #[test]
    fn test_translate_returns_string() {
        set_locale("en");
        let result = translate("greeting.hello");
        assert!(!result.is_empty());
    }

    #[test]
    fn test_translate_with_missing_key() {
        set_locale("en");
        let result = translate("nonexistent.key");
        assert!(result.contains("[nonexistent.key]"));
    }
}