1use std::fmt::{self, Display};
2
3use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::SerializeMap};
4#[cfg(feature = "simdnbt")]
5use simdnbt::Serialize as _;
6
7use crate::{FormattedText, base_component::BaseComponent, text_component::TextComponent};
8
9#[derive(Clone, Debug, PartialEq, Serialize)]
10#[serde(untagged)]
11pub enum StringOrComponent {
12 String(String),
13 FormattedText(FormattedText),
14}
15
16#[cfg(feature = "simdnbt")]
17impl simdnbt::ToNbtTag for StringOrComponent {
18 fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
19 match self {
20 StringOrComponent::String(s) => s.to_nbt_tag(),
21 StringOrComponent::FormattedText(c) => c.to_nbt_tag(),
22 }
23 }
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct TranslatableComponent {
29 pub base: BaseComponent,
30 pub key: String,
31 pub fallback: Option<String>,
32 pub args: Vec<StringOrComponent>,
33}
34
35impl Serialize for TranslatableComponent {
36 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
37 where
38 S: Serializer,
39 {
40 let mut state = serializer.serialize_map(None)?;
41 state.serialize_entry("translate", &self.key)?;
42 Serialize::serialize(&self.base, FlatMapSerializer(&mut state))?;
43 state.serialize_entry("with", &self.args)?;
44 state.end()
45 }
46}
47
48#[cfg(feature = "simdnbt")]
49fn serialize_args_as_nbt(args: &[StringOrComponent]) -> simdnbt::owned::NbtList {
50 use tracing::debug;
55
56 let mut string_list = Vec::new();
57 let mut compound_list = Vec::new();
58
59 for arg in args {
60 match arg {
61 StringOrComponent::String(s) => {
62 string_list.push(s.clone());
63 }
64 StringOrComponent::FormattedText(c) => {
65 compound_list.push(c.clone().to_compound());
66 }
67 }
68 }
69
70 if !string_list.is_empty() && !compound_list.is_empty() {
71 debug!("Tried to serialize a TranslatableComponent with a mix of strings and components.");
74 return string_list.into();
75 }
76
77 if !string_list.is_empty() {
78 return string_list.into();
79 }
80
81 compound_list.into()
82}
83
84#[cfg(feature = "simdnbt")]
85impl simdnbt::Serialize for TranslatableComponent {
86 fn to_compound(self) -> simdnbt::owned::NbtCompound {
87 let mut compound = simdnbt::owned::NbtCompound::new();
88 compound.insert("translate", self.key);
89 compound.extend(self.base.style.to_compound());
90
91 compound.insert("with", serialize_args_as_nbt(&self.args));
92 compound
93 }
94}
95
96impl TranslatableComponent {
97 pub fn new(key: String, args: Vec<StringOrComponent>) -> Self {
98 Self {
99 base: BaseComponent::new(),
100 key,
101 fallback: None,
102 args,
103 }
104 }
105
106 pub fn with_fallback(
107 key: String,
108 fallback: Option<String>,
109 args: Vec<StringOrComponent>,
110 ) -> Self {
111 Self {
112 base: BaseComponent::new(),
113 key,
114 fallback,
115 args,
116 }
117 }
118
119 pub fn read(&self) -> Result<TextComponent, fmt::Error> {
121 let template = azalea_language::get(&self.key).unwrap_or_else(|| {
122 if let Some(fallback) = &self.fallback {
123 fallback.as_str()
124 } else {
125 &self.key
126 }
127 });
128 let mut i = 0;
131 let mut matched = 0;
132
133 let mut built_text = String::new();
136 let mut components = Vec::new();
137
138 while i < template.chars().count() {
139 if template.chars().nth(i).unwrap() == '%' {
140 let Some(char_after) = template.chars().nth(i + 1) else {
141 built_text.push('%');
142 break;
143 };
144 i += 1;
145 match char_after {
146 '%' => {
147 built_text.push('%');
148 }
149 's' => {
150 let arg_component = self
151 .args
152 .get(matched)
153 .cloned()
154 .unwrap_or_else(|| StringOrComponent::String("".to_string()));
155
156 components.push(TextComponent::new(built_text.clone()));
157 built_text.clear();
158 components.push(TextComponent::from(arg_component));
159 matched += 1;
160 }
161 _ => {
162 if let Some(d) = char_after.to_digit(10) {
164 if let Some('$') = template.chars().nth(i + 1) {
166 if let Some('s') = template.chars().nth(i + 2) {
167 i += 2;
168 built_text.push_str(
169 &self
170 .args
171 .get((d - 1) as usize)
172 .unwrap_or(&StringOrComponent::String("".to_string()))
173 .to_string(),
174 );
175 } else {
176 return Err(fmt::Error);
177 }
178 } else {
179 return Err(fmt::Error);
180 }
181 } else {
182 i -= 1;
183 built_text.push('%');
184 }
185 }
186 }
187 } else {
188 built_text.push(template.chars().nth(i).unwrap());
189 }
190
191 i += 1;
192 }
193
194 if components.is_empty() {
195 return Ok(TextComponent::new(built_text));
196 }
197
198 components.push(TextComponent::new(built_text));
199
200 Ok(TextComponent {
201 base: BaseComponent {
202 siblings: components.into_iter().map(FormattedText::Text).collect(),
203 style: Default::default(),
204 },
205 text: "".to_string(),
206 })
207 }
208}
209
210impl Display for TranslatableComponent {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 for component in FormattedText::Translatable(self.clone()).into_iter() {
214 let component_text = match &component {
215 FormattedText::Text(c) => c.text.to_string(),
216 FormattedText::Translatable(c) => match c.read() {
217 Ok(c) => c.to_string(),
218 Err(_) => c.key.to_string(),
219 },
220 };
221
222 f.write_str(&component_text)?;
223 }
224
225 Ok(())
226 }
227}
228
229impl Display for StringOrComponent {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
231 match self {
232 StringOrComponent::String(s) => write!(f, "{s}"),
233 StringOrComponent::FormattedText(c) => write!(f, "{c}"),
234 }
235 }
236}
237
238impl From<StringOrComponent> for TextComponent {
239 fn from(soc: StringOrComponent) -> Self {
240 match soc {
241 StringOrComponent::String(s) => TextComponent::new(s),
242 StringOrComponent::FormattedText(c) => TextComponent::new(c.to_string()),
243 }
244 }
245}
246impl From<&str> for TranslatableComponent {
247 fn from(s: &str) -> Self {
248 TranslatableComponent::new(s.to_string(), vec![])
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_none() {
258 let c = TranslatableComponent::new("translation.test.none".to_string(), vec![]);
259 assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_string());
260 }
261 #[test]
262 fn test_complex() {
263 let c = TranslatableComponent::new(
264 "translation.test.complex".to_string(),
265 vec![
266 StringOrComponent::String("a".to_string()),
267 StringOrComponent::String("b".to_string()),
268 StringOrComponent::String("c".to_string()),
269 StringOrComponent::String("d".to_string()),
270 ],
271 );
272 assert_eq!(
274 c.read().unwrap().to_string(),
275 "Prefix, ab again b and a lastly c and also a again!".to_string()
276 );
277 }
278 #[test]
279 fn test_escape() {
280 let c = TranslatableComponent::new(
281 "translation.test.escape".to_string(),
282 vec![
283 StringOrComponent::String("a".to_string()),
284 StringOrComponent::String("b".to_string()),
285 StringOrComponent::String("c".to_string()),
286 StringOrComponent::String("d".to_string()),
287 ],
288 );
289 assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_string());
290 }
291 #[test]
292 fn test_invalid() {
293 let c = TranslatableComponent::new(
294 "translation.test.invalid".to_string(),
295 vec![
296 StringOrComponent::String("a".to_string()),
297 StringOrComponent::String("b".to_string()),
298 StringOrComponent::String("c".to_string()),
299 StringOrComponent::String("d".to_string()),
300 ],
301 );
302 assert_eq!(c.read().unwrap().to_string(), "hi %".to_string());
303 }
304 #[test]
305 fn test_invalid2() {
306 let c = TranslatableComponent::new(
307 "translation.test.invalid2".to_string(),
308 vec![
309 StringOrComponent::String("a".to_string()),
310 StringOrComponent::String("b".to_string()),
311 StringOrComponent::String("c".to_string()),
312 StringOrComponent::String("d".to_string()),
313 ],
314 );
315 assert_eq!(c.read().unwrap().to_string(), "hi % s".to_string());
316 }
317
318 #[test]
319 fn test_undefined() {
320 let c = TranslatableComponent::new(
321 "translation.test.undefined".to_string(),
322 vec![StringOrComponent::String("a".to_string())],
323 );
324 assert_eq!(
325 c.read().unwrap().to_string(),
326 "translation.test.undefined".to_string()
327 );
328 }
329
330 #[test]
331 fn test_undefined_with_fallback() {
332 let c = TranslatableComponent::with_fallback(
333 "translation.test.undefined".to_string(),
334 Some("translation fallback: %s".to_string()),
335 vec![StringOrComponent::String("a".to_string())],
336 );
337 assert_eq!(
338 c.read().unwrap().to_string(),
339 "translation fallback: a".to_string()
340 );
341 }
342}