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();
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      // For non-parameterized templates, just return the raw template string
142      Ok(pool_guard.template_str.clone())
143    }
144  }?;
145
146  pool_guard.return_instance(tera);
147  Ok(result)
148}