azalea_chat/
translatable_component.rs

1use std::fmt::{self, Display};
2
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "simdnbt")]
5use simdnbt::{
6    ToNbtTag,
7    owned::{NbtList, NbtTag},
8};
9
10use crate::{FormattedText, base_component::BaseComponent, text_component::TextComponent};
11
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum PrimitiveOrComponent {
15    Boolean(bool),
16    Short(i16),
17    Integer(i32),
18    Long(i64),
19    Float(f32),
20    Double(f64),
21    String(String),
22    FormattedText(FormattedText),
23}
24
25#[cfg(feature = "simdnbt")]
26impl simdnbt::ToNbtTag for PrimitiveOrComponent {
27    fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
28        match self {
29            PrimitiveOrComponent::Boolean(value) => value.to_nbt_tag(),
30            PrimitiveOrComponent::Short(value) => value.to_nbt_tag(),
31            PrimitiveOrComponent::Integer(value) => value.to_nbt_tag(),
32            PrimitiveOrComponent::Long(value) => value.to_nbt_tag(),
33            PrimitiveOrComponent::Float(value) => value.to_nbt_tag(),
34            PrimitiveOrComponent::Double(value) => value.to_nbt_tag(),
35            PrimitiveOrComponent::String(value) => value.to_nbt_tag(),
36            PrimitiveOrComponent::FormattedText(value) => value.to_nbt_tag(),
37        }
38    }
39}
40
41/// A message whose content depends on the client's language.
42#[derive(Clone, Debug, PartialEq, Serialize)]
43pub struct TranslatableComponent {
44    #[serde(flatten)]
45    pub base: BaseComponent,
46    #[serde(rename = "translate")]
47    pub key: String,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub fallback: Option<String>,
50    #[serde(rename = "with")]
51    pub args: Vec<PrimitiveOrComponent>,
52}
53
54#[cfg(feature = "simdnbt")]
55fn serialize_args_as_nbt(args: Vec<PrimitiveOrComponent>) -> NbtList {
56    let tags = args
57        .into_iter()
58        .map(|arg| arg.to_nbt_tag())
59        .collect::<Vec<NbtTag>>();
60    NbtList::from(tags)
61}
62
63#[cfg(feature = "simdnbt")]
64impl simdnbt::Serialize for TranslatableComponent {
65    fn to_compound(self) -> simdnbt::owned::NbtCompound {
66        let mut compound = simdnbt::owned::NbtCompound::new();
67        compound.insert("translate", self.key);
68        compound.extend(self.base.style.to_compound());
69
70        compound.insert("with", serialize_args_as_nbt(self.args));
71        compound
72    }
73}
74
75impl TranslatableComponent {
76    pub fn new(key: String, args: Vec<PrimitiveOrComponent>) -> Self {
77        Self {
78            base: BaseComponent::new(),
79            key,
80            fallback: None,
81            args,
82        }
83    }
84
85    pub fn with_fallback(
86        key: String,
87        fallback: Option<String>,
88        args: Vec<PrimitiveOrComponent>,
89    ) -> Self {
90        Self {
91            base: BaseComponent::new(),
92            key,
93            fallback,
94            args,
95        }
96    }
97
98    /// Convert the key and args to a FormattedText.
99    pub fn read(&self) -> Result<TextComponent, fmt::Error> {
100        let template = azalea_language::get(&self.key).unwrap_or_else(|| {
101            if let Some(fallback) = &self.fallback {
102                fallback.as_str()
103            } else {
104                &self.key
105            }
106        });
107        // decode the % things
108
109        let mut i = 0;
110        let mut matched = 0;
111
112        // every time we get a char we add it to built_text, and we push it to
113        // `arguments` and clear it when we add a new argument component
114        let mut built_text = String::new();
115        let mut components = Vec::new();
116
117        while i < template.chars().count() {
118            if template.chars().nth(i).unwrap() == '%' {
119                let Some(char_after) = template.chars().nth(i + 1) else {
120                    built_text.push('%');
121                    break;
122                };
123                i += 1;
124                match char_after {
125                    '%' => {
126                        built_text.push('%');
127                    }
128                    's' => {
129                        let arg_component = self
130                            .args
131                            .get(matched)
132                            .cloned()
133                            .unwrap_or_else(|| PrimitiveOrComponent::String("".to_string()));
134
135                        components.push(TextComponent::new(built_text.clone()));
136                        built_text.clear();
137                        components.push(TextComponent::from(arg_component));
138                        matched += 1;
139                    }
140                    _ => {
141                        // check if the char is a number
142                        if let Some(d) = char_after.to_digit(10) {
143                            // make sure the next two chars are $s
144                            if let Some('$') = template.chars().nth(i + 1) {
145                                if let Some('s') = template.chars().nth(i + 2) {
146                                    i += 2;
147                                    built_text.push_str(
148                                        &self
149                                            .args
150                                            .get((d - 1) as usize)
151                                            .unwrap_or(&PrimitiveOrComponent::String(
152                                                "".to_string(),
153                                            ))
154                                            .to_string(),
155                                    );
156                                } else {
157                                    return Err(fmt::Error);
158                                }
159                            } else {
160                                return Err(fmt::Error);
161                            }
162                        } else {
163                            i -= 1;
164                            built_text.push('%');
165                        }
166                    }
167                }
168            } else {
169                built_text.push(template.chars().nth(i).unwrap());
170            }
171
172            i += 1;
173        }
174
175        if components.is_empty() {
176            return Ok(TextComponent::new(built_text));
177        }
178
179        components.push(TextComponent::new(built_text));
180
181        Ok(TextComponent {
182            base: BaseComponent {
183                siblings: components.into_iter().map(FormattedText::Text).collect(),
184                style: Default::default(),
185            },
186            text: "".to_string(),
187        })
188    }
189}
190
191impl Display for TranslatableComponent {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        // this contains the final string will all the ansi escape codes
194        for component in FormattedText::Translatable(self.clone()).into_iter() {
195            let component_text = match &component {
196                FormattedText::Text(c) => c.text.to_string(),
197                FormattedText::Translatable(c) => match c.read() {
198                    Ok(c) => c.to_string(),
199                    Err(_) => c.key.to_string(),
200                },
201            };
202
203            f.write_str(&component_text)?;
204        }
205
206        Ok(())
207    }
208}
209
210impl Display for PrimitiveOrComponent {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
212        match self {
213            PrimitiveOrComponent::Boolean(value) => write!(f, "{value}"),
214            PrimitiveOrComponent::Short(value) => write!(f, "{value}"),
215            PrimitiveOrComponent::Integer(value) => write!(f, "{value}"),
216            PrimitiveOrComponent::Long(value) => write!(f, "{value}"),
217            PrimitiveOrComponent::Float(value) => write!(f, "{value}"),
218            PrimitiveOrComponent::Double(value) => write!(f, "{value}"),
219            PrimitiveOrComponent::String(value) => write!(f, "{value}"),
220            PrimitiveOrComponent::FormattedText(value) => write!(f, "{value}"),
221        }
222    }
223}
224
225impl From<PrimitiveOrComponent> for TextComponent {
226    fn from(soc: PrimitiveOrComponent) -> Self {
227        match soc {
228            PrimitiveOrComponent::String(value) => TextComponent::new(value),
229            PrimitiveOrComponent::Boolean(value) => TextComponent::new(value.to_string()),
230            PrimitiveOrComponent::Short(value) => TextComponent::new(value.to_string()),
231            PrimitiveOrComponent::Integer(value) => TextComponent::new(value.to_string()),
232            PrimitiveOrComponent::Long(value) => TextComponent::new(value.to_string()),
233            PrimitiveOrComponent::Float(value) => TextComponent::new(value.to_string()),
234            PrimitiveOrComponent::Double(value) => TextComponent::new(value.to_string()),
235            PrimitiveOrComponent::FormattedText(value) => TextComponent::new(value.to_string()),
236        }
237    }
238}
239impl From<&str> for TranslatableComponent {
240    fn from(s: &str) -> Self {
241        TranslatableComponent::new(s.to_string(), vec![])
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_none() {
251        let c = TranslatableComponent::new("translation.test.none".to_string(), vec![]);
252        assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_string());
253    }
254    #[test]
255    fn test_complex() {
256        let c = TranslatableComponent::new(
257            "translation.test.complex".to_string(),
258            vec![
259                PrimitiveOrComponent::String("a".to_string()),
260                PrimitiveOrComponent::String("b".to_string()),
261                PrimitiveOrComponent::String("c".to_string()),
262                PrimitiveOrComponent::String("d".to_string()),
263            ],
264        );
265        // so true mojang
266        assert_eq!(
267            c.read().unwrap().to_string(),
268            "Prefix, ab again b and a lastly c and also a again!".to_string()
269        );
270    }
271    #[test]
272    fn test_escape() {
273        let c = TranslatableComponent::new(
274            "translation.test.escape".to_string(),
275            vec![
276                PrimitiveOrComponent::String("a".to_string()),
277                PrimitiveOrComponent::String("b".to_string()),
278                PrimitiveOrComponent::String("c".to_string()),
279                PrimitiveOrComponent::String("d".to_string()),
280            ],
281        );
282        assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_string());
283    }
284    #[test]
285    fn test_invalid() {
286        let c = TranslatableComponent::new(
287            "translation.test.invalid".to_string(),
288            vec![
289                PrimitiveOrComponent::String("a".to_string()),
290                PrimitiveOrComponent::String("b".to_string()),
291                PrimitiveOrComponent::String("c".to_string()),
292                PrimitiveOrComponent::String("d".to_string()),
293            ],
294        );
295        assert_eq!(c.read().unwrap().to_string(), "hi %".to_string());
296    }
297    #[test]
298    fn test_invalid2() {
299        let c = TranslatableComponent::new(
300            "translation.test.invalid2".to_string(),
301            vec![
302                PrimitiveOrComponent::String("a".to_string()),
303                PrimitiveOrComponent::String("b".to_string()),
304                PrimitiveOrComponent::String("c".to_string()),
305                PrimitiveOrComponent::String("d".to_string()),
306            ],
307        );
308        assert_eq!(c.read().unwrap().to_string(), "hi %  s".to_string());
309    }
310
311    #[test]
312    fn test_undefined() {
313        let c = TranslatableComponent::new(
314            "translation.test.undefined".to_string(),
315            vec![PrimitiveOrComponent::String("a".to_string())],
316        );
317        assert_eq!(
318            c.read().unwrap().to_string(),
319            "translation.test.undefined".to_string()
320        );
321    }
322
323    #[test]
324    fn test_undefined_with_fallback() {
325        let c = TranslatableComponent::with_fallback(
326            "translation.test.undefined".to_string(),
327            Some("translation fallback: %s".to_string()),
328            vec![PrimitiveOrComponent::String("a".to_string())],
329        );
330        assert_eq!(
331            c.read().unwrap().to_string(),
332            "translation fallback: a".to_string()
333        );
334    }
335}