imessage_database/message_types/
translation.rs

1/*!
2 Translated messages generated by the iOS 26 (or newer) translations feature.
3*/
4use plist::Value;
5use std::io::Cursor;
6
7use crate::{
8    error::plist::PlistParseError,
9    util::plist::{
10        extract_array_key, extract_bytes_key, extract_dict_idx, extract_dictionary,
11        extract_string_key, parse_ns_keyed_archiver, plist_as_dictionary,
12    },
13};
14
15/// A translated message
16#[derive(Debug, PartialEq, Eq)]
17pub struct Translation {
18    /// The translated text
19    pub translated_text: String,
20    /// The language the text was translated to
21    pub translation_lang: String,
22    /// The language the text was translated from
23    pub source_lang: String,
24}
25
26impl Translation {
27    /// Generates a [`Translation`] from a `message_summary_info` payload
28    pub fn from_payload(plist: &Value) -> Result<Self, PlistParseError> {
29        let translation_data = extract_bytes_key(plist_as_dictionary(plist)?, "tmp")?;
30
31        let inner_plist = parse_ns_keyed_archiver(
32            &Value::from_reader(Cursor::new(translation_data))
33                .map_err(|_| PlistParseError::NoPayload)?,
34        )?;
35
36        // Yep, this is how it is designed
37        let translation_data = extract_dict_idx(
38            extract_array_key(
39                extract_dictionary(plist_as_dictionary(&inner_plist)?, "0")?,
40                "0",
41            )?,
42            0,
43        )?;
44
45        Ok(Self {
46            translated_text: extract_string_key(
47                extract_dictionary(translation_data, "translatedText")?,
48                "NSString",
49            )?
50            .to_string(),
51            translation_lang: extract_string_key(translation_data, "translationLanguage")?
52                .to_string(),
53            source_lang: extract_string_key(translation_data, "sourceLanguage")?.to_string(),
54        })
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use std::{env::current_dir, fs::File};
61
62    use plist::Value;
63
64    use crate::message_types::translation::Translation;
65
66    #[test]
67    fn test_parse_translation() {
68        let plist_path = current_dir()
69            .unwrap()
70            .as_path()
71            .join("test_data/app_message/Translation.plist");
72        let plist_data = File::open(plist_path).unwrap();
73        let plist = Value::from_reader(plist_data).unwrap();
74
75        let translation = Translation::from_payload(&plist).unwrap();
76
77        println!("{:#?}", translation);
78        assert_eq!(
79            translation.translated_text,
80            "Oh, il a traduit ce que j'ai envoyé !"
81        );
82        assert_eq!(translation.source_lang, "en_US");
83        assert_eq!(translation.translation_lang, "fr_FR");
84    }
85
86    #[test]
87    fn test_parse_translation_received() {
88        let plist_path = current_dir()
89            .unwrap()
90            .as_path()
91            .join("test_data/app_message/TranslationReceived.plist");
92        let plist_data = File::open(plist_path).unwrap();
93        let plist = Value::from_reader(plist_data).unwrap();
94
95        let translation = Translation::from_payload(&plist).unwrap();
96
97        println!("{:#?}", translation);
98        assert_eq!(translation.translated_text, "I want chicken");
99        assert_eq!(translation.source_lang, "fr_FR");
100        assert_eq!(translation.translation_lang, "en_US");
101    }
102}