megacommerce_shared/models/
translate.rs

1use 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      // For non-parameterized templates, just return the raw template string
135      Ok(pool_guard.template_str.clone())
136    }
137  }?;
138
139  pool_guard.return_instance(tera);
140  Ok(result)
141}