Skip to main content

imessage_database/message_types/
translation.rs

1/*!
2 Translation metadata stored in `message_summary_info`.
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/// Parsed translation metadata for a message.
16#[derive(Debug, PartialEq, Eq)]
17pub struct Translation {
18    /// Translated text.
19    pub translated_text: String,
20    /// Target language identifier.
21    pub translation_lang: String,
22    /// Source language identifier.
23    pub source_lang: String,
24}
25
26impl Translation {
27    /// Parse translation metadata 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        // The summary wraps the translation data in a nested archive.
37        // Yep, this is really how it is designed
38        let translation_data = extract_dict_idx(
39            extract_array_key(
40                extract_dictionary(plist_as_dictionary(&inner_plist)?, "0")?,
41                "0",
42            )?,
43            0,
44        )?;
45
46        Ok(Self {
47            // A translation carries no attachment data, so any object-replacement
48            // placeholders (U+FFFC) mirroring the original's inline stickers are
49            // orphaned noise. Strip them so the text renders cleanly.
50            translated_text: extract_string_key(
51                extract_dictionary(translation_data, "translatedText")?,
52                "NSString",
53            )?
54            .replace('\u{FFFC}', ""),
55            translation_lang: extract_string_key(translation_data, "translationLanguage")?
56                .to_string(),
57            source_lang: extract_string_key(translation_data, "sourceLanguage")?.to_string(),
58        })
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use std::{env::current_dir, fs::File};
65
66    use plist::Value;
67
68    use crate::message_types::translation::Translation;
69
70    #[test]
71    fn test_parse_translation_with_inline_stickers() {
72        // The original message carried two inline stickers (a Memoji + a genmoji)
73        // and a trailing emoji ("This is a test 🫪"). The translation has no
74        // attachment data, so the two sticker placeholders are stripped and only
75        // the text + regular emoji remain.
76        let plist_path = current_dir()
77            .unwrap()
78            .as_path()
79            .join("test_data/app_message/TranslatedWithInlineStickersAndEmojiSummaryInfo.plist");
80        let plist_data = File::open(plist_path).unwrap();
81        let plist = Value::from_reader(plist_data).unwrap();
82
83        let translation = Translation::from_payload(&plist).unwrap();
84
85        assert_eq!(translation.translated_text, "Ceci est un test 🫪");
86        assert_eq!(translation.source_lang, "en_US");
87        assert_eq!(translation.translation_lang, "fr_FR");
88    }
89
90    #[test]
91    fn test_parse_translation() {
92        let plist_path = current_dir()
93            .unwrap()
94            .as_path()
95            .join("test_data/app_message/Translation.plist");
96        let plist_data = File::open(plist_path).unwrap();
97        let plist = Value::from_reader(plist_data).unwrap();
98
99        let translation = Translation::from_payload(&plist).unwrap();
100
101        println!("{:#?}", translation);
102        assert_eq!(
103            translation.translated_text,
104            "Oh, il a traduit ce que j'ai envoyé !"
105        );
106        assert_eq!(translation.source_lang, "en_US");
107        assert_eq!(translation.translation_lang, "fr_FR");
108    }
109
110    #[test]
111    fn test_parse_translation_received() {
112        let plist_path = current_dir()
113            .unwrap()
114            .as_path()
115            .join("test_data/app_message/TranslationReceived.plist");
116        let plist_data = File::open(plist_path).unwrap();
117        let plist = Value::from_reader(plist_data).unwrap();
118
119        let translation = Translation::from_payload(&plist).unwrap();
120
121        println!("{:#?}", translation);
122        assert_eq!(translation.translated_text, "I want chicken");
123        assert_eq!(translation.source_lang, "fr_FR");
124        assert_eq!(translation.translation_lang, "en_US");
125    }
126}