embedded_lang/
language.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Part of a path to a string
5#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
6#[serde(untagged)]
7pub enum LanguageStringObject {
8    /// A string endpoint
9    Direct(String),
10
11    /// Part of a path to an endpoint
12    Category(HashMap<String, LanguageStringObject>),
13}
14
15impl LanguageStringObject {
16    /// Flatten a LanguageStringObject tree into a flat object
17    pub fn flatten(&self, own_key: &str) -> HashMap<String, String> {
18        let mut map = HashMap::<String, String>::default();
19        match self {
20            LanguageStringObject::Direct(s) => {
21                map.insert(own_key.to_string(), s.clone());
22            }
23            LanguageStringObject::Category(c) => map.extend(Self::flatten_all(c, Some(own_key))),
24        };
25        map
26    }
27
28    fn flatten_all(
29        c: &HashMap<String, LanguageStringObject>,
30        root_key: Option<&str>,
31    ) -> HashMap<String, String> {
32        let mut map = HashMap::<String, String>::default();
33        c.iter().for_each(|e| {
34            let key = if root_key.is_some() {
35                format!("{}\\{}", root_key.unwrap(), e.0)
36            } else {
37                e.0.clone()
38            };
39            map.extend(e.1.flatten(&key))
40        });
41        map
42    }
43}
44
45/// Represents a single language lookup instance
46#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
47pub struct Language {
48    name: String,
49    short_name: String,
50    strings: HashMap<String, LanguageStringObject>,
51
52    #[serde(default)]
53    resources: HashMap<String, Vec<u8>>,
54}
55
56impl Language {
57    /// Create a new language instance
58    ///
59    /// # Arguments
60    /// * `name` - Full language name
61    /// * `short_name` - Language code
62    /// * `strings` - Language lookup table
63    pub fn new(
64        name: String,
65        short_name: String,
66        strings: HashMap<String, LanguageStringObject>,
67        resources: HashMap<String, Vec<u8>>,
68    ) -> Self {
69        Self {
70            name,
71            short_name,
72            strings,
73            resources,
74        }
75    }
76
77    /// Read language from a JSON string
78    ///
79    /// # Arguments
80    /// * `path` - Path to the file
81    pub fn new_from_string(
82        json: &str,
83        resources: HashMap<String, Vec<u8>>,
84    ) -> Result<Self, String> {
85        match serde_json::from_str::<Self>(json) {
86            Ok(mut lang) => {
87                lang.resources = resources;
88                Ok(lang)
89            }
90            Err(e) => Err(e.to_string()),
91        }
92    }
93
94    /// Read language from a file
95    ///
96    /// # Arguments
97    /// * `path` - Path to the file
98    pub fn new_from_file(path: &str, resources: HashMap<String, Vec<u8>>) -> Result<Self, String> {
99        match std::fs::read_to_string(path) {
100            Ok(json) => Self::new_from_string(&json, resources),
101            Err(e) => Err(e.to_string()),
102        }
103    }
104
105    /// Get full language name
106    pub fn name(&self) -> &str {
107        &self.name
108    }
109
110    /// Get language code
111    pub fn short_name(&self) -> &str {
112        &self.short_name
113    }
114
115    /// Get language lookup table
116    pub fn strings(&self) -> HashMap<String, String> {
117        LanguageStringObject::flatten_all(&self.strings, None)
118    }
119
120    /// Look up a string in the given language
121    ///
122    /// # Arguments
123    /// * `name` - String to find
124    pub fn get(&self, name: &str) -> Option<&str> {
125        let mut path = name.split("\\").peekable();
126        if path.peek().is_none() {
127            return None;
128        }
129
130        let mut pos = self.strings.get(path.next().unwrap());
131        for item in path {
132            if pos.is_none() {
133                return None;
134            }
135            match pos.unwrap() {
136                LanguageStringObject::Direct(s) => return Some(s),
137                LanguageStringObject::Category(c) => pos = c.get(item),
138            }
139        }
140
141        if let Some(pos) = pos {
142            match pos {
143                LanguageStringObject::Direct(s) => Some(s),
144                LanguageStringObject::Category(_) => None,
145            }
146        } else {
147            None
148        }
149    }
150
151    /// Return an embedded resource as a utf8 string
152    pub fn utf8_resource(&self, name: &str) -> Option<&str> {
153        self.resources
154            .get(name)
155            .and_then(|bytes| std::str::from_utf8(&bytes.as_slice()).ok())
156    }
157
158    /// Return an embedded resource as a slice of bytes
159    pub fn binary_resource(&self, name: &str) -> Option<&[u8]> {
160        self.resources
161            .get(name)
162            .and_then(|bytes| Some(bytes.as_slice()))
163    }
164}
165
166#[cfg(test)]
167mod test_token {
168    use super::*;
169    use crate as embedded_lang;
170    use crate::embedded_language;
171
172    #[test]
173    fn test_new_from_string() {
174        if let Ok(s) = std::fs::read_to_string("examples/en.lang.json") {
175            let lang = Language::new_from_string(&s, HashMap::default()).unwrap();
176            assert_eq!(lang.short_name(), "en");
177        }
178    }
179
180    #[test]
181    fn test_new_from_file() {
182        let lang = Language::new_from_file("examples/en.lang.json", HashMap::default()).unwrap();
183        assert_eq!(lang.short_name(), "en");
184    }
185
186    #[test]
187    fn test_short_name() {
188        let lang = embedded_language!("../examples/en.lang.json");
189        assert_eq!(lang.short_name(), "en");
190    }
191
192    #[test]
193    fn test_name() {
194        let lang = embedded_language!("../examples/en.lang.json");
195        assert_eq!(lang.name(), "English");
196    }
197
198    #[test]
199    fn test_get() {
200        let lang = embedded_language!("../examples/en.lang.json");
201
202        assert_eq!(lang.get("hello_msg"), Some("hello world!"));
203        assert_eq!(lang.get("goodbye_msg"), None);
204    }
205}