use serde_json::Value; use std::collections::HashMap; use std::path::PathBuf; use tokio::fs; use tokio::sync::RwLock; use crate::domain::services::i18n_service::{I18nError, I18nResult, I18nService, Locale}; /// File system implementation of the I18nService pub struct FileSystemI18nService { /// Base directory containing translation files translations_dir: PathBuf, /// Cached translations (locale code -> JSON data) cache: RwLock>, } impl FileSystemI18nService { /// Create a dummy service for testing pub fn dummy() -> Self { Self { translations_dir: PathBuf::from("/tmp/dummy_translations"), cache: RwLock::new(HashMap::new()), } } /// Creates a new file system i18n service pub fn new(translations_dir: PathBuf) -> Self { Self { translations_dir, cache: RwLock::new(HashMap::new()), } } /// Get translation file path for a locale fn get_locale_file_path(&self, locale: Locale) -> PathBuf { self.translations_dir .join(format!("{}.json", locale.as_str())) } /// Get a nested key from JSON data fn get_nested_value(&self, data: &Value, key: &str) -> Option { let parts: Vec<&str> = key.split('.').collect(); let mut current = data; for part in &parts[0..parts.len() - 1] { if let Some(next) = current.get(part) { current = next; } else { return None; } } if let Some(last_part) = parts.last() && let Some(value) = current.get(last_part) && value.is_string() { return value.as_str().map(|s| s.to_string()); } None } } impl I18nService for FileSystemI18nService { async fn translate(&self, key: &str, locale: Locale) -> I18nResult { // Check if translations are cached { let cache = self.cache.read().await; if let Some(translations) = cache.get(&locale) { if let Some(value) = self.get_nested_value(translations, key) { return Ok(value); } // Try to use English as fallback if we couldn't find the key if locale != Locale::English && let Some(english_translations) = cache.get(&Locale::English) && let Some(value) = self.get_nested_value(english_translations, key) { return Ok(value); } return Err(I18nError::KeyNotFound(key.to_string())); } } // If not cached, load translations and try again self.load_translations(locale).await?; { let cache = self.cache.read().await; if let Some(translations) = cache.get(&locale) { if let Some(value) = self.get_nested_value(translations, key) { return Ok(value); } // Try to use English as fallback if locale != Locale::English && let Some(english_translations) = cache.get(&Locale::English) && let Some(value) = self.get_nested_value(english_translations, key) { return Ok(value); } } } Err(I18nError::KeyNotFound(key.to_string())) } async fn load_translations(&self, locale: Locale) -> I18nResult<()> { let file_path = self.get_locale_file_path(locale); tracing::info!( "Loading translations for locale {} from {:?}", locale.as_str(), file_path ); // Check if file exists if !file_path.exists() { return Err(I18nError::InvalidLocale(locale.as_str().to_string())); } // Read and parse file let content = fs::read_to_string(&file_path) .await .map_err(|e| I18nError::LoadError(format!("Failed to read translation file: {}", e)))?; let translations: Value = serde_json::from_str(&content).map_err(|e| { I18nError::LoadError(format!("Failed to parse translation file: {}", e)) })?; // Update cache { let mut cache = self.cache.write().await; cache.insert(locale, translations); } tracing::info!("Translations loaded for locale {}", locale.as_str()); Ok(()) } async fn available_locales(&self) -> Vec { vec![ Locale::English, Locale::Spanish, Locale::French, Locale::German, Locale::Portuguese, ] } async fn is_supported(&self, locale: Locale) -> bool { let file_path = self.get_locale_file_path(locale); file_path.exists() } }