use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use crate::core::Reloadable;
use crate::error::I18nError;
pub struct HotReloader {
_watcher: RecommendedWatcher,
_thread: thread::JoinHandle<()>,
}
impl std::fmt::Debug for HotReloader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HotReloader").finish_non_exhaustive()
}
}
impl HotReloader {
pub fn watch(dir: impl AsRef<Path>, backend: Arc<dyn Reloadable>) -> Result<Self, I18nError> {
let dir = dir
.as_ref()
.canonicalize()
.map_err(|e| I18nError::IoError {
path: dir.as_ref().to_path_buf(),
source: e,
})?;
if !dir.is_dir() {
return Err(I18nError::IoError {
path: dir.clone(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"locale directory not found",
),
});
}
let extension = backend.file_extension().to_string();
let (tx, rx) = std::sync::mpsc::channel::<Event>();
let mut watcher = RecommendedWatcher::new(
move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
let _ = tx.send(event);
}
},
Config::default(),
)
.map_err(|e| I18nError::WatchError(e.to_string()))?;
watcher
.watch(&dir, RecursiveMode::NonRecursive)
.map_err(|e| I18nError::WatchError(e.to_string()))?;
let watch_dir = dir;
let handle = thread::spawn(move || {
while let Ok(event) = rx.recv() {
let mut pending: HashSet<PathBuf> = HashSet::new();
collect_paths(&event, &extension, &mut pending);
while let Ok(event) = rx.recv_timeout(Duration::from_millis(200)) {
collect_paths(&event, &extension, &mut pending);
}
for path in &pending {
reload_file(&watch_dir, &*backend, path);
}
}
});
Ok(Self {
_watcher: watcher,
_thread: handle,
})
}
}
fn collect_paths(event: &Event, extension: &str, out: &mut HashSet<PathBuf>) {
let relevant = matches!(
event.kind,
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
);
if !relevant {
return;
}
for path in &event.paths {
if path.extension().and_then(|e| e.to_str()) == Some(extension) {
out.insert(path.clone());
}
}
}
fn reload_file(dir: &Path, backend: &dyn Reloadable, path: &Path) {
let locale = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
if !path.starts_with(dir) {
return;
}
match std::fs::read_to_string(path) {
Ok(content) => match backend.reload_from_str(locale, &content) {
Ok(()) => tracing::info!(locale, path = %path.display(), "hot-reloaded locale"),
Err(e) => {
tracing::warn!(locale, error = %e, "failed to parse locale file on hot-reload")
}
},
Err(e) => {
if path.exists() {
tracing::warn!(path = %path.display(), error = %e, "failed to read locale file");
}
}
}
}