azalea_chat/
translatable_component.rs

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