localize/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    borrow::Cow,
5    collections::HashMap,
6    fs,
7    marker::PhantomPinned,
8    path::{Path, PathBuf},
9};
10
11macro_rules! include_mods {
12    ($($mod:ident),*) => {
13        $(
14            mod $mod;
15            #[allow(unused_imports)]
16            pub use $mod::*;
17        )*
18    };
19}
20
21include_mods!(cache, error);
22
23pub type Locale = json::Value;
24pub(crate) type Sources = HashMap<String, PathBuf>;
25
26/// Heart and soul of this crated. Intended to be initialized once.
27pub struct Localizer<'this> {
28    locales: Sources,
29    // cache contains references to `locales`
30    cache: LocaleCache<'this>,
31    default: Option<&'static str>,
32    debug: bool,
33    _pin: PhantomPinned,
34}
35
36impl<'this> Localizer<'this> {
37    /// Creates new [`Localizer`] and searches for available locales in specified folder.
38    pub fn new(locales_path: impl AsRef<Path>) -> Self {
39        let locales = walkdir::WalkDir::new(locales_path)
40            .into_iter()
41            .filter_map(|de| match de {
42                Ok(de)
43                    if de.file_type().is_file()
44                        && de.file_name().to_string_lossy().ends_with(".json") =>
45                {
46                    log::trace!("Found locale: `{:?}`", de.path());
47                    Some((
48                        de.file_name()
49                            .to_str()
50                            .unwrap()
51                            .split_once(".json")
52                            .unwrap()
53                            .0
54                            .into(),
55                        de.path().to_path_buf(),
56                    ))
57                }
58                _ => None,
59            })
60            .collect();
61
62        Self {
63            locales,
64            cache: LocaleCache::new(),
65            debug: false,
66            _pin: PhantomPinned::default(),
67            default: None,
68        }
69    }
70
71    /// Loads all available locales into internal cache.
72    #[must_use]
73    pub fn precache_all(mut self) -> Self {
74        for (s, _) in unsafe { std::mem::transmute::<_, &'_ Sources>(&self.locales) }.iter() {
75            self.cache.prefer_recache(s, &self.locales).unwrap();
76        }
77        self
78    }
79
80    /// Enables debug mode.
81    /// This mostly means that [`Localizer`] will prefer fetching locales from disk rather than cache.
82    #[must_use]
83    #[inline]
84    pub fn debug(mut self, enable: bool) -> Self {
85        self.debug = enable;
86        self
87    }
88
89    pub fn default_locale(mut self, default_locale: &'static str) -> Self {
90        self.default = Some(default_locale);
91        self.cache.1 = Some(default_locale);
92        self
93    }
94
95    /// # Normal mode
96    /// Looks up cache for tag. If not found fetches locale from disk and **overwrites** old value in cache,
97    /// returns a reference to new locale afterwards. Otherwise returns cached value.
98    /// # Debug mode
99    /// Looks up available locales on disk for specified tag. If found, overwrites locale in cache with value from disk and returns a reference to it.
100    /// Otherwise tries to fetch the locale from cache.
101    pub fn localize<'w>(&mut self, tag: &'w str) -> Result<&Locale>
102    where
103        'w: 'this,
104    {
105        if self.debug {
106            self.cache.prefer_recache(tag, &self.locales)
107        } else {
108            self.cache.look_up_or_cache(tag, &self.locales)
109        }
110    }
111
112    /// # Normal mode
113    /// Looks up cache for tag. If not found fetches locale from disk and returns borrowed value of it.
114    /// Otherwise returns cached value.
115    /// # Debug mode
116    /// Looks up available locales for specified tag. If found fetches locale from disk and returns owned value of it.
117    /// Otherwise returns an error.
118    pub fn localize_no_cache<'w>(&self, tag: &'w str) -> Result<Cow<Locale>>
119    where
120        'w: 'this,
121    {
122        if self.debug {
123            log::trace!("Running debug mode, fetching locale `{}` from disk", tag);
124
125            if let Some(locale) = self.locales.get(tag) {
126                log::trace!("Found locale `{}` in sources, fetching from disk.", tag);
127                
128                Ok(
129                    Cow::Owned(
130                        json::from_reader(fs::File::open(locale)?)?
131                    )
132                )
133            } else if let Some(default_tag) = self.default {
134                log::trace!("Locale `{}` missing in sources, falling back to default locale `{}`", tag,default_tag);
135
136                Ok(
137                    Cow::Owned(
138                        json::from_reader(fs::File::open(
139                            self.locales.get(default_tag).ok_or(
140                                LocaleError::FallbackToDefaultFailed
141                            )?
142                        )?)?
143                    )
144                )
145            } else {
146                log::error!("Locale `{}` wasn't found in sources, and default was `None`. Aborted.", tag);
147
148                Err(LocaleError::SourceLookupFailed)
149            }
150        } else {
151            self.cache.look_up_or_fetch(tag, &self.locales)
152        }
153    }
154}