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, Deserialize, PartialEq, Serialize)]
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_owned()));
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("".to_owned()))
152                                            .to_string(),
153                                    );
154                                } else {
155                                    return Err(fmt::Error);
156                                }
157                            } else {
158                                return Err(fmt::Error);
159                            }
160                        } else {
161                            i -= 1;
162                            built_text.push('%');
163                        }
164                    }
165                }
166            } else {
167                built_text.push(template.chars().nth(i).unwrap());
168            }
169
170            i += 1;
171        }
172
173        if components.is_empty() {
174            return Ok(TextComponent::new(built_text));
175        }
176
177        components.push(TextComponent::new(built_text));
178
179        Ok(TextComponent {
180            base: BaseComponent {
181                siblings: components.into_iter().map(FormattedText::Text).collect(),
182                style: Default::default(),
183            },
184            text: "".to_owned(),
185        })
186    }
187}
188
189impl Display for TranslatableComponent {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        // this contains the final string will all the ansi escape codes
192        for component in FormattedText::Translatable(self.clone()).into_iter() {
193            let component_text = match &component {
194                FormattedText::Text(c) => c.text.to_string(),
195                FormattedText::Translatable(c) => match c.read() {
196                    Ok(c) => c.to_string(),
197                    Err(_) => c.key.to_string(),
198                },
199            };
200
201            f.write_str(&component_text)?;
202        }
203
204        Ok(())
205    }
206}
207
208impl Display for PrimitiveOrComponent {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
210        match self {
211            PrimitiveOrComponent::Boolean(value) => write!(f, "{value}"),
212            PrimitiveOrComponent::Short(value) => write!(f, "{value}"),
213            PrimitiveOrComponent::Integer(value) => write!(f, "{value}"),
214            PrimitiveOrComponent::Long(value) => write!(f, "{value}"),
215            PrimitiveOrComponent::Float(value) => write!(f, "{value}"),
216            PrimitiveOrComponent::Double(value) => write!(f, "{value}"),
217            PrimitiveOrComponent::String(value) => write!(f, "{value}"),
218            PrimitiveOrComponent::FormattedText(value) => write!(f, "{value}"),
219        }
220    }
221}
222
223impl From<PrimitiveOrComponent> for TextComponent {
224    fn from(soc: PrimitiveOrComponent) -> Self {
225        match soc {
226            PrimitiveOrComponent::String(value) => TextComponent::new(value),
227            PrimitiveOrComponent::Boolean(value) => TextComponent::new(value.to_string()),
228            PrimitiveOrComponent::Short(value) => TextComponent::new(value.to_string()),
229            PrimitiveOrComponent::Integer(value) => TextComponent::new(value.to_string()),
230            PrimitiveOrComponent::Long(value) => TextComponent::new(value.to_string()),
231            PrimitiveOrComponent::Float(value) => TextComponent::new(value.to_string()),
232            PrimitiveOrComponent::Double(value) => TextComponent::new(value.to_string()),
233            PrimitiveOrComponent::FormattedText(value) => TextComponent::new(value.to_string()),
234        }
235    }
236}
237impl From<&str> for TranslatableComponent {
238    fn from(s: &str) -> Self {
239        TranslatableComponent::new(s.to_owned(), vec![])
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_none() {
249        let c = TranslatableComponent::new("translation.test.none".to_owned(), vec![]);
250        assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_owned());
251    }
252    #[test]
253    fn test_complex() {
254        let c = TranslatableComponent::new(
255            "translation.test.complex".to_owned(),
256            vec![
257                PrimitiveOrComponent::String("a".to_owned()),
258                PrimitiveOrComponent::String("b".to_owned()),
259                PrimitiveOrComponent::String("c".to_owned()),
260                PrimitiveOrComponent::String("d".to_owned()),
261            ],
262        );
263        // so true mojang
264        assert_eq!(
265            c.read().unwrap().to_string(),
266            "Prefix, ab again b and a lastly c and also a again!".to_owned()
267        );
268    }
269    #[test]
270    fn test_escape() {
271        let c = TranslatableComponent::new(
272            "translation.test.escape".to_owned(),
273            vec![
274                PrimitiveOrComponent::String("a".to_owned()),
275                PrimitiveOrComponent::String("b".to_owned()),
276                PrimitiveOrComponent::String("c".to_owned()),
277                PrimitiveOrComponent::String("d".to_owned()),
278            ],
279        );
280        assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_owned());
281    }
282    #[test]
283    fn test_invalid() {
284        let c = TranslatableComponent::new(
285            "translation.test.invalid".to_owned(),
286            vec![
287                PrimitiveOrComponent::String("a".to_owned()),
288                PrimitiveOrComponent::String("b".to_owned()),
289                PrimitiveOrComponent::String("c".to_owned()),
290                PrimitiveOrComponent::String("d".to_owned()),
291            ],
292        );
293        assert_eq!(c.read().unwrap().to_string(), "hi %".to_owned());
294    }
295    #[test]
296    fn test_invalid2() {
297        let c = TranslatableComponent::new(
298            "translation.test.invalid2".to_owned(),
299            vec![
300                PrimitiveOrComponent::String("a".to_owned()),
301                PrimitiveOrComponent::String("b".to_owned()),
302                PrimitiveOrComponent::String("c".to_owned()),
303                PrimitiveOrComponent::String("d".to_owned()),
304            ],
305        );
306        assert_eq!(c.read().unwrap().to_string(), "hi %  s".to_owned());
307    }
308
309    #[test]
310    fn test_undefined() {
311        let c = TranslatableComponent::new(
312            "translation.test.undefined".to_owned(),
313            vec![PrimitiveOrComponent::String("a".to_owned())],
314        );
315        assert_eq!(
316            c.read().unwrap().to_string(),
317            "translation.test.undefined".to_owned()
318        );
319    }
320
321    #[test]
322    fn test_undefined_with_fallback() {
323        let c = TranslatableComponent::with_fallback(
324            "translation.test.undefined".to_owned(),
325            Some("translation fallback: %s".to_owned()),
326            vec![PrimitiveOrComponent::String("a".to_owned())],
327        );
328        assert_eq!(
329            c.read().unwrap().to_string(),
330            "translation fallback: a".to_owned()
331        );
332    }
333}