azalea_chat/
translatable_component.rs

1use std::fmt::{self, Display, Formatter};
2
3use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
4#[cfg(feature = "simdnbt")]
5use simdnbt::Serialize as _;
6
7use crate::{
8    base_component::BaseComponent, style::Style, text_component::TextComponent, FormattedText,
9};
10
11#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)]
12#[serde(untagged)]
13pub enum StringOrComponent {
14    String(String),
15    FormattedText(FormattedText),
16}
17
18#[cfg(feature = "simdnbt")]
19impl simdnbt::ToNbtTag for StringOrComponent {
20    fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
21        match self {
22            StringOrComponent::String(s) => s.to_nbt_tag(),
23            StringOrComponent::FormattedText(c) => c.to_nbt_tag(),
24        }
25    }
26}
27
28/// A message whose content depends on the client's language.
29#[derive(Clone, Debug, PartialEq, Eq, Hash)]
30pub struct TranslatableComponent {
31    pub base: BaseComponent,
32    pub key: String,
33    pub args: Vec<StringOrComponent>,
34}
35
36impl Serialize for TranslatableComponent {
37    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38    where
39        S: Serializer,
40    {
41        let mut state = serializer.serialize_map(None)?;
42        state.serialize_entry("translate", &self.key)?;
43        Serialize::serialize(&self.base, FlatMapSerializer(&mut state))?;
44        state.serialize_entry("with", &self.args)?;
45        state.end()
46    }
47}
48
49#[cfg(feature = "simdnbt")]
50fn serialize_args_as_nbt(args: &[StringOrComponent]) -> simdnbt::owned::NbtList {
51    // if it's all strings then make it a string list
52    // if it's all components then make it a compound list
53    // if it's a mix then return an error
54
55    use tracing::debug;
56
57    let mut string_list = Vec::new();
58    let mut compound_list = Vec::new();
59
60    for arg in args {
61        match arg {
62            StringOrComponent::String(s) => {
63                string_list.push(s.clone());
64            }
65            StringOrComponent::FormattedText(c) => {
66                compound_list.push(c.clone().to_compound());
67            }
68        }
69    }
70
71    if !string_list.is_empty() && !compound_list.is_empty() {
72        // i'm actually not sure what vanilla does here, so i just made it return the
73        // string list
74        debug!("Tried to serialize a TranslatableComponent with a mix of strings and components.");
75        return string_list.into();
76    }
77
78    if !string_list.is_empty() {
79        return string_list.into();
80    }
81
82    compound_list.into()
83}
84
85#[cfg(feature = "simdnbt")]
86impl simdnbt::Serialize for TranslatableComponent {
87    fn to_compound(self) -> simdnbt::owned::NbtCompound {
88        let mut compound = simdnbt::owned::NbtCompound::new();
89        compound.insert("translate", self.key);
90        compound.extend(self.base.style.to_compound());
91
92        compound.insert("with", serialize_args_as_nbt(&self.args));
93        compound
94    }
95}
96
97impl TranslatableComponent {
98    pub fn new(key: String, args: Vec<StringOrComponent>) -> Self {
99        Self {
100            base: BaseComponent::new(),
101            key,
102            args,
103        }
104    }
105
106    /// Convert the key and args to a FormattedText.
107    pub fn read(&self) -> Result<TextComponent, fmt::Error> {
108        let template = azalea_language::get(&self.key).unwrap_or(&self.key);
109        // decode the % things
110
111        let mut i = 0;
112        let mut matched = 0;
113
114        // every time we get a char we add it to built_text, and we push it to
115        // `arguments` and clear it when we add a new argument component
116        let mut built_text = String::new();
117        let mut components = Vec::new();
118
119        while i < template.chars().count() {
120            if template.chars().nth(i).unwrap() == '%' {
121                let Some(char_after) = template.chars().nth(i + 1) else {
122                    built_text.push('%');
123                    break;
124                };
125                i += 1;
126                match char_after {
127                    '%' => {
128                        built_text.push('%');
129                    }
130                    's' => {
131                        let arg_component = self
132                            .args
133                            .get(matched)
134                            .cloned()
135                            .unwrap_or_else(|| StringOrComponent::String("".to_string()));
136
137                        components.push(TextComponent::new(built_text.clone()));
138                        built_text.clear();
139                        components.push(TextComponent::from(arg_component));
140                        matched += 1;
141                    }
142                    _ => {
143                        // check if the char is a number
144                        if let Some(d) = char_after.to_digit(10) {
145                            // make sure the next two chars are $s
146                            if let Some('$') = template.chars().nth(i + 1) {
147                                if let Some('s') = template.chars().nth(i + 2) {
148                                    i += 2;
149                                    built_text.push_str(
150                                        &self
151                                            .args
152                                            .get((d - 1) as usize)
153                                            .unwrap_or(&StringOrComponent::String("".to_string()))
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: Style::default(),
185            },
186            text: "".to_string(),
187        })
188    }
189}
190
191impl Display for TranslatableComponent {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 StringOrComponent {
211    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
212        match self {
213            StringOrComponent::String(s) => write!(f, "{s}"),
214            StringOrComponent::FormattedText(c) => write!(f, "{c}"),
215        }
216    }
217}
218
219impl From<StringOrComponent> for TextComponent {
220    fn from(soc: StringOrComponent) -> Self {
221        match soc {
222            StringOrComponent::String(s) => TextComponent::new(s),
223            StringOrComponent::FormattedText(c) => TextComponent::new(c.to_string()),
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_none() {
234        let c = TranslatableComponent::new("translation.test.none".to_string(), vec![]);
235        assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_string());
236    }
237    #[test]
238    fn test_complex() {
239        let c = TranslatableComponent::new(
240            "translation.test.complex".to_string(),
241            vec![
242                StringOrComponent::String("a".to_string()),
243                StringOrComponent::String("b".to_string()),
244                StringOrComponent::String("c".to_string()),
245                StringOrComponent::String("d".to_string()),
246            ],
247        );
248        // so true mojang
249        assert_eq!(
250            c.read().unwrap().to_string(),
251            "Prefix, ab again b and a lastly c and also a again!".to_string()
252        );
253    }
254    #[test]
255    fn test_escape() {
256        let c = TranslatableComponent::new(
257            "translation.test.escape".to_string(),
258            vec![
259                StringOrComponent::String("a".to_string()),
260                StringOrComponent::String("b".to_string()),
261                StringOrComponent::String("c".to_string()),
262                StringOrComponent::String("d".to_string()),
263            ],
264        );
265        assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_string());
266    }
267    #[test]
268    fn test_invalid() {
269        let c = TranslatableComponent::new(
270            "translation.test.invalid".to_string(),
271            vec![
272                StringOrComponent::String("a".to_string()),
273                StringOrComponent::String("b".to_string()),
274                StringOrComponent::String("c".to_string()),
275                StringOrComponent::String("d".to_string()),
276            ],
277        );
278        assert_eq!(c.read().unwrap().to_string(), "hi %".to_string());
279    }
280    #[test]
281    fn test_invalid2() {
282        let c = TranslatableComponent::new(
283            "translation.test.invalid2".to_string(),
284            vec![
285                StringOrComponent::String("a".to_string()),
286                StringOrComponent::String("b".to_string()),
287                StringOrComponent::String("c".to_string()),
288                StringOrComponent::String("d".to_string()),
289            ],
290        );
291        assert_eq!(c.read().unwrap().to_string(), "hi %  s".to_string());
292    }
293}