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}
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 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}