use std::fmt::{self, Display};
use serde::{Deserialize, Serialize};
#[cfg(feature = "simdnbt")]
use simdnbt::{
ToNbtTag,
owned::{NbtList, NbtTag},
};
use crate::{FormattedText, base_component::BaseComponent, text_component::TextComponent};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
pub enum PrimitiveOrComponent {
Boolean(bool),
Short(i16),
Integer(i32),
Long(i64),
Float(f32),
Double(f64),
String(String),
FormattedText(FormattedText),
}
#[cfg(feature = "simdnbt")]
impl simdnbt::ToNbtTag for PrimitiveOrComponent {
fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
match self {
PrimitiveOrComponent::Boolean(value) => value.to_nbt_tag(),
PrimitiveOrComponent::Short(value) => value.to_nbt_tag(),
PrimitiveOrComponent::Integer(value) => value.to_nbt_tag(),
PrimitiveOrComponent::Long(value) => value.to_nbt_tag(),
PrimitiveOrComponent::Float(value) => value.to_nbt_tag(),
PrimitiveOrComponent::Double(value) => value.to_nbt_tag(),
PrimitiveOrComponent::String(value) => value.to_nbt_tag(),
PrimitiveOrComponent::FormattedText(value) => value.to_nbt_tag(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct TranslatableComponent {
#[serde(flatten)]
pub base: BaseComponent,
#[serde(rename = "translate")]
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback: Option<String>,
#[serde(rename = "with")]
pub args: Vec<PrimitiveOrComponent>,
}
#[cfg(feature = "simdnbt")]
fn serialize_args_as_nbt(args: Vec<PrimitiveOrComponent>) -> NbtList {
let tags = args
.into_iter()
.map(|arg| arg.to_nbt_tag())
.collect::<Vec<NbtTag>>();
NbtList::from(tags)
}
#[cfg(feature = "simdnbt")]
impl simdnbt::Serialize for TranslatableComponent {
fn to_compound(self) -> simdnbt::owned::NbtCompound {
let mut compound = simdnbt::owned::NbtCompound::new();
compound.insert("translate", self.key);
compound.extend(self.base.style.to_compound());
compound.insert("with", serialize_args_as_nbt(self.args));
compound
}
}
impl TranslatableComponent {
pub fn new(key: String, args: Vec<PrimitiveOrComponent>) -> Self {
Self {
base: BaseComponent::new(),
key,
fallback: None,
args,
}
}
pub fn with_fallback(
key: String,
fallback: Option<String>,
args: Vec<PrimitiveOrComponent>,
) -> Self {
Self {
base: BaseComponent::new(),
key,
fallback,
args,
}
}
pub fn read(&self) -> Result<TextComponent, fmt::Error> {
let template = azalea_language::get(&self.key).unwrap_or_else(|| {
if let Some(fallback) = &self.fallback {
fallback.as_str()
} else {
&self.key
}
});
let mut matched = 0;
let mut built_text = String::new();
let mut components = Vec::new();
let mut chars = template.chars();
while let Some(char) = chars.next() {
if char != '%' {
built_text.push(char);
continue;
}
let mut chars_preview = chars.clone();
let Some(char_after) = chars_preview.next() else {
built_text.push('%');
break;
};
match char_after {
'%' => {
chars.next();
built_text.push('%');
}
's' => {
chars.next();
let arg_component = self
.args
.get(matched)
.cloned()
.unwrap_or_else(|| PrimitiveOrComponent::String("".to_owned()));
components.push(TextComponent::new(built_text.clone()));
built_text.clear();
components.push(TextComponent::from(arg_component));
matched += 1;
}
'0'..='9' if let Some(d) = char_after.to_digit(10) => {
chars.next();
let Some('$') = chars.next() else {
return Err(fmt::Error);
};
let Some('s') = chars.next() else {
return Err(fmt::Error);
};
built_text.push_str(
&self
.args
.get((d - 1) as usize)
.unwrap_or(&PrimitiveOrComponent::String("".to_owned()))
.to_string(),
);
}
_ => {
built_text.push('%');
}
}
}
if components.is_empty() {
return Ok(TextComponent::new(built_text));
}
components.push(TextComponent::new(built_text));
Ok(TextComponent {
base: BaseComponent {
siblings: components.into_iter().map(FormattedText::Text).collect(),
style: Default::default(),
},
text: "".to_owned(),
})
}
}
impl Display for TranslatableComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for component in FormattedText::Translatable(self.clone()).into_iter() {
let component_text = match &component {
FormattedText::Text(c) => c.text.to_string(),
FormattedText::Translatable(c) => match c.read() {
Ok(c) => c.to_string(),
Err(_) => c.key.to_string(),
},
};
f.write_str(&component_text)?;
}
Ok(())
}
}
impl Display for PrimitiveOrComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
PrimitiveOrComponent::Boolean(value) => write!(f, "{value}"),
PrimitiveOrComponent::Short(value) => write!(f, "{value}"),
PrimitiveOrComponent::Integer(value) => write!(f, "{value}"),
PrimitiveOrComponent::Long(value) => write!(f, "{value}"),
PrimitiveOrComponent::Float(value) => write!(f, "{value}"),
PrimitiveOrComponent::Double(value) => write!(f, "{value}"),
PrimitiveOrComponent::String(value) => write!(f, "{value}"),
PrimitiveOrComponent::FormattedText(value) => write!(f, "{value}"),
}
}
}
impl From<PrimitiveOrComponent> for TextComponent {
fn from(soc: PrimitiveOrComponent) -> Self {
match soc {
PrimitiveOrComponent::String(value) => TextComponent::new(value),
PrimitiveOrComponent::Boolean(value) => TextComponent::new(value.to_string()),
PrimitiveOrComponent::Short(value) => TextComponent::new(value.to_string()),
PrimitiveOrComponent::Integer(value) => TextComponent::new(value.to_string()),
PrimitiveOrComponent::Long(value) => TextComponent::new(value.to_string()),
PrimitiveOrComponent::Float(value) => TextComponent::new(value.to_string()),
PrimitiveOrComponent::Double(value) => TextComponent::new(value.to_string()),
PrimitiveOrComponent::FormattedText(value) => TextComponent::new(value.to_string()),
}
}
}
impl From<&str> for TranslatableComponent {
fn from(s: &str) -> Self {
TranslatableComponent::new(s.to_owned(), vec![])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_none() {
let c = TranslatableComponent::new("translation.test.none".to_owned(), vec![]);
assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_owned());
}
#[test]
fn test_complex() {
let c = TranslatableComponent::new(
"translation.test.complex".to_owned(),
vec![
PrimitiveOrComponent::String("a".to_owned()),
PrimitiveOrComponent::String("b".to_owned()),
PrimitiveOrComponent::String("c".to_owned()),
PrimitiveOrComponent::String("d".to_owned()),
],
);
assert_eq!(
c.read().unwrap().to_string(),
"Prefix, ab again b and a lastly c and also a again!".to_owned()
);
}
#[test]
fn test_escape() {
let c = TranslatableComponent::new(
"translation.test.escape".to_owned(),
vec![
PrimitiveOrComponent::String("a".to_owned()),
PrimitiveOrComponent::String("b".to_owned()),
PrimitiveOrComponent::String("c".to_owned()),
PrimitiveOrComponent::String("d".to_owned()),
],
);
assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_owned());
}
#[test]
fn test_invalid() {
let c = TranslatableComponent::new(
"translation.test.invalid".to_owned(),
vec![
PrimitiveOrComponent::String("a".to_owned()),
PrimitiveOrComponent::String("b".to_owned()),
PrimitiveOrComponent::String("c".to_owned()),
PrimitiveOrComponent::String("d".to_owned()),
],
);
assert_eq!(c.read().unwrap().to_string(), "hi %".to_owned());
}
#[test]
fn test_invalid2() {
let c = TranslatableComponent::new(
"translation.test.invalid2".to_owned(),
vec![
PrimitiveOrComponent::String("a".to_owned()),
PrimitiveOrComponent::String("b".to_owned()),
PrimitiveOrComponent::String("c".to_owned()),
PrimitiveOrComponent::String("d".to_owned()),
],
);
assert_eq!(c.read().unwrap().to_string(), "hi % s".to_owned());
}
#[test]
fn test_undefined() {
let c = TranslatableComponent::new(
"translation.test.undefined".to_owned(),
vec![PrimitiveOrComponent::String("a".to_owned())],
);
assert_eq!(
c.read().unwrap().to_string(),
"translation.test.undefined".to_owned()
);
}
#[test]
fn test_undefined_with_fallback() {
let c = TranslatableComponent::with_fallback(
"translation.test.undefined".to_owned(),
Some("translation fallback: %s".to_owned()),
vec![PrimitiveOrComponent::String("a".to_owned())],
);
assert_eq!(
c.read().unwrap().to_string(),
"translation fallback: a".to_owned()
);
}
}