From 1a42c08030c865948e9e9b6dc8a1f4e38550063a Mon Sep 17 00:00:00 2001 From: Tert0 Date: Mon, 15 Sep 2025 06:58:00 +0200 Subject: implement translation fallback (#244) --- azalea-chat/src/component.rs | 72 +++++++++++++++++++++++++++++-- azalea-chat/src/translatable_component.rs | 48 ++++++++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs index 62e5e151..c7a69390 100644 --- a/azalea-chat/src/component.rs +++ b/azalea-chat/src/component.rs @@ -295,6 +295,16 @@ impl<'de> Deserialize<'de> for FormattedText { .as_str() .ok_or_else(|| de::Error::custom("\"translate\" must be a string"))? .into(); + let fallback = if let Some(fallback) = json.get("fallback") { + Some( + fallback + .as_str() + .ok_or_else(|| de::Error::custom("\"fallback\" must be a string"))? + .to_string(), + ) + } else { + None + }; if let Some(with) = json.get("with") { let with = with .as_array() @@ -316,13 +326,14 @@ impl<'de> Deserialize<'de> for FormattedText { FormattedText::deserialize(item).map_err(de::Error::custom)?, )); } - component = FormattedText::Translatable(TranslatableComponent::new( - translate, with_array, + component = FormattedText::Translatable(TranslatableComponent::with_fallback( + translate, fallback, with_array, )); } else { // if it doesn't have a "with", just have the with_array be empty - component = FormattedText::Translatable(TranslatableComponent::new( + component = FormattedText::Translatable(TranslatableComponent::with_fallback( translate, + fallback, Vec::new(), )); } @@ -678,3 +689,58 @@ impl Default for FormattedText { FormattedText::Text(TextComponent::default()) } } + +#[cfg(test)] +mod tests { + use serde_json::Value; + + use super::*; + + #[test] + fn deserialize_translation() { + let j: Value = + serde_json::from_str(r#"{"translate": "translation.test.args", "with": ["a", "b"]}"#) + .unwrap(); + let component = FormattedText::deserialize(&j).unwrap(); + assert_eq!( + component, + FormattedText::Translatable(TranslatableComponent::new( + "translation.test.args".to_string(), + vec![ + StringOrComponent::String("a".to_string()), + StringOrComponent::String("b".to_string()) + ] + )) + ); + } + + #[test] + fn deserialize_translation_invalid_arguments() { + let j: Value = + serde_json::from_str(r#"{"translate": "translation.test.args", "with": {}}"#).unwrap(); + assert!(FormattedText::deserialize(&j).is_err()); + } + + #[test] + fn deserialize_translation_fallback() { + let j: Value = serde_json::from_str(r#"{"translate": "translation.test.undefined", "fallback": "fallback: %s", "with": ["a"]}"#).unwrap(); + let component = FormattedText::deserialize(&j).unwrap(); + assert_eq!( + component, + FormattedText::Translatable(TranslatableComponent::with_fallback( + "translation.test.undefined".to_string(), + Some("fallback: %s".to_string()), + vec![StringOrComponent::String("a".to_string())] + )) + ); + } + + #[test] + fn deserialize_translation_invalid_fallback() { + let j: Value = serde_json::from_str( + r#"{"translate": "translation.test.undefined", "fallback": {"text": "invalid"}}"#, + ) + .unwrap(); + assert!(FormattedText::deserialize(&j).is_err()); + } +} diff --git a/azalea-chat/src/translatable_component.rs b/azalea-chat/src/translatable_component.rs index 10c502a8..28700366 100644 --- a/azalea-chat/src/translatable_component.rs +++ b/azalea-chat/src/translatable_component.rs @@ -28,6 +28,7 @@ impl simdnbt::ToNbtTag for StringOrComponent { pub struct TranslatableComponent { pub base: BaseComponent, pub key: String, + pub fallback: Option, pub args: Vec, } @@ -97,13 +98,33 @@ impl TranslatableComponent { Self { base: BaseComponent::new(), key, + fallback: None, + args, + } + } + + pub fn with_fallback( + key: String, + fallback: Option, + args: Vec, + ) -> Self { + Self { + base: BaseComponent::new(), + key, + fallback, args, } } /// Convert the key and args to a FormattedText. pub fn read(&self) -> Result { - let template = azalea_language::get(&self.key).unwrap_or(&self.key); + let template = azalea_language::get(&self.key).unwrap_or_else(|| { + if let Some(fallback) = &self.fallback { + fallback.as_str() + } else { + &self.key + } + }); // decode the % things let mut i = 0; @@ -293,4 +314,29 @@ mod tests { ); assert_eq!(c.read().unwrap().to_string(), "hi % s".to_string()); } + + #[test] + fn test_undefined() { + let c = TranslatableComponent::new( + "translation.test.undefined".to_string(), + vec![StringOrComponent::String("a".to_string())], + ); + assert_eq!( + c.read().unwrap().to_string(), + "translation.test.undefined".to_string() + ); + } + + #[test] + fn test_undefined_with_fallback() { + let c = TranslatableComponent::with_fallback( + "translation.test.undefined".to_string(), + Some("translation fallback: %s".to_string()), + vec![StringOrComponent::String("a".to_string())], + ); + assert_eq!( + c.read().unwrap().to_string(), + "translation fallback: a".to_string() + ); + } } -- cgit v1.2.3