Files
Oxicloud/src/infrastructure/services/file_system_i18n_service.rs
T
2026-04-01 23:14:42 +02:00

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()
}
}