Skip to main content

alizarin_core/graph/
translatable.rs

1//! Translatable string type for multi-language support.
2
3use serde::{Deserialize, Deserializer, Serialize};
4use std::collections::HashMap;
5
6/// A string with translations for multiple languages
7#[derive(Clone, Debug, Default)]
8pub struct StaticTranslatableString {
9    pub translations: HashMap<String, String>,
10    pub lang: String,
11}
12
13impl StaticTranslatableString {
14    /// Create a new translatable string from a map of translations
15    pub fn from_translations(
16        translations: HashMap<String, String>,
17        default_lang: Option<String>,
18    ) -> Self {
19        let lang = default_lang.unwrap_or_else(|| "en".to_string());
20        let actual_lang = if translations.contains_key(&lang) {
21            lang
22        } else {
23            translations
24                .keys()
25                .next()
26                .cloned()
27                .unwrap_or_else(|| "en".to_string())
28        };
29        StaticTranslatableString {
30            translations,
31            lang: actual_lang,
32        }
33    }
34
35    /// Create from a simple string (assumes English)
36    pub fn from_string(s: &str) -> Self {
37        let mut translations = HashMap::new();
38        translations.insert("en".to_string(), s.to_string());
39        StaticTranslatableString {
40            translations,
41            lang: "en".to_string(),
42        }
43    }
44
45    /// Create an empty translatable string
46    pub fn empty() -> Self {
47        StaticTranslatableString {
48            translations: HashMap::new(),
49            lang: "en".to_string(),
50        }
51    }
52
53    /// Get the string for a specific language, falling back to any available
54    pub fn get(&self, lang: &str) -> String {
55        self.translations
56            .get(lang)
57            .or_else(|| self.translations.get("en"))
58            .or_else(|| self.translations.values().next())
59            .cloned()
60            .unwrap_or_default()
61    }
62
63    /// Get the string using the default language
64    pub fn to_string_default(&self) -> String {
65        self.get(&self.lang)
66    }
67
68    /// Copy/clone the translatable string
69    pub fn copy(&self) -> Self {
70        self.clone()
71    }
72
73    /// Serialize to JSON value
74    pub fn to_json(&self) -> serde_json::Value {
75        serde_json::to_value(&self.translations).unwrap_or(serde_json::Value::Null)
76    }
77}
78
79impl std::fmt::Display for StaticTranslatableString {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "{}", self.to_string_default())
82    }
83}
84
85impl Serialize for StaticTranslatableString {
86    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87    where
88        S: serde::Serializer,
89    {
90        self.translations.serialize(serializer)
91    }
92}
93
94impl<'de> Deserialize<'de> for StaticTranslatableString {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: Deserializer<'de>,
98    {
99        use serde_json::Value;
100        let value = Value::deserialize(deserializer)?;
101
102        match value {
103            Value::Object(map) => {
104                let translations: HashMap<String, String> = map
105                    .into_iter()
106                    .filter_map(|(k, v)| v.as_str().map(|s| (k, s.to_string())))
107                    .collect();
108
109                let lang = translations
110                    .keys()
111                    .next()
112                    .cloned()
113                    .unwrap_or_else(|| "en".to_string());
114
115                Ok(StaticTranslatableString { translations, lang })
116            }
117            Value::String(s) => {
118                let mut translations = HashMap::new();
119                translations.insert("en".to_string(), s);
120                Ok(StaticTranslatableString {
121                    translations,
122                    lang: "en".to_string(),
123                })
124            }
125            _ => {
126                let mut translations = HashMap::new();
127                translations.insert("en".to_string(), String::new());
128                Ok(StaticTranslatableString {
129                    translations,
130                    lang: "en".to_string(),
131                })
132            }
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_translatable_string_from_map() {
143        let mut translations = HashMap::new();
144        translations.insert("en".to_string(), "Hello".to_string());
145        translations.insert("de".to_string(), "Hallo".to_string());
146
147        let ts = StaticTranslatableString::from_translations(translations, None);
148        assert_eq!(ts.get("en"), "Hello");
149        assert_eq!(ts.get("de"), "Hallo");
150    }
151
152    #[test]
153    fn test_translatable_string_fallback() {
154        let mut translations = HashMap::new();
155        translations.insert("de".to_string(), "Hallo".to_string());
156
157        let ts = StaticTranslatableString::from_translations(translations, None);
158        // Should fall back to any available language
159        assert_eq!(ts.get("en"), "Hallo");
160    }
161}