tiny_web/sys/
lang.rs

1use std::{
2    collections::{btree_map::Entry, BTreeMap},
3    fs::{read_dir, read_to_string},
4    path::PathBuf,
5    sync::Arc,
6};
7
8#[cfg(debug_assertions)]
9use std::time::SystemTime;
10
11use crate::fnv1a_64;
12use tiny_web_macro::fnv1a_64 as m_fnv1a_64;
13
14#[cfg(debug_assertions)]
15use tokio::fs;
16
17use toml::{Table, Value};
18
19use super::{data::Data, dbs::adapter::DB, log::Log};
20
21/// Describes a language element
22///
23/// # Values
24///
25/// * `id: i64` - Language ID.
26/// * `lang: String` - Languane name ISO 639-1: uk - ukrainian, en - english, en - english.
27/// * `code: String` - Languane code ISO 3166 alpha-2: ua - Ukraine, us - USA, gb - United Kingdom.
28/// * `name: String` - Native name of the language.
29/// * `index: i64` - Index in JSON type field db.
30#[derive(Debug, Clone)]
31pub struct LangItem {
32    /// Language ID
33    pub id: i64,
34    /// Languane name ISO 639-1: uk - ukrainian, en - english, en - english
35    pub code: String,
36    /// Native name of the language
37    pub name: String,
38    /// Index in JSON type field db
39    pub index: i64,
40}
41
42/// I18n
43///
44/// # Index
45///
46/// * 1 - language ID
47/// * 2 - Module ID
48/// * 3 - Class ID
49/// * 4 - Key ID
50/// * 5 - Key value
51type LangList = BTreeMap<i64, BTreeMap<i64, BTreeMap<i64, Arc<BTreeMap<i64, String>>>>>;
52
53/// Descrives all languages
54///
55/// # Values
56///
57/// * `langs: Vec<LangItem>` - List of languages
58/// * `list: LangList` - List of translations
59/// * `default: usize` - Default language
60#[derive(Debug)]
61pub struct Lang {
62    /// List of languages
63    pub langs: Arc<Vec<Arc<LangItem>>>,
64    /// List of translations
65    pub list: Arc<LangList>,
66    /// Default language
67    pub default: usize,
68    /// SystemTime last modification
69    #[cfg(debug_assertions)]
70    last: SystemTime,
71    /// Sum of all filename hashes
72    #[cfg(debug_assertions)]
73    hash: i128,
74    /// Path to langs' files
75    #[cfg(debug_assertions)]
76    pub(crate) root: Arc<String>,
77    /// List of lang codes
78    codes: BTreeMap<String, i64>,
79}
80
81impl Lang {
82    /// Reads ./app/ and recognizes translations
83    ///
84    /// # Description
85    ///
86    /// In the root directory of the project (`Init::root_path`) the `app` directory is searched.  
87    ///
88    /// Translation files are logically located in this directory.  
89    /// Each file must be named `LangItem::lang` and have the extension `.lang`
90    ///
91    /// ## Example:
92    ///
93    /// * English:   ./app/module_name/class_name/en.lang
94    /// * Ukrainian: ./app/module_name/class_name/uk.lang
95    ///
96    /// module_name - Name of the module  <br />
97    /// class_name - Class name  
98    ///
99    /// For all controllers in the same class - one translation file in one language is used.
100    ///
101    /// Each translation file is divided into lines.  
102    /// Each line consists of a key and a translation.  
103    ///
104    /// ## Example:
105    ///
106    /// `en.lang`<br />
107    /// about=About<br />
108    /// articles=Articles<br />
109    /// article=Article<br />
110    /// contact=Contact<br />
111    /// terms=Terms Conditions<br />
112    /// policy=Privacy Policy<br />
113    ///
114    /// ## Use in the controller:
115    ///
116    /// To get a translation, it is enough to set the `this.lang("contact")` function,
117    /// which will return the corresponding translation.<br />
118    /// If no translation is found, the key will be returned.
119    pub async fn new(root: Arc<String>, default_lang: &str, db: &mut DB) -> Lang {
120        #[cfg(debug_assertions)]
121        let last_time = SystemTime::UNIX_EPOCH;
122        let mut codes = BTreeMap::new();
123
124        let files = Lang::get_files(Arc::clone(&root)).await;
125
126        let langs = if db.in_use() { Lang::get_langs(db).await } else { Lang::get_langs_install(&files) };
127
128        if langs.is_empty() {
129            Log::warning(1151, None);
130            return Lang {
131                langs: Arc::new(Vec::new()),
132                list: Arc::new(BTreeMap::new()),
133                default: 0,
134                #[cfg(debug_assertions)]
135                last: last_time,
136                #[cfg(debug_assertions)]
137                hash: 0,
138                #[cfg(debug_assertions)]
139                root,
140                codes,
141            };
142        }
143
144        let mut default = 0;
145        for item in &langs {
146            codes.insert(item.code.clone(), item.id);
147            if item.code == default_lang {
148                default = item.id as usize;
149            }
150        }
151        let mut lang = Lang {
152            langs: Arc::new(langs),
153            list: Arc::new(BTreeMap::new()),
154            default,
155            #[cfg(debug_assertions)]
156            last: last_time,
157            #[cfg(debug_assertions)]
158            hash: 0,
159            #[cfg(debug_assertions)]
160            root,
161            codes,
162        };
163        lang.load(files).await;
164        lang
165    }
166
167    pub(crate) async fn get_all_langs(db: Arc<DB>) -> Vec<LangItem> {
168        let mut vec = Vec::with_capacity(200);
169        if db.in_use() {
170            let res = match db.query_prepare(m_fnv1a_64!("lib_get_all_langs"), &[], false).await {
171                Some(r) => r,
172                None => {
173                    Log::warning(1150, None);
174                    return Vec::new();
175                }
176            };
177            if res.is_empty() {
178                Log::warning(1151, None);
179                return Vec::new();
180            }
181            for row in res {
182                if let Data::Vec(row) = row {
183                    if row.len() != 4 {
184                        Log::warning(1150, None);
185                        return Vec::new();
186                    }
187                    let id = if let Data::I64(val) = unsafe { row.get_unchecked(0) } {
188                        *val
189                    } else {
190                        Log::warning(1150, None);
191                        return Vec::new();
192                    };
193                    let index = if let Data::I64(val) = unsafe { row.get_unchecked(3) } {
194                        *val
195                    } else {
196                        Log::warning(1150, None);
197                        return Vec::new();
198                    };
199                    let code = if let Data::String(val) = unsafe { row.get_unchecked(1) } {
200                        val.to_owned()
201                    } else {
202                        Log::warning(1150, None);
203                        return Vec::new();
204                    };
205                    let name = if let Data::String(val) = unsafe { row.get_unchecked(2) } {
206                        val.to_owned()
207                    } else {
208                        Log::warning(1150, None);
209                        return Vec::new();
210                    };
211                    vec.push(LangItem { id, code, name, index });
212                } else {
213                    Log::warning(1150, None);
214                    return Vec::new();
215                }
216            }
217        } else {
218            return Lang::gelt_all_langs_install();
219        }
220
221        vec
222    }
223
224    /// Get list of enabled langs from database
225    async fn get_langs(db: &mut DB) -> Vec<Arc<LangItem>> {
226        let res = match db.query_prepare(m_fnv1a_64!("lib_get_langs"), &[], false).await {
227            Some(r) => r,
228            None => {
229                Log::warning(1150, None);
230                return Vec::new();
231            }
232        };
233        if res.is_empty() {
234            Log::warning(1151, None);
235            return Vec::new();
236        }
237        let mut vec = Vec::with_capacity(res.len());
238        for row in res {
239            if let Data::Vec(row) = row {
240                if row.len() != 4 {
241                    Log::warning(1150, None);
242                    return Vec::new();
243                }
244                let id = if let Data::I64(val) = unsafe { row.get_unchecked(0) } {
245                    *val
246                } else {
247                    Log::warning(1150, None);
248                    return Vec::new();
249                };
250                let index = if let Data::I64(val) = unsafe { row.get_unchecked(3) } {
251                    *val
252                } else {
253                    Log::warning(1150, None);
254                    return Vec::new();
255                };
256                let code = if let Data::String(val) = unsafe { row.get_unchecked(1) } {
257                    val.to_owned()
258                } else {
259                    Log::warning(1150, None);
260                    return Vec::new();
261                };
262                let name = if let Data::String(val) = unsafe { row.get_unchecked(2) } {
263                    val.to_owned()
264                } else {
265                    Log::warning(1150, None);
266                    return Vec::new();
267                };
268                vec.push(Arc::new(LangItem { id, code, name, index }));
269            } else {
270                Log::warning(1150, None);
271                return Vec::new();
272            }
273        }
274        vec
275    }
276
277    /// Other languages ​​will be added as quality translation is provided
278    fn gelt_all_langs_install() -> Vec<LangItem> {
279        let list = vec![
280            LangItem {
281                id: 0,
282                code: "en".to_owned(),
283                name: "English".to_string(),
284                index: m_fnv1a_64!("en"),
285            },
286            LangItem {
287                id: 1,
288                code: "uk".to_owned(),
289                name: "Ukrainian (Українська)".to_string(),
290                index: m_fnv1a_64!("uk"),
291            },
292            LangItem {
293                id: 28,
294                code: "cs".to_owned(),
295                name: "Czech (Čeština)".to_string(),
296                index: m_fnv1a_64!("cs"),
297            },
298            LangItem {
299                id: 40,
300                code: "et".to_owned(),
301                name: "Estonian (Eesti)".to_string(),
302                index: m_fnv1a_64!("et"),
303            },
304            LangItem {
305                id: 97,
306                code: "lt".to_owned(),
307                name: "Lithuanian (Lietuvių kalba)".to_string(),
308                index: m_fnv1a_64!("lt"),
309            },
310            LangItem {
311                id: 99,
312                code: "lv".to_owned(),
313                name: "Latvian (Latviešu valoda)".to_string(),
314                index: m_fnv1a_64!("lv"),
315            },
316            LangItem {
317                id: 117,
318                code: "no".to_owned(),
319                name: "Norwegian (Norsk)".to_string(),
320                index: m_fnv1a_64!("no"),
321            },
322            LangItem {
323                id: 128,
324                code: "pl".to_owned(),
325                name: "Polish (Język polski)".to_string(),
326                index: m_fnv1a_64!("pl"),
327            },
328        ];
329        list
330    }
331
332    /// Get list of enabled langs for install module
333    pub(crate) fn get_langs_install(files: &Vec<(PathBuf, String, String, String)>) -> Vec<Arc<LangItem>> {
334        let list = Lang::gelt_all_langs_install();
335
336        let mut vec = Vec::with_capacity(files.len());
337        let mut index = 0;
338        for (_, module, class, code) in files {
339            if module == "index" && class == "install" {
340                for lang in &list {
341                    if lang.index == fnv1a_64(code.as_bytes()) {
342                        let mut l = lang.clone();
343                        l.index = index;
344                        vec.push(Arc::new(l));
345                        index += 1;
346                        break;
347                    }
348                }
349            }
350        }
351
352        vec
353    }
354
355    /// Load lang's files
356    pub(crate) async fn get_files(root: Arc<String>) -> Vec<(PathBuf, String, String, String)> {
357        let mut vec = Vec::new();
358
359        let path = format!("{}/app/", root);
360        let read_path = match read_dir(&path) {
361            Ok(r) => r,
362            Err(e) => {
363                Log::warning(1100, Some(format!("Path: {}. Err: {}", path, e)));
364                return vec;
365            }
366        };
367
368        // Read first level dir
369        for entry in read_path {
370            let path = match entry {
371                Ok(e) => e.path(),
372                Err(e) => {
373                    Log::warning(1101, Some(format!("{} ({})", e, path)));
374                    continue;
375                }
376            };
377            if !path.is_dir() {
378                continue;
379            }
380            let module = match path.file_name() {
381                Some(m) => match m.to_str() {
382                    Some(module) => module,
383                    None => continue,
384                },
385                None => continue,
386            };
387            let read_path = match read_dir(&path) {
388                Ok(r) => r,
389                Err(e) => {
390                    Log::warning(1102, Some(format!("{} ({})", e, path.display())));
391                    continue;
392                }
393            };
394
395            // Read second level dir
396            for entry in read_path {
397                let path = match entry {
398                    Ok(e) => e.path(),
399                    Err(e) => {
400                        Log::warning(1101, Some(format!("{} ({})", e, path.display())));
401                        continue;
402                    }
403                };
404                if !path.is_dir() {
405                    continue;
406                }
407                let class = match path.file_name() {
408                    Some(c) => match c.to_str() {
409                        Some(class) => class,
410                        None => continue,
411                    },
412                    None => continue,
413                };
414                let read_path = match read_dir(&path) {
415                    Ok(r) => r,
416                    Err(e) => {
417                        Log::warning(1102, Some(format!("{} ({})", e, path.display())));
418                        continue;
419                    }
420                };
421                // Read third level dir
422                for entry in read_path {
423                    let path = match entry {
424                        Ok(e) => e.path(),
425                        Err(e) => {
426                            Log::warning(1101, Some(format!("{} ({})", e, path.display())));
427                            continue;
428                        }
429                    };
430                    if !path.is_file() {
431                        continue;
432                    }
433                    let code = match path.file_name() {
434                        Some(v) => match v.to_str() {
435                            Some(view) => view,
436                            None => continue,
437                        },
438                        None => continue,
439                    };
440                    if code.starts_with("lang.") && code.ends_with(".toml") && code.len() == 12 {
441                        let code = unsafe { code.get_unchecked(5..7) }.to_owned();
442                        vec.push((path, module.to_owned(), class.to_owned(), code));
443                    }
444                }
445            }
446        }
447        vec
448    }
449
450    /// Check system time
451    #[cfg(debug_assertions)]
452    pub(crate) async fn check_time(&self) -> bool {
453        let files = Lang::get_files(Arc::clone(&self.root)).await;
454        let mut last_time = SystemTime::UNIX_EPOCH;
455        let mut hash: i128 = 0;
456
457        for (path, _, _, _) in files {
458            if let Ok(metadata) = fs::metadata(&path).await {
459                if let Ok(modified_time) = metadata.modified() {
460                    if modified_time > last_time {
461                        last_time = modified_time;
462                    }
463                    if let Some(s) = path.as_os_str().to_str() {
464                        hash += fnv1a_64(s.as_bytes()) as i128;
465                    }
466                }
467            }
468        }
469        last_time != self.last || hash != self.hash
470    }
471
472    /// Load translates
473    pub(crate) async fn load(&mut self, files: Vec<(PathBuf, String, String, String)>) {
474        #[cfg(debug_assertions)]
475        let mut last_time = SystemTime::UNIX_EPOCH;
476        #[cfg(debug_assertions)]
477        let mut hash: i128 = 0;
478
479        let mut list = BTreeMap::new();
480
481        for (path, module, class, code) in files {
482            if let Some(id) = self.codes.get(&code) {
483                if let Ok(text) = read_to_string(&path) {
484                    #[cfg(debug_assertions)]
485                    if let Ok(metadata) = fs::metadata(&path).await {
486                        if let Ok(modified_time) = metadata.modified() {
487                            if modified_time > last_time {
488                                last_time = modified_time;
489                            }
490                            if let Some(s) = path.as_os_str().to_str() {
491                                hash += fnv1a_64(s.as_bytes()) as i128;
492                            }
493                        }
494                    }
495                    if !text.is_empty() {
496                        let text = match text.parse::<Table>() {
497                            Ok(v) => v,
498                            Err(e) => {
499                                Log::warning(19, Some(format!("{:?} {} ", path.to_str(), e)));
500                                continue;
501                            }
502                        };
503                        for (key, value) in text {
504                            if let Value::String(val) = value {
505                                let l1 = match list.entry(*id) {
506                                    Entry::Vacant(v) => v.insert(BTreeMap::new()),
507                                    Entry::Occupied(o) => o.into_mut(),
508                                };
509                                // module
510                                let l2 = match l1.entry(fnv1a_64(module.as_bytes())) {
511                                    Entry::Vacant(v) => v.insert(BTreeMap::new()),
512                                    Entry::Occupied(o) => o.into_mut(),
513                                };
514                                // class
515                                let l3 = match l2.entry(fnv1a_64(class.as_bytes())) {
516                                    Entry::Vacant(v) => v.insert(BTreeMap::new()),
517                                    Entry::Occupied(o) => o.into_mut(),
518                                };
519                                l3.insert(fnv1a_64(key.as_bytes()), val);
520                            } else {
521                                Log::warning(20, Some(format!("{:?} {} ", path.to_str(), value)));
522                                continue;
523                            }
524                        }
525                    }
526                }
527            }
528        }
529
530        // Add Arc to async operation
531        let mut list_lang = BTreeMap::new();
532        for (key_lang, item_lang) in list {
533            let mut list_module = BTreeMap::new();
534            for (key_module, item_module) in item_lang {
535                let mut list_class = BTreeMap::new();
536                for (key_class, item_class) in item_module {
537                    list_class.insert(key_class, Arc::new(item_class));
538                }
539                list_module.insert(key_module, list_class);
540            }
541            list_lang.insert(key_lang, list_module);
542        }
543        self.list = Arc::new(list_lang);
544        #[cfg(debug_assertions)]
545        {
546            self.last = last_time;
547        }
548        #[cfg(debug_assertions)]
549        {
550            self.hash = hash;
551        }
552    }
553}