Skip to main content

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 matched = 0;
110
111        // every time we get a char we add it to built_text, and we push it to
112        // `arguments` and clear it when we add a new argument component
113        let mut built_text = String::new();
114        let mut components = Vec::new();
115
116        let mut chars = template.chars();
117        while let Some(char) = chars.next() {
118            if char != '%' {
119                built_text.push(char);
120                continue;
121            }
122
123            let mut chars_preview = chars.clone();
124            let Some(char_after) = chars_preview.next() else {
125                built_text.push('%');
126                break;
127            };
128            match char_after {
129                '%' => {
130                    chars.next();
131
132                    built_text.push('%');
133                }
134                's' => {
135                    chars.next();
136
137                    let arg_component = self
138                        .args
139                        .get(matched)
140                        .cloned()
141                        .unwrap_or_else(|| PrimitiveOrComponent::String("".to_owned()));
142
143                    components.push(TextComponent::new(built_text.clone()));
144                    built_text.clear();
145                    components.push(TextComponent::from(arg_component));
146                    matched += 1;
147                }
148                '0'..='9' if let Some(d) = char_after.to_digit(10) => {
149                    chars.next();
150                    // make sure the next two chars are $s
151                    let Some('$') = chars.next() else {
152                        return Err(fmt::Error);
153                    };
154                    let Some('s') = chars.next() else {
155                        return Err(fmt::Error);
156                    };
157                    built_text.push_str(
158                        &self
159                            .args
160                            .get((d - 1) as usize)
161                            .unwrap_or(&PrimitiveOrComponent::String("".to_owned()))
162                            .to_string(),
163                    );
164                }
165                _ => {
166                    built_text.push('%');
167                }
168            }
169        }
170
171        if components.is_empty() {
172            return Ok(TextComponent::new(built_text));
173        }
174
175        components.push(TextComponent::new(built_text));
176
177        Ok(TextComponent {
178            base: BaseComponent {
179                siblings: components.into_iter().map(FormattedText::Text).collect(),
180                style: Default::default(),
181            },
182            text: "".to_owned(),
183        })
184    }
185}
186
187impl Display for TranslatableComponent {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        // this contains the final string will all the ansi escape codes
190        for component in FormattedText::Translatable(self.clone()).into_iter() {
191            let component_text = match &component {
192                FormattedText::Text(c) => c.text.to_string(),
193                FormattedText::Translatable(c) => match c.read() {
194                    Ok(c) => c.to_string(),
195                    Err(_) => c.key.to_string(),
196                },
197            };
198
199            f.write_str(&component_text)?;
200        }
201
202        Ok(())
203    }
204}
205
206impl Display for PrimitiveOrComponent {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
208        match self {
209            PrimitiveOrComponent::Boolean(value) => write!(f, "{value}"),
210            PrimitiveOrComponent::Short(value) => write!(f, "{value}"),
211            PrimitiveOrComponent::Integer(value) => write!(f, "{value}"),
212            PrimitiveOrComponent::Long(value) => write!(f, "{value}"),
213            PrimitiveOrComponent::Float(value) => write!(f, "{value}"),
214            PrimitiveOrComponent::Double(value) => write!(f, "{value}"),
215            PrimitiveOrComponent::String(value) => write!(f, "{value}"),
216            PrimitiveOrComponent::FormattedText(value) => write!(f, "{value}"),
217        }
218    }
219}
220
221impl From<PrimitiveOrComponent> for TextComponent {
222    fn from(soc: PrimitiveOrComponent) -> Self {
223        match soc {
224            PrimitiveOrComponent::String(value) => TextComponent::new(value),
225            PrimitiveOrComponent::Boolean(value) => TextComponent::new(value.to_string()),
226            PrimitiveOrComponent::Short(value) => TextComponent::new(value.to_string()),
227            PrimitiveOrComponent::Integer(value) => TextComponent::new(value.to_string()),
228            PrimitiveOrComponent::Long(value) => TextComponent::new(value.to_string()),
229            PrimitiveOrComponent::Float(value) => TextComponent::new(value.to_string()),
230            PrimitiveOrComponent::Double(value) => TextComponent::new(value.to_string()),
231            PrimitiveOrComponent::FormattedText(value) => TextComponent::new(value.to_string()),
232        }
233    }
234}
235impl From<&str> for TranslatableComponent {
236    fn from(s: &str) -> Self {
237        TranslatableComponent::new(s.to_owned(), vec![])
238    }
239}
240impl From<TranslatableComponent> for FormattedText {
241    fn from(c: TranslatableComponent) -> Self {
242        FormattedText::Translatable(c)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_none() {
252        let c = TranslatableComponent::new("translation.test.none".to_owned(), vec![]);
253        assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_owned());
254    }
255    #[test]
256    fn test_complex() {
257        let c = TranslatableComponent::new(
258            "translation.test.complex".to_owned(),
259            vec![
260                PrimitiveOrComponent::String("a".to_owned()),
261                PrimitiveOrComponent::String("b".to_owned()),
262                PrimitiveOrComponent::String("c".to_owned()),
263                PrimitiveOrComponent::String("d".to_owned()),
264            ],
265        );
266        // so true mojang
267        assert_eq!(
268            c.read().unwrap().to_string(),
269            "Prefix, ab again b and a lastly c and also a again!".to_owned()
270        );
271    }
272    #[test]
273    fn test_escape() {
274        let c = TranslatableComponent::new(
275            "translation.test.escape".to_owned(),
276            vec![
277                PrimitiveOrComponent::String("a".to_owned()),
278                PrimitiveOrComponent::String("b".to_owned()),
279                PrimitiveOrComponent::String("c".to_owned()),
280                PrimitiveOrComponent::String("d".to_owned()),
281            ],
282        );
283        assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_owned());
284    }
285    #[test]
286    fn test_invalid() {
287        let c = TranslatableComponent::new(
288            "translation.test.invalid".to_owned(),
289            vec![
290                PrimitiveOrComponent::String("a".to_owned()),
291                PrimitiveOrComponent::String("b".to_owned()),
292                PrimitiveOrComponent::String("c".to_owned()),
293                PrimitiveOrComponent::String("d".to_owned()),
294            ],
295        );
296        assert_eq!(c.read().unwrap().to_string(), "hi %".to_owned());
297    }
298    #[test]
299    fn test_invalid2() {
300        let c = TranslatableComponent::new(
301            "translation.test.invalid2".to_owned(),
302            vec![
303                PrimitiveOrComponent::String("a".to_owned()),
304                PrimitiveOrComponent::String("b".to_owned()),
305                PrimitiveOrComponent::String("c".to_owned()),
306                PrimitiveOrComponent::String("d".to_owned()),
307            ],
308        );
309        assert_eq!(c.read().unwrap().to_string(), "hi %  s".to_owned());
310    }
311
312    #[test]
313    fn test_undefined() {
314        let c = TranslatableComponent::new(
315            "translation.test.undefined".to_owned(),
316            vec![PrimitiveOrComponent::String("a".to_owned())],
317        );
318        assert_eq!(
319            c.read().unwrap().to_string(),
320            "translation.test.undefined".to_owned()
321        );
322    }
323
324    #[test]
325    fn test_undefined_with_fallback() {
326        let c = TranslatableComponent::with_fallback(
327            "translation.test.undefined".to_owned(),
328            Some("translation fallback: %s".to_owned()),
329            vec![PrimitiveOrComponent::String("a".to_owned())],
330        );
331        assert_eq!(
332            c.read().unwrap().to_string(),
333            "translation fallback: a".to_owned()
334        );
335    }
336}