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#[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 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 let mut i = 0;
110 let mut matched = 0;
111
112 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 if let Some(d) = char_after.to_digit(10) {
143 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 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 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}