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();
85
86pub fn translations_init(
87 trans: HashMap<String, TranslationElements>,
88 max_pool_size: usize,
89) -> Result<(), TranslationError> {
90 let parsed = parse_translations_grpc_respones(trans);
91 let mut store = HashMap::new();
92
93 for (lang, lang_trans) in parsed {
94 let mut lang_map = HashMap::new();
95 for (id, tr) in lang_trans {
96 let pool = Arc::new(Mutex::new(TemplatePool::new(&tr, max_pool_size)));
97 lang_map.insert(id, pool);
98 }
99 store.insert(lang, lang_map);
100 }
101
102 TRANSLATION_STORE
103 .set(store)
104 .map_err(|_| TranslationError::NotInitialized)
105}
106
107pub fn tr<P: Serialize>(
108 lang: &str,
109 id: &str,
110 params: Option<P>,
111) -> Result<String, TranslationError> {
112 let store = TRANSLATION_STORE
113 .get()
114 .ok_or(TranslationError::NotInitialized)?;
115
116 let pool = store
117 .get(lang)
118 .and_then(|lang_pools| lang_pools.get(id))
119 .ok_or_else(|| TranslationError::KeyNotFound(id.to_string()))?;
120
121 let mut pool_guard = pool.lock().unwrap();
122 if pool_guard.has_vars && params.is_none() {
123 return Err(TranslationError::MissingParams);
124 }
125
126 let tera = pool_guard.get()?;
127
128 let result = match params {
129 Some(p) => {
130 let context = tera::Context::from_serialize(&p)?;
131 tera.render("pooled_template", &context)
132 }
133 None => {
134 Ok(pool_guard.template_str.clone())
136 }
137 }?;
138
139 pool_guard.return_instance(tera);
140 Ok(result)
141}