157 lines
4.8 KiB
Rust
157 lines
4.8 KiB
Rust
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<HashMap<Locale, Value>>,
|
|
}
|
|
|
|
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<String> {
|
|
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<String> {
|
|
// 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<Locale> {
|
|
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()
|
|
}
|
|
}
|