rspolib/entry/
moentry.rs

1use std::cmp::Ordering;
2use std::fmt;
3
4use crate::entry::{
5    maybe_msgid_msgctxt_eot_split, mo_entry_to_string,
6    EntryCmpByOptions, MsgidEotMsgctxt, POEntry, Translated,
7};
8use crate::traits::Merge;
9
10/// MO file entry representing a message
11///
12/// Unlike PO files, MO files contain only the content
13/// needed to translate a program at runtime, so this
14/// is struct optimized as saves much more memory
15/// than [POEntry].
16///
17/// MO entries ieally contain `msgstr` or the fields
18/// `msgid_plural` and `msgstr_plural` as not being `None`.
19/// The logic would be:
20///
21/// - If `msgstr` is not `None`, then the entry is a
22///   translation of a singular form.
23/// - If `msgid_plural` is not `None`, then the entry
24///   is a translation of a plural form contained in
25///   `msgstr_plural`.
26#[derive(Default, Clone, Debug, PartialEq)]
27pub struct MOEntry {
28    /// untranslated string
29    pub msgid: String,
30    /// translated string
31    pub msgstr: Option<String>,
32    /// untranslated string for plural form
33    pub msgid_plural: Option<String>,
34    /// translated strings for plural form
35    pub msgstr_plural: Vec<String>,
36    /// context
37    pub msgctxt: Option<String>,
38}
39
40impl MOEntry {
41    pub fn new(
42        msgid: String,
43        msgstr: Option<String>,
44        msgid_plural: Option<String>,
45        msgstr_plural: Vec<String>,
46        msgctxt: Option<String>,
47    ) -> MOEntry {
48        MOEntry {
49            msgid,
50            msgstr,
51            msgid_plural,
52            msgstr_plural,
53            msgctxt,
54        }
55    }
56
57    /// Convert to a string representation with a given wrap width
58    pub fn to_string_with_wrapwidth(
59        &self,
60        wrapwidth: usize,
61    ) -> String {
62        mo_entry_to_string(self, wrapwidth, "")
63    }
64
65    /// Compare the current entry with other entry
66    ///
67    /// You can disable some comparison options by setting the corresponding
68    /// field in `options` to `false`. See [EntryCmpByOptions].
69    pub fn cmp_by(
70        &self,
71        other: &Self,
72        options: &EntryCmpByOptions,
73    ) -> Ordering {
74        let placeholder = &"\0".to_string();
75
76        if options.by_msgctxt {
77            let msgctxt = self
78                .msgctxt
79                .as_ref()
80                .unwrap_or(placeholder)
81                .to_string();
82            let other_msgctxt = other
83                .msgctxt
84                .as_ref()
85                .unwrap_or(placeholder)
86                .to_string();
87            if msgctxt > other_msgctxt {
88                return Ordering::Greater;
89            } else if msgctxt < other_msgctxt {
90                return Ordering::Less;
91            }
92        }
93
94        if options.by_msgid_plural {
95            let msgid_plural = self
96                .msgid_plural
97                .as_ref()
98                .unwrap_or(placeholder)
99                .to_string();
100            let other_msgid_plural = other
101                .msgid_plural
102                .as_ref()
103                .unwrap_or(placeholder)
104                .to_string();
105            if msgid_plural > other_msgid_plural {
106                return Ordering::Greater;
107            } else if msgid_plural < other_msgid_plural {
108                return Ordering::Less;
109            }
110        }
111
112        if options.by_msgstr_plural {
113            let mut msgstr_plural = self.msgstr_plural.clone();
114            msgstr_plural.sort();
115            let mut other_msgstr_plural = other.msgstr_plural.clone();
116            other_msgstr_plural.sort();
117            if msgstr_plural > other_msgstr_plural {
118                return Ordering::Greater;
119            } else if msgstr_plural < other_msgstr_plural {
120                return Ordering::Less;
121            }
122        }
123
124        if options.by_msgid {
125            if self.msgid > other.msgid {
126                return Ordering::Greater;
127            } else if self.msgid < other.msgid {
128                return Ordering::Less;
129            }
130        }
131
132        if options.by_msgstr {
133            let msgstr = self
134                .msgstr
135                .as_ref()
136                .unwrap_or(placeholder)
137                .to_string();
138            let other_msgstr = other
139                .msgstr
140                .as_ref()
141                .unwrap_or(placeholder)
142                .to_string();
143            if msgstr > other_msgstr {
144                return Ordering::Greater;
145            } else if msgstr < other_msgstr {
146                return Ordering::Less;
147            }
148        }
149
150        Ordering::Equal
151    }
152}
153
154impl MsgidEotMsgctxt for MOEntry {
155    fn msgid_eot_msgctxt(&self) -> String {
156        maybe_msgid_msgctxt_eot_split(&self.msgid, &self.msgctxt)
157            .to_string()
158    }
159}
160
161impl Translated for MOEntry {
162    /// Returns `true` if the entry is translated
163    ///
164    /// Really, MO files has only translated entries,
165    /// but this function is here to be consistent
166    /// with the PO implementation and to be used
167    /// when manipulating MOEntry directly.
168    fn translated(&self) -> bool {
169        if let Some(msgstr) = &self.msgstr {
170            return !msgstr.is_empty();
171        }
172
173        if self.msgstr_plural.is_empty() {
174            return false;
175        } else {
176            for msgstr_plural in &self.msgstr_plural {
177                if !msgstr_plural.is_empty() {
178                    return true;
179                }
180            }
181        }
182
183        false
184    }
185}
186
187impl Merge for MOEntry {
188    fn merge(&mut self, other: Self) {
189        self.msgid = other.msgid;
190        self.msgstr = other.msgstr;
191        self.msgid_plural = other.msgid_plural;
192        self.msgstr_plural = other.msgstr_plural;
193        self.msgctxt = other.msgctxt;
194    }
195}
196
197impl fmt::Display for MOEntry {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        write!(f, "{}", self.to_string_with_wrapwidth(78))
200    }
201}
202
203impl From<&str> for MOEntry {
204    /// Generates a [MOEntry] from a string as the `msgid`
205    fn from(s: &str) -> Self {
206        MOEntry::new(s.to_string(), None, None, vec![], None)
207    }
208}
209
210impl From<&POEntry> for MOEntry {
211    /// Generates a [MOEntry] from a [POEntry]
212    ///
213    /// Keep in mind that this conversion loss the information
214    /// that is contained in [POEntry]s but not in [MOEntry]s.
215    fn from(entry: &POEntry) -> Self {
216        MOEntry {
217            msgid: entry.msgid.clone(),
218            msgstr: entry.msgstr.clone(),
219            msgid_plural: entry.msgid_plural.clone(),
220            msgstr_plural: entry.msgstr_plural.clone(),
221            msgctxt: entry.msgctxt.clone(),
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn constructor() {
232        let moentry = MOEntry::new(
233            "msgid".to_string(),
234            Some("msgstr".to_string()),
235            None,
236            vec![],
237            None,
238        );
239
240        assert_eq!(moentry.msgid, "msgid");
241        assert_eq!(moentry.msgstr, Some("msgstr".to_string()));
242        assert_eq!(moentry.msgid_plural, None);
243        assert_eq!(moentry.msgstr_plural, vec![] as Vec<String>);
244        assert_eq!(moentry.msgctxt, None);
245    }
246
247    #[test]
248    fn moentry_translated() {
249        // empty msgstr means untranslated
250        let moentry = MOEntry::new(
251            "msgid".to_string(),
252            Some("".to_string()),
253            None,
254            vec![],
255            None,
256        );
257        assert_eq!(moentry.translated(), false);
258
259        let moentry = MOEntry::new(
260            "msgid".to_string(),
261            Some("msgstr".to_string()),
262            None,
263            vec![],
264            None,
265        );
266        assert_eq!(moentry.translated(), true);
267
268        // empty msgstr_plural means untranslated
269        let moentry = MOEntry::new(
270            "msgid".to_string(),
271            None,
272            None,
273            vec![],
274            None,
275        );
276        assert_eq!(moentry.translated(), false);
277
278        // empty msgstr in msgstr_plural means untranslated
279        let moentry = MOEntry::new(
280            "msgid".to_string(),
281            None,
282            None,
283            vec!["".to_string()],
284            None,
285        );
286        assert_eq!(moentry.translated(), false);
287    }
288
289    #[test]
290    fn moentry_merge() {
291        let mut moentry = MOEntry::new(
292            "msgid".to_string(),
293            Some("msgstr".to_string()),
294            Some("msgid_plural".to_string()),
295            vec!["msgstr_plural".to_string()],
296            Some("msgctxt".to_string()),
297        );
298        let other = MOEntry::new(
299            "other_msgid".to_string(),
300            Some("other_msgstr".to_string()),
301            Some("other_msgid_plural".to_string()),
302            vec!["other_msgstr_plural".to_string()],
303            Some("other_msgctxt".to_string()),
304        );
305
306        moentry.merge(other);
307
308        assert_eq!(moentry.msgid, "other_msgid");
309        assert_eq!(moentry.msgstr, Some("other_msgstr".to_string()));
310        assert_eq!(
311            moentry.msgid_plural,
312            Some("other_msgid_plural".to_string())
313        );
314        assert_eq!(
315            moentry.msgstr_plural,
316            vec!["other_msgstr_plural".to_string()],
317        );
318        assert_eq!(
319            moentry.msgctxt,
320            Some("other_msgctxt".to_string())
321        );
322    }
323
324    #[test]
325    fn moentry_to_string() {
326        // with msgid_plural
327        let moentry = MOEntry::new(
328            "msgid".to_string(),
329            Some("msgstr".to_string()),
330            Some("msgid_plural".to_string()),
331            vec!["msgstr_plural".to_string()],
332            Some("msgctxt".to_string()),
333        );
334
335        let expected = r#"msgctxt "msgctxt"
336msgid "msgid"
337msgid_plural "msgid_plural"
338msgstr[0] "msgstr_plural"
339"#
340        .to_string();
341
342        assert_eq!(moentry.to_string(), expected);
343
344        // with msgstr
345        let moentry = MOEntry::new(
346            "msgid".to_string(),
347            Some("msgstr".to_string()),
348            None,
349            vec![],
350            Some("msgctxt".to_string()),
351        );
352
353        let expected = r#"msgctxt "msgctxt"
354msgid "msgid"
355msgstr "msgstr"
356"#
357        .to_string();
358
359        assert_eq!(moentry.to_string(), expected);
360    }
361
362    #[test]
363    fn moentry_from_poentry() {
364        let msgstr_plural = vec!["msgstr_plural".to_string()];
365
366        let mut poentry = POEntry::new(0);
367        poentry.msgid = "msgid".to_string();
368        poentry.msgstr = Some("msgstr".to_string());
369        poentry.msgid_plural = Some("msgid_plural".to_string());
370        poentry.msgstr_plural = msgstr_plural.clone();
371        poentry.msgctxt = Some("msgctxt".to_string());
372
373        let moentry = MOEntry::from(&poentry);
374
375        assert_eq!(moentry.msgid, "msgid");
376        assert_eq!(moentry.msgstr, Some("msgstr".to_string()));
377        assert_eq!(
378            moentry.msgid_plural,
379            Some("msgid_plural".to_string())
380        );
381        assert_eq!(moentry.msgstr_plural, msgstr_plural);
382        assert_eq!(moentry.msgctxt, Some("msgctxt".to_string()));
383    }
384}