1use std::{cell::{Cell, RefCell}, collections::{HashMap, HashSet}, sync::Arc};
2use maplit::{hashmap, hashset};
3use language_objects::{Language};
4use lazy_static::lazy_static;
5use lazy_regex::regex;
6
7#[macro_export]
24macro_rules! locator_vars {
25 (@single $($x:tt)*) => (());
26 (@count $($rest:expr),*) => (<[()]>::len(&[$(locator_vars!(@single $rest)),*]));
27
28 ($($key:expr => $value:expr,)+) => { locator_vars!($($key => $value),+) };
29 ($($key:expr => $value:expr),*) => {
30 {
31 let _cap = locator_vars!(@count $($key),*);
32 let mut _map = ::std::collections::HashMap::<String, String>::with_capacity(_cap);
33 $(
34 let _ = _map.insert($key.to_string(), $value.to_string());
35 )*
36 _map
37 }
38 };
39}
40
41pub struct MessageLocator {
42 _current_locale: Option<Language>,
43 _locale_path_components: Arc<HashMap<Language, String>>,
44 _supported_locales: Arc<HashSet<Language>>,
45 _default_locale: Language,
46 _fallbacks: Arc<HashMap<Language, Vec<Language>>>,
47 _assets: Arc<HashMap<Language, serde_json::Value>>,
48 _assets_src: String,
49 _assets_base_file_names: Vec<String>,
50 _assets_clean_unused: bool,
51 _assets_load_method: MessageLocatorLoadMethod,
52}
53
54impl MessageLocator {
55 pub fn new(options: &MessageLocatorOptions) -> Self {
57 let mut locale_path_components = HashMap::<Language, String>::new();
58 let mut supported_locales = HashSet::<Language>::new();
59 for code in options._supported_locales.borrow().iter() {
60 let locale_parse = Language::parse(code).unwrap();
61 locale_path_components.insert(locale_parse.clone(), code.clone());
62 supported_locales.insert(locale_parse);
63 }
64 let mut fallbacks = HashMap::<Language, Vec<Language>>::new();
65 for (k, v) in options._fallbacks.borrow().iter() {
66 fallbacks.insert(Language::parse(k).unwrap(), v.iter().map(|s| Language::parse(s).unwrap()).collect());
67 }
68 let default_locale = options._default_locale.borrow().clone();
69 Self {
70 _current_locale: None,
71 _locale_path_components: Arc::new(locale_path_components),
72 _supported_locales: Arc::new(supported_locales),
73 _default_locale: Language::parse(&default_locale).unwrap(),
74 _fallbacks: Arc::new(fallbacks),
75 _assets: Arc::new(HashMap::new()),
76 _assets_src: options._assets.borrow()._src.borrow().clone(),
77 _assets_base_file_names: options._assets.borrow()._base_file_names.borrow().iter().map(|s| s.clone()).collect(),
78 _assets_clean_unused: options._assets.borrow()._clean_unused.get(),
79 _assets_load_method: options._assets.borrow()._load_method.get(),
80 }
81 }
82
83 pub fn supported_locales(&self) -> HashSet<Language> {
86 self._supported_locales.as_ref().clone()
87 }
88
89 pub fn supports_locale(&self, arg: &Language) -> bool {
93 self._supported_locales.contains(arg)
94 }
95
96 pub fn current_locale(&self) -> Option<Language> {
98 self._current_locale.clone()
99 }
100
101 pub fn current_locale_seq(&self) -> HashSet<Language> {
103 if let Some(c) = self.current_locale() {
104 let mut r: HashSet<Language> = hashset![c.clone()];
105 self.enumerate_fallbacks(c.clone(), &mut r);
106 return r;
107 }
108 hashset![]
109 }
110
111 pub async fn update_locale(&mut self, new_locale: Language) -> bool {
114 self.load(Some(new_locale)).await
115 }
116
117 pub async fn load(&mut self, mut new_locale: Option<Language>) -> bool {
124 if new_locale.is_none() { new_locale = Some(self._default_locale.clone()); }
125 let new_locale = new_locale.unwrap();
126 if !self.supports_locale(&new_locale) {
127 panic!("Unsupported locale {}", new_locale.tag());
128 }
129 let mut to_load: HashSet<Language> = hashset![new_locale.clone()];
130 self.enumerate_fallbacks(new_locale.clone(), &mut to_load);
131
132 let mut new_assets: HashMap<Language, serde_json::Value> = hashmap![];
133 for locale in to_load {
134 let res = self.load_single_locale(&locale).await;
135 if res.is_none() {
136 return false;
137 }
138 new_assets.insert(locale.clone(), res.unwrap());
139 }
140 if self._assets_clean_unused {
141 Arc::get_mut(&mut self._assets).unwrap().clear();
142 }
143
144 for (locale, root) in new_assets {
145 Arc::get_mut(&mut self._assets).unwrap().insert(locale, root);
146 }
147 self._current_locale = Some(new_locale.clone());
148 true
151 }
152
153 async fn load_single_locale(&self, locale: &Language) -> Option<serde_json::Value> {
154 let mut r = serde_json::Value::Object(serde_json::Map::new());
155 match self._assets_load_method {
156 MessageLocatorLoadMethod::FileSystem => {
157 for base_name in self._assets_base_file_names.iter() {
158 let locale_path_comp = self._locale_path_components.get(locale);
159 if locale_path_comp.is_none() {
160 panic!("Fallback locale is not supported a locale: {}", locale.tag());
161 }
162 let res_path = format!("{}/{}/{}.json", self._assets_src, locale_path_comp.unwrap(), base_name);
163 let content = std::fs::read(res_path.clone());
164 if content.is_err() {
165 println!("Failed to load resource at {}.", res_path);
166 return None;
167 }
168 MessageLocator::apply_deep(base_name, serde_json::from_str(String::from_utf8(content.unwrap()).unwrap().as_ref()).unwrap(), &mut r);
169 }
170 },
171 MessageLocatorLoadMethod::Http => {
172 for base_name in self._assets_base_file_names.iter() {
173 let locale_path_comp = self._locale_path_components.get(locale);
174 if locale_path_comp.is_none() {
175 panic!("Fallback locale is not supported a locale: {}", locale.tag());
176 }
177 let res_path = format!("{}/{}/{}.json", self._assets_src, locale_path_comp.unwrap(), base_name);
178 let content = reqwest::get(reqwest::Url::parse(res_path.clone().as_ref()).unwrap()).await;
179 if content.is_err() {
180 println!("Failed to load resource at {}.", res_path);
181 return None;
182 }
183 let content = if content.is_ok() { Some(content.unwrap().text().await) } else { None };
184 MessageLocator::apply_deep(base_name, serde_json::from_str(content.unwrap().unwrap().as_ref()).unwrap(), &mut r);
185 }
186 },
187 }
188 Some(r)
189 }
190
191 fn apply_deep(name: &String, assign: serde_json::Value, mut output: &mut serde_json::Value) {
192 let mut names: Vec<&str> = name.split("/").collect();
193 let last_name = names.pop();
194 for name in names {
195 let r = output.get(name);
196 if r.is_none() || r.unwrap().as_object().is_none() {
197 let r = serde_json::Value::Object(serde_json::Map::new());
198 output.as_object_mut().unwrap().insert(String::from(name), r);
199 }
200 output = output.get_mut(name).unwrap();
201 }
202 output.as_object_mut().unwrap().insert(String::from(last_name.unwrap()), assign);
203 }
204
205 fn enumerate_fallbacks(&self, locale: Language, output: &mut HashSet<Language>) {
206 for list in self._fallbacks.get(&locale).iter() {
207 for item in list.iter() {
208 output.insert(item.clone());
209 self.enumerate_fallbacks(item.clone(), output);
210 }
211 }
212 }
213
214 pub fn get<S: ToString>(&self, id: S) -> String {
216 self.get_formatted(id, vec![])
217 }
218
219 pub fn get_formatted<S: ToString>(&self, id: S, options: Vec<&dyn MessageLocatorFormatArgument>) -> String {
221 let mut variables: Option<HashMap<String, String>> = None;
222 let mut id = id.to_string();
223
224 for option in options.iter() {
225 if let Some(r) = option.as_str() {
226 id.push('_');
227 id.push_str(r);
228 }
229 else if let Some(r) = option.as_string() {
230 id.push('_');
231 id.push_str(r.as_str());
232 }
233 else if let Some(r) = option.as_string_map() {
234 variables = Some(r.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
235 }
236 }
237
238 if variables.is_none() { variables = Some(HashMap::new()); }
239 let variables = variables.unwrap();
240
241 let id: Vec<String> = id.split(".").map(|s| s.to_string()).collect();
242 if self._current_locale.is_none() {
243 return id.join(".");
244 }
245 let r = self.get_formatted_with_locale(self._current_locale.clone().unwrap(), &id, &variables);
246 if let Some(r) = r { r } else { id.join(".") }
247 }
248
249 fn get_formatted_with_locale(&self, locale: Language, id: &Vec<String>, vars: &HashMap<String, String>) -> Option<String> {
250 let message = self.resolve_id(self._assets.get(&locale), id);
251 if message.is_some() {
252 return Some(self.apply_message(message.unwrap(), vars));
253 }
254
255 let fallbacks = self._fallbacks.get(&locale);
256 if fallbacks.is_some() {
257 for fl in fallbacks.unwrap().iter() {
258 let r = self.get_formatted_with_locale(fl.clone(), id, vars);
259 if r.is_some() {
260 return r;
261 }
262 }
263 }
264 None
265 }
266
267 fn apply_message(&self, message: String, vars: &HashMap<String, String>) -> String {
268 regex!(r"\$(\$|[A-Za-z0-9_-]+)").replace_all(&message, |s: ®ex::Captures<'_>| {
270 let s = s.get(0).unwrap().as_str();
271 if s == "$$" {
272 "$"
273 } else {
274 let v = vars.get(&s.to_string().replace("$", ""));
275 if let Some(v) = v { v } else { "undefined" }
276 }
277 }).as_ref().to_string()
278 }
279
280 fn resolve_id(&self, root: Option<&serde_json::Value>, id: &Vec<String>) -> Option<String> {
281 let mut r = root;
282 for frag in id.iter() {
283 if r.is_none() {
284 return None;
285 }
286 r = r.unwrap().get(frag);
287 }
288 if r.is_none() {
289 return None;
290 }
291 let r = r.unwrap().as_str();
292 if let Some(r) = r { Some(r.to_string()) } else { None }
293 }
294}
295
296impl Clone for MessageLocator {
297 fn clone(&self) -> Self {
300 Self {
301 _current_locale: self._current_locale.clone(),
302 _locale_path_components: self._locale_path_components.clone(),
303 _supported_locales: self._supported_locales.clone(),
304 _default_locale: self._default_locale.clone(),
305 _fallbacks: self._fallbacks.clone(),
306 _assets: self._assets.clone(),
307 _assets_src: self._assets_src.clone(),
308 _assets_base_file_names: self._assets_base_file_names.clone(),
309 _assets_clean_unused: self._assets_clean_unused,
310 _assets_load_method: self._assets_load_method,
311 }
312 }
313}
314
315pub trait MessageLocatorFormatArgument {
316 fn as_str(&self) -> Option<&'static str> { None }
317 fn as_string(&self) -> Option<String> { None }
318 fn as_string_map(&self) -> Option<HashMap<String, String>> { None }
319}
320
321impl MessageLocatorFormatArgument for &'static str {
322 fn as_str(&self) -> Option<&'static str> { Some(self) }
323}
324
325impl MessageLocatorFormatArgument for String {
326 fn as_string(&self) -> Option<String> { Some(self.clone()) }
327}
328
329impl MessageLocatorFormatArgument for HashMap<String, String> {
330 fn as_string_map(&self) -> Option<HashMap<String, String>> { Some(self.clone()) }
331}
332
333impl MessageLocatorFormatArgument for i8 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
334impl MessageLocatorFormatArgument for i16 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
335impl MessageLocatorFormatArgument for i32 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
336impl MessageLocatorFormatArgument for i64 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
337impl MessageLocatorFormatArgument for i128 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
338impl MessageLocatorFormatArgument for isize { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
339impl MessageLocatorFormatArgument for u8 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
340impl MessageLocatorFormatArgument for u16 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
341impl MessageLocatorFormatArgument for u32 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
342impl MessageLocatorFormatArgument for u64 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
343impl MessageLocatorFormatArgument for u128 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
344impl MessageLocatorFormatArgument for usize { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
345impl MessageLocatorFormatArgument for f32 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
346impl MessageLocatorFormatArgument for f64 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
347
348pub struct MessageLocatorOptions {
349 _default_locale: RefCell<String>,
350 _supported_locales: RefCell<Vec<String>>,
351 _fallbacks: RefCell<HashMap<String, Vec<String>>>,
352 _assets: RefCell<MessageLocatorAssetOptions>,
353}
354
355impl MessageLocatorOptions {
356 pub fn new() -> Self {
357 MessageLocatorOptions {
358 _default_locale: RefCell::new("en".to_string()),
359 _supported_locales: RefCell::new(vec!["en".to_string()]),
360 _fallbacks: RefCell::new(hashmap! {}),
361 _assets: RefCell::new(MessageLocatorAssetOptions::new()),
362 }
363 }
364
365 pub fn default_locale<S: ToString>(&self, value: S) -> &Self {
366 self._default_locale.replace(value.to_string());
367 self
368 }
369
370 pub fn supported_locales<S: ToString>(&self, list: Vec<S>) -> &Self {
371 self._supported_locales.replace(list.iter().map(|name| name.to_string()).collect());
372 self
373 }
374
375 pub fn fallbacks<S: ToString>(&self, map: HashMap<S, Vec<S>>) -> &Self {
376 self._fallbacks.replace(map.iter().map(|(k, v)| (
377 k.to_string(),
378 v.iter().map(|s| s.to_string()).collect()
379 )).collect());
380 self
381 }
382
383 pub fn assets(&self, options: &MessageLocatorAssetOptions) -> &Self {
384 self._assets.replace(options.clone());
385 self
386 }
387}
388
389pub struct MessageLocatorAssetOptions {
390 _src: RefCell<String>,
391 _base_file_names: RefCell<Vec<String>>,
392 _clean_unused: Cell<bool>,
393 _load_method: Cell<MessageLocatorLoadMethod>,
394}
395
396impl Clone for MessageLocatorAssetOptions {
397 fn clone(&self) -> Self {
398 Self {
399 _src: self._src.clone(),
400 _base_file_names: self._base_file_names.clone(),
401 _clean_unused: self._clean_unused.clone(),
402 _load_method: self._load_method.clone(),
403 }
404 }
405}
406
407impl MessageLocatorAssetOptions {
408 pub fn new() -> Self {
409 MessageLocatorAssetOptions {
410 _src: RefCell::new("res/lang".to_string()),
411 _base_file_names: RefCell::new(vec![]),
412 _clean_unused: Cell::new(true),
413 _load_method: Cell::new(MessageLocatorLoadMethod::Http),
414 }
415 }
416
417 pub fn src<S: ToString>(&self, src: S) -> &Self {
418 self._src.replace(src.to_string());
419 self
420 }
421
422 pub fn base_file_names<S: ToString>(&self, list: Vec<S>) -> &Self {
423 self._base_file_names.replace(list.iter().map(|name| name.to_string()).collect());
424 self
425 }
426
427 pub fn clean_unused(&self, value: bool) -> &Self {
428 self._clean_unused.set(value);
429 self
430 }
431
432 pub fn load_method(&self, value: MessageLocatorLoadMethod) -> &Self {
433 self._load_method.set(value);
434 self
435 }
436}
437
438#[derive(Copy, Clone)]
439pub enum MessageLocatorLoadMethod {
440 FileSystem,
441 Http,
442}