gettext_ng/
lib.rs

1//! This crate is a reimplementation
2//! of GNU gettext translation framework in Rust.
3//! It allows your Rust programs to parse out GNU MO files
4//! containing translations and use them in your user interface.
5//!
6//! It contains several differences from the official C implementation.
7//! Notably, this crate does not in any way depend on a global locale
8//! ([2.2](https://www.gnu.org/software/gettext/manual/gettext.html#Setting-the-GUI-Locale))
9//! and does not enforce a directory structure
10//! for storing your translation catalogs
11//! ([11.2.3](https://www.gnu.org/software/gettext/manual/gettext.html#Locating-Catalogs)).
12//! Instead, the choice of translation catalog to use is explicitly made by the user.
13//!
14//! This crate is still in-progress
15//! and may not be on par with the original implementation feature-wise.
16//!
17//! For the exact feature parity see the roadmap in the
18//! [README](https://github.com/justinas/gettext#readme).
19//!
20//! # Example
21//!
22//! ```ignore
23//!
24//! use std::fs::File;
25//! use gettext::Catalog;
26//!
27//! fn main() {
28//!     let f = File::open("french.mo").expect("could not open the catalog");
29//!     let catalog = Catalog::parse(f).expect("could not parse the catalog");
30//!
31//!     // Will print out the French translation
32//!     // if it is found in the parsed file
33//!     // or "Name" otherwise.
34//!     println!("{}", catalog.gettext("Name"));
35//! }
36//! ```
37
38#![warn(clippy::all)]
39// https://pascalhertleif.de/artikel/good-practices-for-writing-rust-libraries/
40#![deny(
41    missing_docs,
42    missing_debug_implementations,
43    trivial_casts,
44    trivial_numeric_casts,
45    unused_import_braces
46)]
47
48mod error;
49mod metadata;
50mod parser;
51mod plurals;
52
53use std::collections::HashMap;
54use std::io::Read;
55use std::ops::Deref;
56
57use crate::parser::default_resolver;
58use crate::plurals::*;
59pub use crate::{error::Error, parser::ParseOptions};
60
61fn key_with_context(context: &str, key: &str) -> String {
62    let mut result = context.to_owned();
63    result.push('\x04');
64    result.push_str(key);
65    result
66}
67
68/// Catalog represents a set of translation strings
69/// parsed out of one MO file.
70#[derive(Clone, Debug)]
71pub struct Catalog {
72    strings: HashMap<String, Message>,
73    resolver: Resolver,
74}
75
76impl Catalog {
77    /// Creates an empty catalog.
78    ///
79    /// All the translated strings will be the same as the original ones.
80    pub fn empty() -> Self {
81        Self::new()
82    }
83
84    /// Creates a new, empty gettext catalog.
85    fn new() -> Self {
86        Catalog {
87            strings: HashMap::new(),
88            resolver: Resolver::Function(default_resolver),
89        }
90    }
91
92    /// Parses a gettext catalog from the given binary MO file.
93    /// Returns the `Err` variant upon encountering an invalid file format
94    /// or invalid byte sequence in strings.
95    ///
96    /// Calling this method is equivalent to calling
97    /// `ParseOptions::new().parse(reader)`.
98    ///
99    /// # Examples
100    ///
101    /// ```ignore
102    /// use gettext::Catalog;
103    /// use std::fs::File;
104    ///
105    /// let file = File::open("french.mo").unwrap();
106    /// let catalog = Catalog::parse(file).unwrap();
107    /// ```
108    pub fn parse<R: Read>(reader: R) -> Result<Self, Error> {
109        ParseOptions::new().parse(reader)
110    }
111
112    fn insert(&mut self, msg: Message) {
113        let key = match msg.context {
114            Some(ref ctxt) => key_with_context(ctxt, &msg.id),
115            None => msg.id.clone(),
116        };
117        self.strings.insert(key, msg);
118    }
119
120    /// Returns the singular translation of `msg_id` from the given catalog
121    /// or `msg_id` itself if a translation does not exist.
122    pub fn gettext<'a>(&'a self, msg_id: &'a str) -> &'a str {
123        self.strings
124            .get(msg_id)
125            .and_then(|msg| msg.get_translated(0))
126            .unwrap_or(msg_id)
127    }
128
129    /// Returns the plural translation of `msg_id` from the given catalog
130    /// with the correct plural form for the number `n` of objects.
131    /// Returns msg_id if a translation does not exist and `n == 1`,
132    /// msg_id_plural otherwise.
133    pub fn ngettext<'a>(&'a self, msg_id: &'a str, msg_id_plural: &'a str, n: u64) -> &'a str {
134        let form_no = self.resolver.resolve(n);
135        let message = self.strings.get(msg_id);
136        match message.and_then(|m| m.get_translated(form_no)) {
137            Some(msg) => msg,
138            None if n == 1 => msg_id,
139            None if n != 1 => msg_id_plural,
140            _ => unreachable!(),
141        }
142    }
143
144    /// Returns the singular translation of `msg_id`
145    /// in the context `msg_context`
146    /// or `msg_id` itself if a translation does not exist.
147    // TODO: DRY gettext/pgettext
148    pub fn pgettext<'a>(&'a self, msg_context: &str, msg_id: &'a str) -> &'a str {
149        let key = key_with_context(msg_context, &msg_id);
150        self.strings
151            .get(&key)
152            .and_then(|msg| msg.get_translated(0))
153            .unwrap_or(msg_id)
154    }
155
156    /// Returns the plural translation of `msg_id`
157    /// in the context `msg_context`
158    /// with the correct plural form for the number `n` of objects.
159    /// Returns msg_id if a translation does not exist and `n == 1`,
160    /// msg_id_plural otherwise.
161    // TODO: DRY ngettext/npgettext
162    pub fn npgettext<'a>(
163        &'a self,
164        msg_context: &str,
165        msg_id: &'a str,
166        msg_id_plural: &'a str,
167        n: u64,
168    ) -> &'a str {
169        let key = key_with_context(msg_context, &msg_id);
170        let form_no = self.resolver.resolve(n);
171        let message = self.strings.get(&key);
172        match message.and_then(|m| m.get_translated(form_no)) {
173            Some(msg) => msg,
174            None if n == 1 => msg_id,
175            None if n != 1 => msg_id_plural,
176            _ => unreachable!(),
177        }
178    }
179
180    /// Returns all known translation strings
181    /// and the gettext translation for it.
182    pub fn alltext<'b: 'a, 'a>(
183        &'b self,
184    ) -> HashMap<&'a str, &'a str> {
185        let mut result = HashMap::<&'a str, &'a str>::with_capacity(self.strings.len());
186        for (key, msg) in self.strings.iter() {
187            result.insert(key, msg.get_translated(0).unwrap_or(key));
188        }
189        result
190    }
191
192    /// Returns all known translation strings
193    /// and all of the ngettext translations for it.
194    pub fn nalltext<'b: 'a, 'a>(
195        &'b self,
196    ) -> HashMap<&'a str, &'a Vec<String>> {
197        let mut result = HashMap::<&'a str, &'a Vec<String>>::with_capacity(self.strings.len());
198        for (key, msg) in self.strings.iter() {
199            result.insert(key, &msg.translated);
200        }
201        result
202    }
203}
204
205#[derive(Clone, Debug, Eq, PartialEq)]
206struct Message {
207    id: String,
208    context: Option<String>,
209    translated: Vec<String>,
210}
211
212impl Message {
213    fn new<T: Into<String>>(id: T, context: Option<T>, translated: Vec<T>) -> Self {
214        Message {
215            id: id.into(),
216            context: context.map(Into::into),
217            translated: translated.into_iter().map(Into::into).collect(),
218        }
219    }
220
221    fn get_translated(&self, form_no: usize) -> Option<&str> {
222        self.translated.get(form_no).map(|s| s.deref())
223    }
224}
225
226#[test]
227fn catalog_impls_send_sync() {
228    fn check<T: Send + Sync>(_: T) {}
229    check(Catalog::new());
230}
231
232#[cfg(test)]
233mod test {
234    use super::*;
235
236    #[test]
237    fn catalog_insert() {
238        let mut cat = Catalog::new();
239        cat.insert(Message::new("thisisid", None, vec![]));
240        cat.insert(Message::new("anotherid", Some("context"), vec![]));
241        let mut keys = cat.strings.keys().collect::<Vec<_>>();
242        keys.sort();
243        assert_eq!(keys, &["context\x04anotherid", "thisisid"])
244    }
245
246    #[test]
247    fn catalog_gettext() {
248        let mut cat = Catalog::new();
249        cat.insert(Message::new("Text", None, vec!["Tekstas"]));
250        cat.insert(Message::new("Image", Some("context"), vec!["Paveikslelis"]));
251        assert_eq!(cat.gettext("Text"), "Tekstas");
252        assert_eq!(cat.gettext("Image"), "Image");
253    }
254
255    #[test]
256    fn catalog_ngettext() {
257        let mut cat = Catalog::new();
258        {
259            // n == 1, no translation
260            assert_eq!(cat.ngettext("Text", "Texts", 1), "Text");
261            // n != 1, no translation
262            assert_eq!(cat.ngettext("Text", "Texts", 0), "Texts");
263            assert_eq!(cat.ngettext("Text", "Texts", 2), "Texts");
264        }
265        {
266            cat.insert(Message::new("Text", None, vec!["Tekstas", "Tekstai"]));
267            // n == 1, translation available
268            assert_eq!(cat.ngettext("Text", "Texts", 1), "Tekstas");
269            // n != 1, translation available
270            assert_eq!(cat.ngettext("Text", "Texts", 0), "Tekstai");
271            assert_eq!(cat.ngettext("Text", "Texts", 2), "Tekstai");
272        }
273    }
274
275    #[test]
276    fn catalog_ngettext_not_enough_forms_in_message() {
277        fn resolver(count: u64) -> usize {
278            count as usize
279        }
280
281        let mut cat = Catalog::new();
282        cat.insert(Message::new("Text", None, vec!["Tekstas", "Tekstai"]));
283        cat.resolver = Resolver::Function(resolver);
284        assert_eq!(cat.ngettext("Text", "Texts", 0), "Tekstas");
285        assert_eq!(cat.ngettext("Text", "Texts", 1), "Tekstai");
286        assert_eq!(cat.ngettext("Text", "Texts", 2), "Texts");
287    }
288
289    #[test]
290    fn catalog_npgettext_not_enough_forms_in_message() {
291        fn resolver(count: u64) -> usize {
292            count as usize
293        }
294
295        let mut cat = Catalog::new();
296        cat.insert(Message::new(
297            "Text",
298            Some("ctx"),
299            vec!["Tekstas", "Tekstai"],
300        ));
301        cat.resolver = Resolver::Function(resolver);
302        assert_eq!(cat.npgettext("ctx", "Text", "Texts", 0), "Tekstas");
303        assert_eq!(cat.npgettext("ctx", "Text", "Texts", 1), "Tekstai");
304        assert_eq!(cat.npgettext("ctx", "Text", "Texts", 2), "Texts");
305    }
306
307    #[test]
308    fn catalog_pgettext() {
309        let mut cat = Catalog::new();
310        cat.insert(Message::new("Text", Some("unit test"), vec!["Tekstas"]));
311        assert_eq!(cat.pgettext("unit test", "Text"), "Tekstas");
312        assert_eq!(cat.pgettext("integration test", "Text"), "Text");
313    }
314
315    #[test]
316    fn catalog_npgettext() {
317        let mut cat = Catalog::new();
318        cat.insert(Message::new(
319            "Text",
320            Some("unit test"),
321            vec!["Tekstas", "Tekstai"],
322        ));
323
324        assert_eq!(cat.npgettext("unit test", "Text", "Texts", 1), "Tekstas");
325        assert_eq!(cat.npgettext("unit test", "Text", "Texts", 0), "Tekstai");
326        assert_eq!(cat.npgettext("unit test", "Text", "Texts", 2), "Tekstai");
327
328        assert_eq!(
329            cat.npgettext("integration test", "Text", "Texts", 1),
330            "Text"
331        );
332        assert_eq!(
333            cat.npgettext("integration test", "Text", "Texts", 0),
334            "Texts"
335        );
336        assert_eq!(
337            cat.npgettext("integration test", "Text", "Texts", 2),
338            "Texts"
339        );
340    }
341}