1use std::borrow::Cow;
2
3use rust_embed::RustEmbed;
4
5use crate::I18nEmbedError;
6
7pub trait I18nAssets {
9 fn get_files(&self, file_path: &str) -> Vec<Cow<'_, [u8]>>;
13 fn filenames_iter(&self) -> Box<dyn Iterator<Item = String> + '_>;
16 fn subscribe_changed(
23 &self,
24 #[allow(unused_variables)] changed: std::sync::Arc<dyn Fn() + Send + Sync + 'static>,
25 ) -> Result<Box<dyn Watcher + Send + Sync + 'static>, I18nEmbedError> {
26 Ok(Box::new(()))
27 }
28}
29
30impl Watcher for () {}
31
32impl<T> I18nAssets for T
33where
34 T: RustEmbed,
35{
36 fn get_files(&self, file_path: &str) -> Vec<Cow<'_, [u8]>> {
37 Self::get(file_path)
38 .map(|file| file.data)
39 .into_iter()
40 .collect()
41 }
42
43 fn filenames_iter(&self) -> Box<dyn Iterator<Item = String>> {
44 Box::new(Self::iter().map(|filename| filename.to_string()))
45 }
46
47 #[allow(unused_variables)]
48 fn subscribe_changed(
49 &self,
50 changed: std::sync::Arc<dyn Fn() + Send + Sync + 'static>,
51 ) -> Result<Box<dyn Watcher + Send + Sync + 'static>, I18nEmbedError> {
52 Ok(Box::new(()))
53 }
54}
55
56#[cfg(feature = "autoreload")]
62#[derive(Debug)]
63pub struct RustEmbedNotifyAssets<T: rust_embed::RustEmbed> {
64 base_dir: std::path::PathBuf,
65 embed: core::marker::PhantomData<T>,
66}
67
68#[cfg(feature = "autoreload")]
69impl<T: rust_embed::RustEmbed> RustEmbedNotifyAssets<T> {
70 pub fn new(base_dir: impl Into<std::path::PathBuf>) -> Self {
72 Self {
73 base_dir: base_dir.into(),
74 embed: core::marker::PhantomData,
75 }
76 }
77}
78
79#[cfg(feature = "autoreload")]
80impl<T> I18nAssets for RustEmbedNotifyAssets<T>
81where
82 T: RustEmbed,
83{
84 fn get_files(&self, file_path: &str) -> Vec<Cow<'_, [u8]>> {
85 T::get(file_path)
86 .map(|file| file.data)
87 .into_iter()
88 .collect()
89 }
90
91 fn filenames_iter(&self) -> Box<dyn Iterator<Item = String>> {
92 Box::new(T::iter().map(|filename| filename.to_string()))
93 }
94
95 fn subscribe_changed(
96 &self,
97 changed: std::sync::Arc<dyn Fn() + Send + Sync + 'static>,
98 ) -> Result<Box<dyn Watcher + Send + Sync + 'static>, I18nEmbedError> {
99 let base_dir = &self.base_dir;
100 if base_dir.is_dir() {
101 log::debug!("Watching for changed files in {:?}", self.base_dir);
102 notify_watcher(base_dir, changed).map_err(Into::into)
103 } else {
104 log::debug!("base_dir {base_dir:?} does not yet exist, unable to watch for changes");
105 Ok(Box::new(()))
106 }
107 }
108}
109
110#[cfg(feature = "filesystem-assets")]
113#[derive(Debug)]
114pub struct FileSystemAssets {
115 base_dir: std::path::PathBuf,
116 #[cfg(feature = "autoreload")]
117 notify_changes_enabled: bool,
118}
119
120#[cfg(feature = "filesystem-assets")]
121impl FileSystemAssets {
122 pub fn try_new<P: Into<std::path::PathBuf>>(base_dir: P) -> Result<Self, I18nEmbedError> {
125 let base_dir = base_dir.into();
126
127 if !base_dir.exists() {
128 return Err(I18nEmbedError::DirectoryDoesNotExist(base_dir));
129 }
130
131 if !base_dir.is_dir() {
132 return Err(I18nEmbedError::PathIsNotDirectory(base_dir));
133 }
134
135 Ok(Self {
136 base_dir,
137 #[cfg(feature = "autoreload")]
138 notify_changes_enabled: false,
139 })
140 }
141
142 #[cfg(feature = "autoreload")]
144 pub fn notify_changes_enabled(mut self, enabled: bool) -> Self {
145 self.notify_changes_enabled = enabled;
146 self
147 }
148}
149
150#[cfg(feature = "autoreload")]
154#[derive(Debug)]
155pub struct NotifyError(notify::Error);
156
157#[cfg(feature = "autoreload")]
158impl From<notify::Error> for NotifyError {
159 fn from(value: notify::Error) -> Self {
160 Self(value)
161 }
162}
163
164#[cfg(feature = "autoreload")]
165impl From<notify::Error> for I18nEmbedError {
166 fn from(value: notify::Error) -> Self {
167 Self::Notify(value.into())
168 }
169}
170
171#[cfg(feature = "autoreload")]
172impl std::fmt::Display for NotifyError {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 self.0.fmt(f)
175 }
176}
177
178#[cfg(feature = "autoreload")]
179impl std::error::Error for NotifyError {}
180
181#[cfg(feature = "autoreload")]
182fn notify_watcher(
183 base_dir: &std::path::Path,
184 changed: std::sync::Arc<dyn Fn() + Send + Sync + 'static>,
185) -> notify::Result<Box<dyn Watcher + Send + Sync + 'static>> {
186 let mut watcher = notify::recommended_watcher(move |event_result| {
187 let event: notify::Event = match event_result {
188 Ok(event) => event,
189 Err(error) => {
190 log::error!("{error}");
191 return;
192 }
193 };
194 match event.kind {
195 notify::EventKind::Any
196 | notify::EventKind::Create(_)
197 | notify::EventKind::Modify(_)
198 | notify::EventKind::Remove(_)
199 | notify::EventKind::Other => changed(),
200 _ => {}
201 }
202 })?;
203
204 notify::Watcher::watch(&mut watcher, base_dir, notify::RecursiveMode::Recursive)?;
205
206 Ok(Box::new(watcher))
207}
208
209pub trait Watcher {}
214
215#[cfg(feature = "autoreload")]
216impl Watcher for notify::RecommendedWatcher {}
217
218#[cfg(feature = "filesystem-assets")]
219impl I18nAssets for FileSystemAssets {
220 fn get_files(&self, file_path: &str) -> Vec<Cow<'_, [u8]>> {
221 let full_path = self.base_dir.join(file_path);
222
223 if !(full_path.is_file() && full_path.exists()) {
224 return Vec::new();
225 }
226
227 match std::fs::read(full_path) {
228 Ok(contents) => vec![Cow::from(contents)],
229 Err(e) => {
230 log::error!(
231 target: "i18n_embed::assets",
232 "Unexpected error while reading localization asset file: {}",
233 e);
234 Vec::new()
235 }
236 }
237 }
238
239 fn filenames_iter(&self) -> Box<dyn Iterator<Item = String>> {
240 Box::new(
241 walkdir::WalkDir::new(&self.base_dir)
242 .into_iter()
243 .filter_map(|f| match f {
244 Ok(f) => {
245 if f.file_type().is_file() {
246 match f.file_name().to_str() {
247 Some(filename) => Some(filename.to_string()),
248 None => {
249 log::error!(
250 target: "i18n_embed::assets",
251 "Filename {:?} is not valid UTF-8.",
252 f.file_name());
253 None
254 }
255 }
256 } else {
257 None
258 }
259 }
260 Err(err) => {
261 log::error!(
262 target: "i18n_embed::assets",
263 "Unexpected error while gathering localization asset filenames: {}",
264 err);
265 None
266 }
267 }),
268 )
269 }
270
271 #[cfg(feature = "autoreload")]
274 fn subscribe_changed(
275 &self,
276 changed: std::sync::Arc<dyn Fn() + Send + Sync + 'static>,
277 ) -> Result<Box<dyn Watcher + Send + Sync + 'static>, I18nEmbedError> {
278 if self.notify_changes_enabled {
279 notify_watcher(&self.base_dir, changed).map_err(Into::into)
280 } else {
281 Ok(Box::new(()))
282 }
283 }
284}
285
286pub struct AssetsMultiplexor {
288 assets: Vec<Box<dyn I18nAssets + Send + Sync + 'static>>,
290}
291
292impl std::fmt::Debug for AssetsMultiplexor {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 f.debug_struct("AssetsMultiplexor")
295 .field(
296 "assets",
297 &self.assets.iter().map(|_| "<ASSET>").collect::<Vec<_>>(),
298 )
299 .finish()
300 }
301}
302
303impl AssetsMultiplexor {
304 pub fn new(
307 assets: impl IntoIterator<Item = Box<dyn I18nAssets + Send + Sync + 'static>>,
308 ) -> Self {
309 Self {
310 assets: assets.into_iter().collect(),
311 }
312 }
313}
314
315#[allow(dead_code)] struct Watchers(Vec<Box<dyn Watcher + Send + Sync + 'static>>);
317
318impl Watcher for Watchers {}
319
320impl I18nAssets for AssetsMultiplexor {
321 fn get_files(&self, file_path: &str) -> Vec<Cow<'_, [u8]>> {
322 self.assets
323 .iter()
324 .flat_map(|assets| assets.get_files(file_path))
325 .collect()
326 }
327
328 fn filenames_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
329 Box::new(
330 self.assets
331 .iter()
332 .flat_map(|assets| assets.filenames_iter()),
333 )
334 }
335
336 fn subscribe_changed(
337 &self,
338 changed: std::sync::Arc<dyn Fn() + Send + Sync + 'static>,
339 ) -> Result<Box<dyn Watcher + Send + Sync + 'static>, I18nEmbedError> {
340 let watchers: Vec<_> = self
341 .assets
342 .iter()
343 .map(|assets| assets.subscribe_changed(changed.clone()))
344 .collect::<Result<_, _>>()?;
345 Ok(Box::new(Watchers(watchers)))
346 }
347}