Skip to main content

teaql_tool_std/
i18n.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use teaql_tool_core::{Result, TeaQLToolError};
4
5#[derive(Debug, Clone)]
6pub struct I18nTool {
7    translations: HashMap<String, HashMap<String, String>>,
8    default_locale: String,
9}
10
11impl I18nTool {
12    pub fn new() -> Self {
13        Self {
14            translations: HashMap::new(),
15            default_locale: "en".to_string(),
16        }
17    }
18
19    pub fn set_default_locale(&mut self, locale: &str) {
20        self.default_locale = locale.to_string();
21    }
22
23    pub fn add(&mut self, locale: &str, key: &str, value: &str) {
24        self.translations
25            .entry(locale.to_string())
26            .or_default()
27            .insert(key.to_string(), value.to_string());
28    }
29
30    /// Loads flat or nested JSON into the specified locale.
31    /// Nested objects will have their keys joined by dots (e.g. "greeting.hello").
32    pub fn load_json(&mut self, locale: &str, json_str: &str) -> Result<()> {
33        let v: Value = serde_json::from_str(json_str)
34            .map_err(|e| TeaQLToolError::ParseError(format!("Invalid JSON for i18n: {}", e)))?;
35        
36        self.flatten_and_add(locale, "", &v);
37        Ok(())
38    }
39
40    fn flatten_and_add(&mut self, locale: &str, prefix: &str, value: &Value) {
41        match value {
42            Value::Object(map) => {
43                for (k, v) in map {
44                    let new_prefix = if prefix.is_empty() {
45                        k.clone()
46                    } else {
47                        format!("{}.{}", prefix, k)
48                    };
49                    self.flatten_and_add(locale, &new_prefix, v);
50                }
51            }
52            Value::String(s) => {
53                self.add(locale, prefix, s);
54            }
55            Value::Number(n) => {
56                self.add(locale, prefix, &n.to_string());
57            }
58            Value::Bool(b) => {
59                self.add(locale, prefix, &b.to_string());
60            }
61            _ => {}
62        }
63    }
64
65    /// Translates a key for a given locale.
66    /// Falls back to the default locale if not found, or returns the key itself.
67    pub fn t(&self, locale: &str, key: &str) -> String {
68        if let Some(map) = self.translations.get(locale) {
69            if let Some(val) = map.get(key) {
70                return val.clone();
71            }
72        }
73        
74        if locale != self.default_locale {
75            if let Some(map) = self.translations.get(&self.default_locale) {
76                if let Some(val) = map.get(key) {
77                    return val.clone();
78                }
79            }
80        }
81        
82        key.to_string()
83    }
84
85    /// Translates a key and replaces placeholders like {name} with provided arguments.
86    pub fn tf(&self, locale: &str, key: &str, args: &[(&str, &str)]) -> String {
87        let mut text = self.t(locale, key);
88        for (k, v) in args {
89            let placeholder = format!("{{{}}}", k);
90            text = text.replace(&placeholder, v);
91        }
92        text
93    }
94}
95
96impl Default for I18nTool {
97    fn default() -> Self {
98        Self::new()
99    }
100}