imessage-database 4.0.0

Parsers and tools to interact with iMessage SQLite data
Documentation
/*!
 Translated messages generated by the iOS 26 (or newer) translations feature.
*/
use plist::Value;
use std::io::Cursor;

use crate::{
    error::plist::PlistParseError,
    util::plist::{
        extract_array_key, extract_bytes_key, extract_dict_idx, extract_dictionary,
        extract_string_key, parse_ns_keyed_archiver, plist_as_dictionary,
    },
};

/// A translated message
#[derive(Debug, PartialEq, Eq)]
pub struct Translation {
    /// The translated text
    pub translated_text: String,
    /// The language the text was translated to
    pub translation_lang: String,
    /// The language the text was translated from
    pub source_lang: String,
}

impl Translation {
    /// Generates a [`Translation`] from a `message_summary_info` payload
    pub fn from_payload(plist: &Value) -> Result<Self, PlistParseError> {
        let translation_data = extract_bytes_key(plist_as_dictionary(plist)?, "tmp")?;

        let inner_plist = parse_ns_keyed_archiver(
            &Value::from_reader(Cursor::new(translation_data))
                .map_err(|_| PlistParseError::NoPayload)?,
        )?;

        // Yep, this is how it is designed
        let translation_data = extract_dict_idx(
            extract_array_key(
                extract_dictionary(plist_as_dictionary(&inner_plist)?, "0")?,
                "0",
            )?,
            0,
        )?;

        Ok(Self {
            translated_text: extract_string_key(
                extract_dictionary(translation_data, "translatedText")?,
                "NSString",
            )?
            .to_string(),
            translation_lang: extract_string_key(translation_data, "translationLanguage")?
                .to_string(),
            source_lang: extract_string_key(translation_data, "sourceLanguage")?.to_string(),
        })
    }
}

#[cfg(test)]
mod tests {
    use std::{env::current_dir, fs::File};

    use plist::Value;

    use crate::message_types::translation::Translation;

    #[test]
    fn test_parse_translation() {
        let plist_path = current_dir()
            .unwrap()
            .as_path()
            .join("test_data/app_message/Translation.plist");
        let plist_data = File::open(plist_path).unwrap();
        let plist = Value::from_reader(plist_data).unwrap();

        let translation = Translation::from_payload(&plist).unwrap();

        println!("{:#?}", translation);
        assert_eq!(
            translation.translated_text,
            "Oh, il a traduit ce que j'ai envoyé !"
        );
        assert_eq!(translation.source_lang, "en_US");
        assert_eq!(translation.translation_lang, "fr_FR");
    }

    #[test]
    fn test_parse_translation_received() {
        let plist_path = current_dir()
            .unwrap()
            .as_path()
            .join("test_data/app_message/TranslationReceived.plist");
        let plist_data = File::open(plist_path).unwrap();
        let plist = Value::from_reader(plist_data).unwrap();

        let translation = Translation::from_payload(&plist).unwrap();

        println!("{:#?}", translation);
        assert_eq!(translation.translated_text, "I want chicken");
        assert_eq!(translation.source_lang, "fr_FR");
        assert_eq!(translation.translation_lang, "en_US");
    }
}