azalea_chat/
translatable_component.rs

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