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 matched = 0;
110
111 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 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 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}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_none() {
247 let c = TranslatableComponent::new("translation.test.none".to_owned(), vec![]);
248 assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_owned());
249 }
250 #[test]
251 fn test_complex() {
252 let c = TranslatableComponent::new(
253 "translation.test.complex".to_owned(),
254 vec![
255 PrimitiveOrComponent::String("a".to_owned()),
256 PrimitiveOrComponent::String("b".to_owned()),
257 PrimitiveOrComponent::String("c".to_owned()),
258 PrimitiveOrComponent::String("d".to_owned()),
259 ],
260 );
261 assert_eq!(
263 c.read().unwrap().to_string(),
264 "Prefix, ab again b and a lastly c and also a again!".to_owned()
265 );
266 }
267 #[test]
268 fn test_escape() {
269 let c = TranslatableComponent::new(
270 "translation.test.escape".to_owned(),
271 vec![
272 PrimitiveOrComponent::String("a".to_owned()),
273 PrimitiveOrComponent::String("b".to_owned()),
274 PrimitiveOrComponent::String("c".to_owned()),
275 PrimitiveOrComponent::String("d".to_owned()),
276 ],
277 );
278 assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_owned());
279 }
280 #[test]
281 fn test_invalid() {
282 let c = TranslatableComponent::new(
283 "translation.test.invalid".to_owned(),
284 vec![
285 PrimitiveOrComponent::String("a".to_owned()),
286 PrimitiveOrComponent::String("b".to_owned()),
287 PrimitiveOrComponent::String("c".to_owned()),
288 PrimitiveOrComponent::String("d".to_owned()),
289 ],
290 );
291 assert_eq!(c.read().unwrap().to_string(), "hi %".to_owned());
292 }
293 #[test]
294 fn test_invalid2() {
295 let c = TranslatableComponent::new(
296 "translation.test.invalid2".to_owned(),
297 vec![
298 PrimitiveOrComponent::String("a".to_owned()),
299 PrimitiveOrComponent::String("b".to_owned()),
300 PrimitiveOrComponent::String("c".to_owned()),
301 PrimitiveOrComponent::String("d".to_owned()),
302 ],
303 );
304 assert_eq!(c.read().unwrap().to_string(), "hi % s".to_owned());
305 }
306
307 #[test]
308 fn test_undefined() {
309 let c = TranslatableComponent::new(
310 "translation.test.undefined".to_owned(),
311 vec![PrimitiveOrComponent::String("a".to_owned())],
312 );
313 assert_eq!(
314 c.read().unwrap().to_string(),
315 "translation.test.undefined".to_owned()
316 );
317 }
318
319 #[test]
320 fn test_undefined_with_fallback() {
321 let c = TranslatableComponent::with_fallback(
322 "translation.test.undefined".to_owned(),
323 Some("translation fallback: %s".to_owned()),
324 vec![PrimitiveOrComponent::String("a".to_owned())],
325 );
326 assert_eq!(
327 c.read().unwrap().to_string(),
328 "translation fallback: a".to_owned()
329 );
330 }
331}