megacommerce_shared/models/
translate.rs1use std::collections::{HashMap, VecDeque};
2use std::error::Error;
3use std::sync::{Arc, Mutex, OnceLock};
4
5use megacommerce_proto::TranslationElements;
6use serde::Serialize;
7use serde_json::Value;
8use thiserror::Error as ThisError;
9
10pub type TranslateFunc =
11 Box<dyn Fn(&str, &str, &HashMap<String, Value>) -> Result<String, Box<dyn Error>>>;
12
13fn parse_translations_grpc_respones(
14 data: HashMap<String, TranslationElements>,
15) -> HashMap<String, HashMap<String, String>> {
16 let mut result = HashMap::new();
17 for (lang, elements) in data {
18 let mut lang_map = HashMap::new();
19 for el in elements.trans {
20 lang_map.insert(el.id, el.tr);
21 }
22
23 result.insert(lang, lang_map);
24 }
25
26 result
27}
28
29#[derive(Debug, ThisError)]
30pub enum TranslationError {
31 #[error("translation store is not initialized")]
32 NotInitialized,
33 #[error("translation is missing params")]
34 MissingParams,
35 #[error("translation key is not found: {0}")]
36 KeyNotFound(String),
37 #[error("template render error: {0}")]
38 RenderError(String),
39}
40
41#[derive(Debug)]
42struct TemplatePool {
43 available: VecDeque<tera::Tera>,
44 template_str: String,
45 has_vars: bool,
46 max_size: usize,
47}
48
49impl From<tera::Error> for TranslationError {
50 fn from(value: tera::Error) -> Self {
51 TranslationError::RenderError(value.to_string())
52 }
53}
54
55impl TemplatePool {
56 fn new(template: &str, max_size: usize) -> Self {
57 Self {
58 available: VecDeque::with_capacity(max_size),
59 template_str: template.to_string(),
60 has_vars: template.contains("{{") && template.contains("}}"),
61 max_size,
62 }
63 }
64
65 fn get(&mut self) -> Result<tera::Tera, tera::Error> {
66 self.available.pop_front().map_or_else(
67 || {
68 let mut t = tera::Tera::default();
69 t.add_raw_template("pooled_template", &self.template_str)?;
70 Ok(t)
71 },
72 Ok,
73 )
74 }
75
76 fn return_instance(&mut self, instance: tera::Tera) {
77 if self.available.len() < self.max_size {
78 self.available.push_back(instance);
79 }
80 }
81}
82
83static TRANSLATION_STORE: OnceLock<HashMap<String, HashMap<String, Arc<Mutex<TemplatePool>>>>> =
84 OnceLock::new();
85static DEFAULT_LANGUAGE: OnceLock<String> = OnceLock::new();
86static AVAILABLE_LANGUAGES: OnceLock<Vec<String>> = OnceLock::new();
87
88pub fn translations_init(
89 trans: HashMap<String, TranslationElements>,
90 max_pool_size: usize,
91 default_language: String,
92 available_languages: Vec<String>,
93) -> Result<(), TranslationError> {
94 let parsed = parse_translations_grpc_respones(trans);
95 let mut store = HashMap::new();
96
97 for (lang, lang_trans) in parsed {
98 let mut lang_map = HashMap::new();
99 for (id, tr) in lang_trans {
100 let pool = Arc::new(Mutex::new(TemplatePool::new(&tr, max_pool_size)));
101 lang_map.insert(id, pool);
102 }
103 store.insert(lang, lang_map);
104 }
105
106 AVAILABLE_LANGUAGES.set(available_languages).map_err(|_| TranslationError::NotInitialized)?;
107 DEFAULT_LANGUAGE.set(default_language.clone()).map_err(|_| TranslationError::NotInitialized)?;
108 TRANSLATION_STORE.set(store).map_err(|_| TranslationError::NotInitialized)
109}
110
111pub fn tr<P: Serialize>(
112 lang: &str,
113 id: &str,
114 params: Option<P>,
115) -> Result<String, TranslationError> {
116 let store = TRANSLATION_STORE.get().ok_or(TranslationError::NotInitialized)?;
117
118 let mut lang = lang;
119 if !AVAILABLE_LANGUAGES.get().unwrap_or(&vec![]).contains(&lang.to_string()) {
120 lang = DEFAULT_LANGUAGE.get().unwrap();
121 }
122
123 let pool = store
124 .get(lang)
125 .and_then(|lang_pools| lang_pools.get(id))
126 .ok_or_else(|| TranslationError::KeyNotFound(id.to_string()))?;
127
128 let mut pool_guard = pool.lock().unwrap();
129 if pool_guard.has_vars && params.is_none() {
130 return Err(TranslationError::MissingParams);
131 }
132
133 let tera = pool_guard.get()?;
134
135 let result = match params {
136 Some(p) => {
137 let context = tera::Context::from_serialize(&p)?;
138 tera.render("pooled_template", &context)
139 }
140 None => {
141 Ok(pool_guard.template_str.clone())
143 }
144 }?;
145
146 pool_guard.return_instance(tera);
147 Ok(result)
148}