use crate::compat::HashMap;
use crate::i18n::types::{ReloadEvent, TranslationFile};
use crossbeam_channel::Sender;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::time::SystemTime;
#[derive(Debug, Clone, PartialEq, Eq)]
struct FileFingerprint {
modified: Option<SystemTime>,
len: u64,
hash: u64,
}
impl FileFingerprint {
fn read(path: &std::path::Path) -> Option<Self> {
let metadata = std::fs::metadata(path).ok()?;
let modified = metadata.modified().ok();
let len = metadata.len();
let hash = std::fs::read(path).map(|bytes| fnv1a(&bytes)).unwrap_or(0);
Some(Self { modified, len, hash })
}
fn is_newer_than(&self, previous: &Self) -> bool {
if let (Some(now), Some(before)) = (self.modified, previous.modified) {
if now != before {
return now > before;
}
}
if self.len != previous.len {
return true;
}
self.hash != previous.hash
}
}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
pub struct I18nManager {
translations: HashMap<String, TranslationFile>,
current_language: String,
translation_paths: HashMap<String, PathBuf>,
file_fingerprints: HashMap<String, FileFingerprint>,
hot_reload_enabled: bool,
reload_sender: Option<Sender<ReloadEvent>>,
}
impl I18nManager {
pub fn new() -> Self {
Self {
translations: HashMap::new(),
current_language: "en".to_string(),
translation_paths: HashMap::new(),
file_fingerprints: HashMap::new(),
hot_reload_enabled: false,
reload_sender: None,
}
}
pub fn enable_hot_reload(&mut self, sender: Sender<ReloadEvent>) {
self.hot_reload_enabled = true;
self.reload_sender = Some(sender);
}
pub fn disable_hot_reload(&mut self) {
self.hot_reload_enabled = false;
self.reload_sender = None;
}
pub fn is_hot_reload_enabled(&self) -> bool {
self.hot_reload_enabled
}
pub fn reload_translation(&mut self, language: &str) -> Result<(), String> {
if let Some(path) = self.translation_paths.get(language) {
let mut file = File::open(path).map_err(|e| format!("Failed to open file: {e}"))?;
let mut content = String::new();
file.read_to_string(&mut content).map_err(|e| format!("Failed to read file: {e}"))?;
let translation_file: TranslationFile =
serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {e}"))?;
self.translations.insert(language.to_string(), translation_file);
if let Some(fingerprint) = FileFingerprint::read(path) {
self.file_fingerprints.insert(language.to_string(), fingerprint);
}
if let Some(ref sender) = self.reload_sender {
if let Err(e) = sender.send(ReloadEvent::TranslationReloaded {
language: language.to_string(),
timestamp: SystemTime::now(),
}) {
log::error!("[i18n] Failed to send reload event: {e:?}");
}
}
Ok(())
} else {
Err(format!("Translation file path not found for language: {language}"))
}
}
pub fn check_and_reload(&mut self) -> Vec<ReloadEvent> {
let mut events = Vec::new();
if !self.hot_reload_enabled {
return events;
}
let mut languages_to_reload: Vec<String> = Vec::new();
for (language, path) in self.translation_paths.iter() {
let Some(fingerprint) = FileFingerprint::read(path) else {
continue;
};
if let Some(previous) = self.file_fingerprints.get(language) {
if fingerprint.is_newer_than(previous) {
languages_to_reload.push(language.clone());
}
}
}
for language in languages_to_reload {
match self.reload_translation(&language) {
Ok(()) => {
events.push(ReloadEvent::TranslationReloaded {
language,
timestamp: SystemTime::now(),
});
}
Err(e) => {
events.push(ReloadEvent::ReloadError { language, error: e });
}
}
}
events
}
pub fn load_translations(&mut self, path: &str) -> Result<(), std::io::Error> {
let path_buf = PathBuf::from(path);
let mut file = File::open(&path_buf)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let translation_file: TranslationFile = serde_json::from_str(&content)?;
let language = translation_file.language.clone();
self.translations.insert(language.clone(), translation_file);
self.translation_paths.insert(language.clone(), path_buf.clone());
if let Some(fingerprint) = FileFingerprint::read(&path_buf) {
self.file_fingerprints.insert(language, fingerprint);
}
Ok(())
}
pub fn inject_translations(&mut self, language: String, file: TranslationFile) {
self.translations.insert(language, file);
}
pub fn set_language(&mut self, language: &str) {
self.current_language = language.to_string();
}
pub fn current_language(&self) -> &String {
&self.current_language
}
pub fn audit_keys(&self) -> Vec<String> {
let mut keys: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for file in self.translations.values() {
for key in file.translations.keys() {
keys.insert(key.clone());
}
}
keys.into_iter().collect()
}
pub fn translation_count(&self) -> usize {
self.translations.len()
}
pub fn translate(&self, key: &str) -> String {
self.translate_with_context(key, None, 1)
}
pub fn translate_with_context(&self, key: &str, context: Option<&str>, count: u32) -> String {
if let Some(translation_file) = self.translations.get(&self.current_language) {
if let Some(translation) = translation_file.translations.get(key) {
if let Some(ctx) = context {
if let Some(trans_ctx) = &translation.context {
if trans_ctx != ctx {
return key.to_string();
}
} else {
return key.to_string();
}
}
if let Some(plural) = &translation.plural {
if let Some(plural_form) = plural.get(&count) {
return plural_form.clone();
}
}
return translation.message.clone();
}
}
key.to_string()
}
}
impl Default for I18nManager {
fn default() -> Self {
Self::new()
}
}