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, PartialEq, Serialize, Deserialize)]
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_string()));
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(
152 "".to_string(),
153 ))
154 .to_string(),
155 );
156 } else {
157 return Err(fmt::Error);
158 }
159 } else {
160 return Err(fmt::Error);
161 }
162 } else {
163 i -= 1;
164 built_text.push('%');
165 }
166 }
167 }
168 } else {
169 built_text.push(template.chars().nth(i).unwrap());
170 }
171
172 i += 1;
173 }
174
175 if components.is_empty() {
176 return Ok(TextComponent::new(built_text));
177 }
178
179 components.push(TextComponent::new(built_text));
180
181 Ok(TextComponent {
182 base: BaseComponent {
183 siblings: components.into_iter().map(FormattedText::Text).collect(),
184 style: Default::default(),
185 },
186 text: "".to_string(),
187 })
188 }
189}
190
191impl Display for TranslatableComponent {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 for component in FormattedText::Translatable(self.clone()).into_iter() {
195 let component_text = match &component {
196 FormattedText::Text(c) => c.text.to_string(),
197 FormattedText::Translatable(c) => match c.read() {
198 Ok(c) => c.to_string(),
199 Err(_) => c.key.to_string(),
200 },
201 };
202
203 f.write_str(&component_text)?;
204 }
205
206 Ok(())
207 }
208}
209
210impl Display for PrimitiveOrComponent {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
212 match self {
213 PrimitiveOrComponent::Boolean(value) => write!(f, "{value}"),
214 PrimitiveOrComponent::Short(value) => write!(f, "{value}"),
215 PrimitiveOrComponent::Integer(value) => write!(f, "{value}"),
216 PrimitiveOrComponent::Long(value) => write!(f, "{value}"),
217 PrimitiveOrComponent::Float(value) => write!(f, "{value}"),
218 PrimitiveOrComponent::Double(value) => write!(f, "{value}"),
219 PrimitiveOrComponent::String(value) => write!(f, "{value}"),
220 PrimitiveOrComponent::FormattedText(value) => write!(f, "{value}"),
221 }
222 }
223}
224
225impl From<PrimitiveOrComponent> for TextComponent {
226 fn from(soc: PrimitiveOrComponent) -> Self {
227 match soc {
228 PrimitiveOrComponent::String(value) => TextComponent::new(value),
229 PrimitiveOrComponent::Boolean(value) => TextComponent::new(value.to_string()),
230 PrimitiveOrComponent::Short(value) => TextComponent::new(value.to_string()),
231 PrimitiveOrComponent::Integer(value) => TextComponent::new(value.to_string()),
232 PrimitiveOrComponent::Long(value) => TextComponent::new(value.to_string()),
233 PrimitiveOrComponent::Float(value) => TextComponent::new(value.to_string()),
234 PrimitiveOrComponent::Double(value) => TextComponent::new(value.to_string()),
235 PrimitiveOrComponent::FormattedText(value) => TextComponent::new(value.to_string()),
236 }
237 }
238}
239impl From<&str> for TranslatableComponent {
240 fn from(s: &str) -> Self {
241 TranslatableComponent::new(s.to_string(), vec![])
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_none() {
251 let c = TranslatableComponent::new("translation.test.none".to_string(), vec![]);
252 assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_string());
253 }
254 #[test]
255 fn test_complex() {
256 let c = TranslatableComponent::new(
257 "translation.test.complex".to_string(),
258 vec![
259 PrimitiveOrComponent::String("a".to_string()),
260 PrimitiveOrComponent::String("b".to_string()),
261 PrimitiveOrComponent::String("c".to_string()),
262 PrimitiveOrComponent::String("d".to_string()),
263 ],
264 );
265 assert_eq!(
267 c.read().unwrap().to_string(),
268 "Prefix, ab again b and a lastly c and also a again!".to_string()
269 );
270 }
271 #[test]
272 fn test_escape() {
273 let c = TranslatableComponent::new(
274 "translation.test.escape".to_string(),
275 vec![
276 PrimitiveOrComponent::String("a".to_string()),
277 PrimitiveOrComponent::String("b".to_string()),
278 PrimitiveOrComponent::String("c".to_string()),
279 PrimitiveOrComponent::String("d".to_string()),
280 ],
281 );
282 assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_string());
283 }
284 #[test]
285 fn test_invalid() {
286 let c = TranslatableComponent::new(
287 "translation.test.invalid".to_string(),
288 vec![
289 PrimitiveOrComponent::String("a".to_string()),
290 PrimitiveOrComponent::String("b".to_string()),
291 PrimitiveOrComponent::String("c".to_string()),
292 PrimitiveOrComponent::String("d".to_string()),
293 ],
294 );
295 assert_eq!(c.read().unwrap().to_string(), "hi %".to_string());
296 }
297 #[test]
298 fn test_invalid2() {
299 let c = TranslatableComponent::new(
300 "translation.test.invalid2".to_string(),
301 vec![
302 PrimitiveOrComponent::String("a".to_string()),
303 PrimitiveOrComponent::String("b".to_string()),
304 PrimitiveOrComponent::String("c".to_string()),
305 PrimitiveOrComponent::String("d".to_string()),
306 ],
307 );
308 assert_eq!(c.read().unwrap().to_string(), "hi % s".to_string());
309 }
310
311 #[test]
312 fn test_undefined() {
313 let c = TranslatableComponent::new(
314 "translation.test.undefined".to_string(),
315 vec![PrimitiveOrComponent::String("a".to_string())],
316 );
317 assert_eq!(
318 c.read().unwrap().to_string(),
319 "translation.test.undefined".to_string()
320 );
321 }
322
323 #[test]
324 fn test_undefined_with_fallback() {
325 let c = TranslatableComponent::with_fallback(
326 "translation.test.undefined".to_string(),
327 Some("translation fallback: %s".to_string()),
328 vec![PrimitiveOrComponent::String("a".to_string())],
329 );
330 assert_eq!(
331 c.read().unwrap().to_string(),
332 "translation fallback: a".to_string()
333 );
334 }
335}