serde_gettext/
lib.rs

1//! Introduction
2//! ============
3//!
4//! This library is only a generic deserializer/API for gettext. With this you can
5//! use JSON or YAML (or "any" format handled by serde) to translate text through
6//! gettext and even format. It also has an API for strftime for formatting dates.
7//!
8//! You can use it in an API service to have a translation endpoint or in a lambda
9//! to translate the input.
10//!
11//!  *  Example in JSON
12//!
13//!     ```json
14//!     {
15//!         "ngettext": {
16//!             "singular": "One item has been deleted",
17//!             "plural": "%(n)s items have been deleted",
18//!             "n": 5
19//!         }
20//!     }
21//!     ```
22//!
23//!  *  Example in YAML
24//!
25//!     ```yaml
26//!     ngettext:
27//!         singular: One item has been deleted
28//!         plural: "%(n)s items have been deleted"
29//!         n: 5
30//!     ```
31//!
32//! When the structure is deserialized, you can simply convert it to a translated
33//! `String`:
34//!
35//! ```rust
36//! use serde_gettext::SerdeGetText;
37//! use std::convert::TryFrom;
38//!
39//! let yaml = r#"---
40//! ngettext:
41//!     singular: One item has been deleted
42//!     plural: "%(n)s items have been deleted"
43//!     n: 5
44//! "#;
45//! let s: SerdeGetText = serde_yaml::from_str(yaml).unwrap();
46//!
47//! assert_eq!(String::try_from(s).unwrap(), "5 items have been deleted");
48//! ```
49//!
50//! Formatting
51//! ==========
52//!
53//!  *  Example in JSON
54//!
55//!     ```json
56//!     {
57//!         "gettext": "Hello %(name)s!",
58//!         "args": {
59//!             "name": "Grace"
60//!         }
61//!     }
62//!     ```
63//!
64//!  *  Example in YAML
65//!
66//!     ```yaml
67//!     gettext: "Hello %(name)s!"
68//!     args:
69//!         name: Grace
70//!     ```
71//!
72//! `args` can handle many different formats and use positional arguments or
73//! keyword arguments:
74//!
75//! ```yaml
76//! gettext: "%s %s %s"
77//! args:
78//!     - true      # "yes" (translated)
79//!     - 3.14      # "3.14"
80//!     -           # "n/a" (translated)
81//! ```
82//!
83//! Output: "yes 3.14 n/a"
84//!
85//! `args` can be added to any function:
86//!
87//! ```yaml
88//! ngettext:
89//!     singular: "%(n)s element deleted (success: %(success)s)"
90//!     plural: "%(n)s elements deleted (success: %(success)s)"
91//!     n: 1
92//! args:
93//!     success: true
94//! ```
95//!
96//! Output: "1 element deleted (success: yes)"
97//!
98//! `args` can handle arrays by joining the items:
99//!
100//! ```yaml
101//! gettext: "%(value)s"
102//! args:
103//!     value:
104//!         - ", "      # The separator
105//!         - true      # "yes" (translated)
106//!         - 3.14      # "3.14"
107//!         -           # "n/a" (translated)
108//! ```
109//!
110//! Output: "yes, 3.14, n/a"
111//!
112//! `args` is recursive and can handle gettext functions:
113//!
114//! ```yaml
115//! gettext: "Last operation status: %(status)s"
116//! args:
117//!     status:
118//!         ngettext:
119//!             singular: "%(n)s element deleted (success: %(success)s)"
120//!             plural: "%(n)s elements deleted (success: %(success)s)"
121//!             n: 1
122//!         args:
123//!             success: true
124//! ```
125//!
126//! Output: "Last operation status: 1 element deleted (success: yes)"
127//!
128//! List of All Available Functions
129//! ===============================
130//!
131//!  *  gettext:
132//!
133//!     ```yaml
134//!     gettext: "msgid"
135//!     ```
136//!
137//!  *  ngettext:
138//!
139//!     ```yaml
140//!     ngettext:
141//!         singular: "msgid_singular"
142//!         plural: "msgid_singular"
143//!         n: 5
144//!     ```
145//!
146//!  *  pgettext:
147//!
148//!     ```yaml
149//!     pgettext:
150//!         ctx: "context"
151//!         msgid: "msgid"
152//!     ```
153//!
154//!  *  dgettext:
155//!
156//!     ```yaml
157//!     dgettext:
158//!         domain: "domain"
159//!         msgid: "msgid"
160//!     ```
161//!
162//!  *  dngettext:
163//!
164//!     ```yaml
165//!     dngettext:
166//!         domain: "domain"
167//!         singular: "msgid_singular"
168//!         plural: "msgid_singular"
169//!         n: 5
170//!     ```
171//!
172//!  *  npgettext:
173//!
174//!     ```yaml
175//!     npgettext:
176//!         ctx: "context"
177//!         singular: "msgid_singular"
178//!         plural: "msgid_singular"
179//!         n: 5
180//!     ```
181//!
182//!  *  dcngettext:
183//!
184//!     ```yaml
185//!     dcngettext:
186//!         domain: "domain"
187//!         singular: "msgid_singular"
188//!         plural: "msgid_singular"
189//!         n: 5
190//!         cateogy: "ctype|numeric|time|collate|monetary|messages|all|paper|name|address|telephone|measurement|identification"
191//!     ```
192//!
193//! Date and Time Formatting
194//! ========================
195//!
196//! You can format date and time in the locale of your choice using strftime:
197//!
198//! ```yaml
199//! strftime: "It is now: %c"
200//! epoch: 1565854615
201//! ```
202//!
203//! Output: "It is now: Thu 15 Aug 2019 09:36:55 CEST"
204//!
205//! You will need to call `set_locale` and `tz_set` from
206//! [libc-strftime](https://docs.rs/libc-strftime/) to
207//! activate the locale and the timezone for your current region.
208//!
209//! If you want to change the locale and timezone for the current process, you
210//! will need to export `TZ` and `LC_ALL` as environment variable first, then call
211//! `set_locale` and `tz_set` again.
212
213#![deny(missing_docs)]
214
215#[macro_use]
216extern crate serde_derive;
217#[macro_use]
218extern crate derive_error;
219
220use dynfmt::{Argument, Format, FormatArgs, PythonFormat};
221use libc_strftime::strftime_local;
222#[allow(unused_imports)]
223use serde::Deserialize;
224use std::collections::HashMap;
225use std::convert::TryFrom;
226use std::string::ToString;
227
228/// Runtime error that occurs when the input cannot be formatted
229#[derive(Debug, Error)]
230pub enum Error {
231    /// Formatting error
232    #[error(msg_embedded, no_from, non_std)]
233    FormatError(String),
234    /// Missing join separator
235    #[error(non_std, no_from, display = "missing join separator")]
236    MissingJoinSeparator,
237}
238
239/// A deserializable struct to translate and format
240#[derive(Deserialize, Clone, Debug)]
241pub struct SerdeGetText {
242    #[serde(flatten)]
243    value: Value,
244    /// Base arguments that can be provided for keywords format
245    #[serde(skip)]
246    pub args: HashMap<String, String>,
247}
248
249impl TryFrom<SerdeGetText> for String {
250    type Error = Error;
251
252    fn try_from(x: SerdeGetText) -> Result<String, Error> {
253        x.value.try_into_string(&x.args)
254    }
255}
256
257impl TryFrom<Box<SerdeGetText>> for String {
258    type Error = Error;
259
260    fn try_from(x: Box<SerdeGetText>) -> Result<String, Error> {
261        String::try_from(*x)
262    }
263}
264
265#[derive(Deserialize, Clone, Debug)]
266#[serde(untagged)]
267enum Value {
268    Text(String),
269    Integer(i64),
270    Float(f64),
271    Bool(bool),
272    Unit(()),
273    Datetime(DatetimeValue),
274    Array(Vec<Value>),
275    FormattedText {
276        text: String,
277        args: Option<Formatter>,
278    },
279    GetText {
280        gettext: ValueGetText,
281        args: Option<Formatter>,
282    },
283    NGetText {
284        ngettext: ValueNGetText,
285        args: Option<Formatter>,
286    },
287    PGetText {
288        pgettext: ValuePGetText,
289        args: Option<Formatter>,
290    },
291    DGetText {
292        dgettext: ValueDGetText,
293        args: Option<Formatter>,
294    },
295    DNGetText {
296        dngettext: ValueDNGetText,
297        args: Option<Formatter>,
298    },
299    NPGetText {
300        npgettext: ValueNPGetText,
301        args: Option<Formatter>,
302    },
303    DCNGetText {
304        dcngettext: ValueDCNGetText,
305        args: Option<Formatter>,
306    },
307}
308
309macro_rules! handle_gettext {
310    ($s:expr, $args:expr, $map:expr, $base_map:expr) => {{
311        Self::format(&$s.to_string(), $args, $map, $base_map)
312    }};
313}
314
315macro_rules! handle_plural {
316    ($s:expr, $args:expr, $map:expr, $base_map:expr) => {{
317        $map.reserve(match $args.as_ref() {
318            Some(Formatter::KeywordArgs(args)) => args.len() + 1,
319            _ => 1,
320        });
321        $map.insert("n".to_string(), $s.n.to_string());
322
323        Self::format(&$s.to_string(), $args, $map, $base_map)
324    }};
325}
326
327impl Value {
328    fn try_into_string(self, base_map: &HashMap<String, String>) -> Result<String, Error> {
329        let mut map = HashMap::new();
330
331        match self {
332            Value::Text(x) => Ok(x.to_string()),
333            Value::Integer(x) => Ok(x.to_string()),
334            Value::Float(x) => Ok(x.to_string()),
335            Value::Bool(x) => Ok(if x {
336                gettextrs::gettext(b"yes" as &[u8])
337            } else {
338                gettextrs::gettext(b"no" as &[u8])
339            }),
340            Value::Unit(()) => Ok(gettextrs::gettext(b"n/a" as &[u8])),
341            Value::Datetime(x) => Ok(x.to_string()),
342            Value::Array(xs) => Ok({
343                let mut it = xs.into_iter();
344                let sep: String = match it.next() {
345                    Some(x) => x.try_into_string(base_map),
346                    None => Err(Error::MissingJoinSeparator),
347                }?;
348
349                let mut vec: Vec<String> = Vec::new();
350
351                for value in it {
352                    vec.push(value.try_into_string(base_map)?);
353                }
354
355                vec.join(&sep)
356            }),
357            Value::FormattedText { text, args } => Self::format(text.as_ref(), args, map, base_map),
358            Value::GetText { gettext, args } => handle_gettext!(gettext, args, map, base_map),
359            Value::NGetText { ngettext, args } => handle_plural!(ngettext, args, map, base_map),
360            Value::PGetText { pgettext, args } => handle_gettext!(pgettext, args, map, base_map),
361            Value::DGetText { dgettext, args } => handle_gettext!(dgettext, args, map, base_map),
362            Value::DNGetText { dngettext, args } => handle_plural!(dngettext, args, map, base_map),
363            Value::NPGetText { npgettext, args } => handle_plural!(npgettext, args, map, base_map),
364            Value::DCNGetText { dcngettext, args } => {
365                handle_plural!(dcngettext, args, map, base_map)
366            }
367        }
368    }
369
370    fn format(
371        message: &str,
372        formatter: Option<Formatter>,
373        mut map: HashMap<String, String>,
374        base_map: &HashMap<String, String>,
375    ) -> Result<String, Error> {
376        match formatter {
377            Some(Formatter::KeywordArgs(kwargs)) => {
378                for (key, value) in kwargs.into_iter() {
379                    map.insert(key, value.try_into_string(base_map)?);
380                }
381
382                PythonFormat
383                    .format(message, UnionMap::new(&map, base_map))
384                    .map_err(|err| Error::FormatError(format!("{}", err)))
385                    .map(|x| x.to_string())
386            }
387            Some(Formatter::PositionalArgs(args)) => PythonFormat
388                .format(
389                    message,
390                    args.into_iter()
391                        .map(|x| x.try_into_string(base_map))
392                        .collect::<Result<Vec<String>, _>>()?,
393                )
394                .map_err(|err| Error::FormatError(format!("{}", err)))
395                .map(|x| x.to_string()),
396            None => PythonFormat
397                .format(message, UnionMap::new(&map, base_map))
398                .map_err(|err| Error::FormatError(format!("{}", err)))
399                .map(|x| x.to_string()),
400        }
401    }
402}
403
404#[derive(Deserialize, Clone, Debug)]
405#[serde(untagged)]
406enum Formatter {
407    KeywordArgs(HashMap<String, Value>),
408    PositionalArgs(Vec<Value>),
409}
410
411struct UnionMap<'a>(&'a HashMap<String, String>, &'a HashMap<String, String>);
412
413impl<'a> UnionMap<'a> {
414    fn new(a: &'a HashMap<String, String>, b: &'a HashMap<String, String>) -> UnionMap<'a> {
415        UnionMap(a, b)
416    }
417}
418
419impl FormatArgs for UnionMap<'_> {
420    fn get_key(&self, key: &str) -> Result<Option<Argument<'_>>, ()> {
421        Ok(self
422            .0
423            .get(key)
424            .or_else(|| self.1.get(key))
425            .map(|x| x as Argument<'_>))
426    }
427}
428
429#[derive(Deserialize, Clone, Debug)]
430struct DatetimeValue {
431    strftime: String,
432    epoch: i64,
433}
434
435impl ToString for DatetimeValue {
436    fn to_string(&self) -> String {
437        strftime_local(&self.strftime, self.epoch)
438    }
439}
440
441#[derive(Deserialize, Clone, Debug)]
442struct ValueGetText(String);
443
444impl ToString for ValueGetText {
445    fn to_string(&self) -> String {
446        gettextrs::gettext(self.0.as_bytes())
447    }
448}
449
450#[derive(Deserialize, Clone, Debug)]
451struct ValueNGetText {
452    singular: String,
453    plural: String,
454    n: u32,
455}
456
457impl ToString for ValueNGetText {
458    fn to_string(&self) -> String {
459        gettextrs::ngettext(self.singular.as_bytes(), self.plural.as_bytes(), self.n)
460    }
461}
462
463#[derive(Deserialize, Clone, Debug)]
464struct ValuePGetText {
465    ctx: String,
466    msgid: String,
467}
468
469impl ToString for ValuePGetText {
470    fn to_string(&self) -> String {
471        gettextrs::pgettext(self.ctx.as_bytes(), self.msgid.as_bytes())
472    }
473}
474
475#[derive(Deserialize, Clone, Debug)]
476struct ValueDGetText {
477    domain: String,
478    msgid: String,
479}
480
481impl ToString for ValueDGetText {
482    fn to_string(&self) -> String {
483        gettextrs::dgettext(self.domain.as_bytes(), self.msgid.as_bytes())
484    }
485}
486
487#[derive(Deserialize, Clone, Debug)]
488struct ValueDNGetText {
489    domain: String,
490    singular: String,
491    plural: String,
492    n: u32,
493}
494
495impl ToString for ValueDNGetText {
496    fn to_string(&self) -> String {
497        gettextrs::dngettext(
498            self.domain.as_bytes(),
499            self.singular.as_bytes(),
500            self.plural.as_bytes(),
501            self.n,
502        )
503    }
504}
505
506#[derive(Deserialize, Clone, Debug)]
507struct ValueNPGetText {
508    ctx: String,
509    singular: String,
510    plural: String,
511    n: u32,
512}
513
514impl ToString for ValueNPGetText {
515    fn to_string(&self) -> String {
516        gettextrs::npgettext(
517            self.ctx.as_bytes(),
518            self.singular.as_bytes(),
519            self.plural.as_bytes(),
520            self.n,
521        )
522    }
523}
524
525#[derive(Deserialize, Clone, Debug)]
526struct ValueDCNGetText {
527    domain: String,
528    singular: String,
529    plural: String,
530    n: u32,
531    category: LocaleCategory,
532}
533
534#[derive(Deserialize, Debug, PartialEq, Clone, Copy)]
535#[allow(clippy::enum_variant_names)]
536enum LocaleCategory {
537    #[serde(rename = "ctype")]
538    LcCType,
539    #[serde(rename = "numeric")]
540    LcNumeric,
541    #[serde(rename = "time")]
542    LcTime,
543    #[serde(rename = "collate")]
544    LcCollate,
545    #[serde(rename = "monetary")]
546    LcMonetary,
547    #[serde(rename = "messages")]
548    LcMessages,
549    #[serde(rename = "all")]
550    LcAll,
551    #[serde(rename = "paper")]
552    LcPaper,
553    #[serde(rename = "name")]
554    LcName,
555    #[serde(rename = "address")]
556    LcAddress,
557    #[serde(rename = "telephone")]
558    LcTelephone,
559    #[serde(rename = "measurement")]
560    LcMeasurement,
561    #[serde(rename = "identification")]
562    LcIdentification,
563}
564
565impl std::convert::From<LocaleCategory> for gettextrs::LocaleCategory {
566    fn from(category: LocaleCategory) -> Self {
567        use gettextrs::LocaleCategory::*;
568
569        match category {
570            LocaleCategory::LcCType => LcCType,
571            LocaleCategory::LcNumeric => LcNumeric,
572            LocaleCategory::LcTime => LcTime,
573            LocaleCategory::LcCollate => LcCollate,
574            LocaleCategory::LcMonetary => LcMonetary,
575            LocaleCategory::LcMessages => LcMessages,
576            LocaleCategory::LcAll => LcAll,
577            LocaleCategory::LcPaper => LcPaper,
578            LocaleCategory::LcName => LcName,
579            LocaleCategory::LcAddress => LcAddress,
580            LocaleCategory::LcTelephone => LcTelephone,
581            LocaleCategory::LcMeasurement => LcMeasurement,
582            LocaleCategory::LcIdentification => LcIdentification,
583        }
584    }
585}
586
587impl ToString for ValueDCNGetText {
588    fn to_string(&self) -> String {
589        gettextrs::dcngettext(
590            self.domain.as_bytes(),
591            self.singular.as_bytes(),
592            self.plural.as_bytes(),
593            self.n,
594            self.category.into(),
595        )
596    }
597}