1use std::fmt::{self, Display};
2
3use serde::Serialize;
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, Serialize)]
28pub struct TranslatableComponent {
29 #[serde(flatten)]
30 pub base: BaseComponent,
31 #[serde(rename = "translate")]
32 pub key: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub fallback: Option<String>,
35 #[serde(rename = "with")]
36 pub args: Vec<StringOrComponent>,
37}
38
39#[cfg(feature = "simdnbt")]
40fn serialize_args_as_nbt(args: &[StringOrComponent]) -> simdnbt::owned::NbtList {
41 use tracing::debug;
46
47 let mut string_list = Vec::new();
48 let mut compound_list = Vec::new();
49
50 for arg in args {
51 match arg {
52 StringOrComponent::String(s) => {
53 string_list.push(s.clone());
54 }
55 StringOrComponent::FormattedText(c) => {
56 compound_list.push(c.clone().to_compound());
57 }
58 }
59 }
60
61 if !string_list.is_empty() && !compound_list.is_empty() {
62 debug!("Tried to serialize a TranslatableComponent with a mix of strings and components.");
65 return string_list.into();
66 }
67
68 if !string_list.is_empty() {
69 return string_list.into();
70 }
71
72 compound_list.into()
73}
74
75#[cfg(feature = "simdnbt")]
76impl simdnbt::Serialize for TranslatableComponent {
77 fn to_compound(self) -> simdnbt::owned::NbtCompound {
78 let mut compound = simdnbt::owned::NbtCompound::new();
79 compound.insert("translate", self.key);
80 compound.extend(self.base.style.to_compound());
81
82 compound.insert("with", serialize_args_as_nbt(&self.args));
83 compound
84 }
85}
86
87impl TranslatableComponent {
88 pub fn new(key: String, args: Vec<StringOrComponent>) -> Self {
89 Self {
90 base: BaseComponent::new(),
91 key,
92 fallback: None,
93 args,
94 }
95 }
96
97 pub fn with_fallback(
98 key: String,
99 fallback: Option<String>,
100 args: Vec<StringOrComponent>,
101 ) -> Self {
102 Self {
103 base: BaseComponent::new(),
104 key,
105 fallback,
106 args,
107 }
108 }
109
110 pub fn read(&self) -> Result<TextComponent, fmt::Error> {
112 let template = azalea_language::get(&self.key).unwrap_or_else(|| {
113 if let Some(fallback) = &self.fallback {
114 fallback.as_str()
115 } else {
116 &self.key
117 }
118 });
119 let mut i = 0;
122 let mut matched = 0;
123
124 let mut built_text = String::new();
127 let mut components = Vec::new();
128
129 while i < template.chars().count() {
130 if template.chars().nth(i).unwrap() == '%' {
131 let Some(char_after) = template.chars().nth(i + 1) else {
132 built_text.push('%');
133 break;
134 };
135 i += 1;
136 match char_after {
137 '%' => {
138 built_text.push('%');
139 }
140 's' => {
141 let arg_component = self
142 .args
143 .get(matched)
144 .cloned()
145 .unwrap_or_else(|| StringOrComponent::String("".to_string()));
146
147 components.push(TextComponent::new(built_text.clone()));
148 built_text.clear();
149 components.push(TextComponent::from(arg_component));
150 matched += 1;
151 }
152 _ => {
153 if let Some(d) = char_after.to_digit(10) {
155 if let Some('$') = template.chars().nth(i + 1) {
157 if let Some('s') = template.chars().nth(i + 2) {
158 i += 2;
159 built_text.push_str(
160 &self
161 .args
162 .get((d - 1) as usize)
163 .unwrap_or(&StringOrComponent::String("".to_string()))
164 .to_string(),
165 );
166 } else {
167 return Err(fmt::Error);
168 }
169 } else {
170 return Err(fmt::Error);
171 }
172 } else {
173 i -= 1;
174 built_text.push('%');
175 }
176 }
177 }
178 } else {
179 built_text.push(template.chars().nth(i).unwrap());
180 }
181
182 i += 1;
183 }
184
185 if components.is_empty() {
186 return Ok(TextComponent::new(built_text));
187 }
188
189 components.push(TextComponent::new(built_text));
190
191 Ok(TextComponent {
192 base: BaseComponent {
193 siblings: components.into_iter().map(FormattedText::Text).collect(),
194 style: Default::default(),
195 },
196 text: "".to_string(),
197 })
198 }
199}
200
201impl Display for TranslatableComponent {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 for component in FormattedText::Translatable(self.clone()).into_iter() {
205 let component_text = match &component {
206 FormattedText::Text(c) => c.text.to_string(),
207 FormattedText::Translatable(c) => match c.read() {
208 Ok(c) => c.to_string(),
209 Err(_) => c.key.to_string(),
210 },
211 };
212
213 f.write_str(&component_text)?;
214 }
215
216 Ok(())
217 }
218}
219
220impl Display for StringOrComponent {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
222 match self {
223 StringOrComponent::String(s) => write!(f, "{s}"),
224 StringOrComponent::FormattedText(c) => write!(f, "{c}"),
225 }
226 }
227}
228
229impl From<StringOrComponent> for TextComponent {
230 fn from(soc: StringOrComponent) -> Self {
231 match soc {
232 StringOrComponent::String(s) => TextComponent::new(s),
233 StringOrComponent::FormattedText(c) => TextComponent::new(c.to_string()),
234 }
235 }
236}
237impl From<&str> for TranslatableComponent {
238 fn from(s: &str) -> Self {
239 TranslatableComponent::new(s.to_string(), 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_string(), vec![]);
250 assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_string());
251 }
252 #[test]
253 fn test_complex() {
254 let c = TranslatableComponent::new(
255 "translation.test.complex".to_string(),
256 vec![
257 StringOrComponent::String("a".to_string()),
258 StringOrComponent::String("b".to_string()),
259 StringOrComponent::String("c".to_string()),
260 StringOrComponent::String("d".to_string()),
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_string()
267 );
268 }
269 #[test]
270 fn test_escape() {
271 let c = TranslatableComponent::new(
272 "translation.test.escape".to_string(),
273 vec![
274 StringOrComponent::String("a".to_string()),
275 StringOrComponent::String("b".to_string()),
276 StringOrComponent::String("c".to_string()),
277 StringOrComponent::String("d".to_string()),
278 ],
279 );
280 assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_string());
281 }
282 #[test]
283 fn test_invalid() {
284 let c = TranslatableComponent::new(
285 "translation.test.invalid".to_string(),
286 vec![
287 StringOrComponent::String("a".to_string()),
288 StringOrComponent::String("b".to_string()),
289 StringOrComponent::String("c".to_string()),
290 StringOrComponent::String("d".to_string()),
291 ],
292 );
293 assert_eq!(c.read().unwrap().to_string(), "hi %".to_string());
294 }
295 #[test]
296 fn test_invalid2() {
297 let c = TranslatableComponent::new(
298 "translation.test.invalid2".to_string(),
299 vec![
300 StringOrComponent::String("a".to_string()),
301 StringOrComponent::String("b".to_string()),
302 StringOrComponent::String("c".to_string()),
303 StringOrComponent::String("d".to_string()),
304 ],
305 );
306 assert_eq!(c.read().unwrap().to_string(), "hi % s".to_string());
307 }
308
309 #[test]
310 fn test_undefined() {
311 let c = TranslatableComponent::new(
312 "translation.test.undefined".to_string(),
313 vec![StringOrComponent::String("a".to_string())],
314 );
315 assert_eq!(
316 c.read().unwrap().to_string(),
317 "translation.test.undefined".to_string()
318 );
319 }
320
321 #[test]
322 fn test_undefined_with_fallback() {
323 let c = TranslatableComponent::with_fallback(
324 "translation.test.undefined".to_string(),
325 Some("translation fallback: %s".to_string()),
326 vec![StringOrComponent::String("a".to_string())],
327 );
328 assert_eq!(
329 c.read().unwrap().to_string(),
330 "translation fallback: a".to_string()
331 );
332 }
333}