tr/
lib.rs

1/* Copyright (C) 2018 Olivier Goffart <ogoffart@woboq.com>
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4associated documentation files (the "Software"), to deal in the Software without restriction,
5including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
6and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
7subject to the following conditions:
8
9The above copyright notice and this permission notice shall be included in all copies or substantial
10portions of the Software.
11
12THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17*/
18
19#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
20
21//! # Internationalisation helper
22//!
23//! This crate maily expose a macro that wraps gettext in a convinient ways.
24//! See the documentation of the [tr! macro](tr!).
25//!
26//! To translate a rust crate, simply wrap your string within the [`tr!` macro](tr!).
27//! One can then use the `xtr` binary to extract all the translatable from a crate in a `.po`
28//! file. GNU gettext tools can be used to process and translate these strings.
29//!
30//! The tr! macro also support support rust-like formating.
31//!
32//! Example:
33//!
34//! ```
35//! #[macro_use]
36//! extern crate tr;
37//! fn main() {
38//!     // use the tr_init macro to tell gettext where to look for translations
39//! #   #[cfg(feature = "gettext-rs")]
40//!     tr_init!("/usr/share/locale/");
41//!     let folder = if let Some(folder) = std::env::args().nth(1) {
42//!         folder
43//!     } else {
44//!         println!("{}", tr!("Please give folder name"));
45//!         return;
46//!     };
47//!     match std::fs::read_dir(&folder) {
48//!         Err(e) => {
49//!             println!("{}", tr!("Could not read directory '{}'\nError: {}",
50//!                                 folder, e));
51//!         }
52//!         Ok(r) => {
53//!             // Singular/plural formating
54//!             println!("{}", tr!(
55//!                 "The directory {} has one file" | "The directory {} has {n} files" % r.count(),
56//!                 folder
57//!             ));
58//!         }
59//!     }
60//! }
61//! ```
62//!
63//! # Optional Features
64//!
65//! You can change which crate is used as a backend for the translation by setting the features
66//!
67//! - **`gettext-rs`** *(enabled by default)* - This crate wraps the gettext C library
68//! - **`gettext`** - A rust re-implementation of gettext. That crate does not take care of loading the
69//!   right .mo files, so one must use the [`set_translator!] macro with a
70//!   `gettext::Catalog` object
71//!
72//! Additionally, this crate permits loading from `.po` or `.mo` files directly via the [`PoTranslator`] and
73//! [`MoTranslator`] types, guarded beind the respective **`mo-translator`** and **`po-translator`** features.
74
75#[cfg(any(feature = "po-translator", feature = "mo-translator"))]
76mod rspolib_translator;
77#[cfg(any(feature = "po-translator", feature = "mo-translator"))]
78pub use rspolib_translator::MoPoTranslatorLoadError;
79
80#[cfg(feature = "mo-translator")]
81pub use rspolib_translator::MoTranslator;
82#[cfg(feature = "po-translator")]
83pub use rspolib_translator::PoTranslator;
84
85use std::borrow::Cow;
86
87#[doc(hidden)]
88pub mod runtime_format {
89    //! poor man's dynamic formater.
90    //!
91    //! This module create a simple dynamic formater which replaces '{}' or '{n}' with the
92    //! argument.
93    //!
94    //! This does not use the runtime_fmt crate because it needs nightly compiler
95    //!
96    //! TODO: better error reporting and support for more replacement option
97
98    /// Converts the result of the runtime_format! macro into the final String
99    pub fn display_string(format_str: &str, args: &[(&str, &dyn ::std::fmt::Display)]) -> String {
100        use ::std::fmt::Write;
101        let fmt_len = format_str.len();
102        let mut res = String::with_capacity(2 * fmt_len);
103        let mut arg_idx = 0;
104        let mut pos = 0;
105        while let Some(mut p) = format_str[pos..].find(['{', '}']) {
106            if fmt_len - pos < p + 1 {
107                break;
108            }
109            p += pos;
110
111            // Skip escaped }
112            if format_str.get(p..=p) == Some("}") {
113                res.push_str(&format_str[pos..=p]);
114                if format_str.get(p + 1..=p + 1) == Some("}") {
115                    pos = p + 2;
116                } else {
117                    // FIXME! this is an error, it should be reported  ('}' must be escaped)
118                    pos = p + 1;
119                }
120                continue;
121            }
122
123            // Skip escaped {
124            if format_str.get(p + 1..=p + 1) == Some("{") {
125                res.push_str(&format_str[pos..=p]);
126                pos = p + 2;
127                continue;
128            }
129
130            // Find the argument
131            let end = if let Some(end) = format_str[p..].find('}') {
132                end + p
133            } else {
134                // FIXME! this is an error, it should be reported
135                res.push_str(&format_str[pos..=p]);
136                pos = p + 1;
137                continue;
138            };
139            let argument = format_str[p + 1..end].trim();
140            let pa = if p == end - 1 {
141                arg_idx += 1;
142                arg_idx - 1
143            } else if let Ok(n) = argument.parse::<usize>() {
144                n
145            } else if let Some(p) = args.iter().position(|x| x.0 == argument) {
146                p
147            } else {
148                // FIXME! this is an error, it should be reported
149                res.push_str(&format_str[pos..end]);
150                pos = end;
151                continue;
152            };
153
154            // format the part before the '{'
155            res.push_str(&format_str[pos..p]);
156            if let Some(a) = args.get(pa) {
157                write!(&mut res, "{}", a.1)
158                    .expect("a Display implementation returned an error unexpectedly");
159            } else {
160                // FIXME! this is an error, it should be reported
161                res.push_str(&format_str[p..=end]);
162            }
163            pos = end + 1;
164        }
165        res.push_str(&format_str[pos..]);
166        res
167    }
168
169    #[doc(hidden)]
170    /// runtime_format! macro. See runtime_format module documentation.
171    #[macro_export]
172    macro_rules! runtime_format {
173        ($fmt:expr) => {{
174            // TODO! check if 'fmt' does not have {}
175            String::from($fmt)
176        }};
177        ($fmt:expr,  $($tail:tt)* ) => {{
178            $crate::runtime_format::display_string(
179                AsRef::as_ref(&$fmt),
180                $crate::runtime_format!(@parse_args [] $($tail)*),
181            )
182        }};
183
184        (@parse_args [$($args:tt)*]) => { &[ $( $args ),* ]  };
185        (@parse_args [$($args:tt)*] $name:ident) => {
186            $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$name)])
187        };
188        (@parse_args [$($args:tt)*] $name:ident, $($tail:tt)*) => {
189            $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$name)] $($tail)*)
190        };
191        (@parse_args [$($args:tt)*] $name:ident = $e:expr) => {
192            $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$e)])
193        };
194        (@parse_args [$($args:tt)*] $name:ident = $e:expr, $($tail:tt)*) => {
195            $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$e)] $($tail)*)
196        };
197        (@parse_args [$($args:tt)*] $e:expr) => {
198            $crate::runtime_format!(@parse_args [$($args)* ("" , &$e)])
199        };
200        (@parse_args [$($args:tt)*] $e:expr, $($tail:tt)*) => {
201            $crate::runtime_format!(@parse_args [$($args)* ("" , &$e)] $($tail)*)
202        };
203    }
204
205    #[cfg(test)]
206    mod tests {
207        #[test]
208        fn test_format() {
209            assert_eq!(runtime_format!("Hello"), "Hello");
210            assert_eq!(runtime_format!("Hello {}!", "world"), "Hello world!");
211            assert_eq!(runtime_format!("Hello {0}!", "world"), "Hello world!");
212            assert_eq!(
213                runtime_format!("Hello -{1}- -{0}-", 40 + 5, "World"),
214                "Hello -World- -45-"
215            );
216            assert_eq!(
217                runtime_format!(format!("Hello {{}}!"), format!("{}", "world")),
218                "Hello world!"
219            );
220            assert_eq!(
221                runtime_format!("Hello -{}- -{}-", 40 + 5, "World"),
222                "Hello -45- -World-"
223            );
224            assert_eq!(
225                runtime_format!("Hello {name}!", name = "world"),
226                "Hello world!"
227            );
228            let name = "world";
229            assert_eq!(runtime_format!("Hello {name}!", name), "Hello world!");
230            assert_eq!(runtime_format!("{} {}!", "Hello", name), "Hello world!");
231            assert_eq!(runtime_format!("{} {name}!", "Hello", name), "Hello world!");
232            assert_eq!(
233                runtime_format!("{0} {name}!", "Hello", name = "world"),
234                "Hello world!"
235            );
236
237            assert_eq!(
238                runtime_format!("Hello {{0}} {}", "world"),
239                "Hello {0} world"
240            );
241        }
242    }
243}
244
245/// This trait can be implemented by object that can provide a backend for the translation
246///
247/// The backend is only responsable to provide a matching string, the formatting is done
248/// using this string.
249///
250/// The translator for a crate can be set with the [`set_translator!`] macro
251pub trait Translator: Send + Sync {
252    fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str>;
253    fn ntranslate<'a>(
254        &'a self,
255        n: u64,
256        singular: &'a str,
257        plural: &'a str,
258        context: Option<&'a str>,
259    ) -> Cow<'a, str>;
260}
261
262impl<T: Translator> Translator for std::sync::Arc<T> {
263    fn translate<'a>(
264        &'a self,
265        string: &'a str,
266        context: Option<&'a str>,
267    ) -> std::borrow::Cow<'a, str> {
268        <T as Translator>::translate(self, string, context)
269    }
270
271    fn ntranslate<'a>(
272        &'a self,
273        n: u64,
274        singular: &'a str,
275        plural: &'a str,
276        context: Option<&'a str>,
277    ) -> std::borrow::Cow<'a, str> {
278        <T as Translator>::ntranslate(self, n, singular, plural, context)
279    }
280}
281
282#[doc(hidden)]
283pub mod internal {
284
285    use super::Translator;
286    use std::{borrow::Cow, collections::HashMap, sync::LazyLock, sync::RwLock};
287
288    static TRANSLATORS: LazyLock<RwLock<HashMap<&'static str, Box<dyn Translator>>>> =
289        LazyLock::new(Default::default);
290
291    pub fn with_translator<T>(module: &'static str, func: impl FnOnce(&dyn Translator) -> T) -> T {
292        let domain = domain_from_module(module);
293        let def = DefaultTranslator(domain);
294        func(
295            TRANSLATORS
296                .read()
297                .unwrap()
298                .get(domain)
299                .map(|x| &**x)
300                .unwrap_or(&def),
301        )
302    }
303
304    fn domain_from_module(module: &str) -> &str {
305        module.split("::").next().unwrap_or(module)
306    }
307
308    #[cfg(feature = "gettext-rs")]
309    fn mangle_context(ctx: &str, s: &str) -> String {
310        format!("{}\u{4}{}", ctx, s)
311    }
312    #[cfg(feature = "gettext-rs")]
313    fn demangle_context(r: String) -> String {
314        if let Some(x) = r.split('\u{4}').next_back() {
315            return x.to_owned();
316        }
317        r
318    }
319
320    struct DefaultTranslator(&'static str);
321
322    #[cfg(feature = "gettext-rs")]
323    impl Translator for DefaultTranslator {
324        fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str> {
325            Cow::Owned(if let Some(ctx) = context {
326                demangle_context(gettextrs::dgettext(self.0, mangle_context(ctx, string)))
327            } else {
328                gettextrs::dgettext(self.0, string)
329            })
330        }
331
332        fn ntranslate<'a>(
333            &'a self,
334            n: u64,
335            singular: &'a str,
336            plural: &'a str,
337            context: Option<&'a str>,
338        ) -> Cow<'a, str> {
339            let n = n as u32;
340            Cow::Owned(if let Some(ctx) = context {
341                demangle_context(gettextrs::dngettext(
342                    self.0,
343                    mangle_context(ctx, singular),
344                    mangle_context(ctx, plural),
345                    n,
346                ))
347            } else {
348                gettextrs::dngettext(self.0, singular, plural, n)
349            })
350        }
351    }
352
353    #[cfg(not(feature = "gettext-rs"))]
354    impl Translator for DefaultTranslator {
355        fn translate<'a>(&'a self, string: &'a str, _context: Option<&'a str>) -> Cow<'a, str> {
356            Cow::Borrowed(string)
357        }
358
359        fn ntranslate<'a>(
360            &'a self,
361            n: u64,
362            singular: &'a str,
363            plural: &'a str,
364            _context: Option<&'a str>,
365        ) -> Cow<'a, str> {
366            Cow::Borrowed(if n == 1 { singular } else { plural })
367        }
368    }
369
370    #[cfg(feature = "gettext-rs")]
371    pub fn init<T: Into<Vec<u8>>>(module: &'static str, dir: T) {
372        // FIXME: change T from `Into<Vec<u8>> to `Into<PathBuf>`
373        let dir = String::from_utf8(dir.into()).unwrap();
374        // FIXME: don't ignore errors
375        let _ = gettextrs::bindtextdomain(domain_from_module(module), dir);
376
377        static START: std::sync::Once = std::sync::Once::new();
378        START.call_once(|| {
379            gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
380        });
381    }
382
383    pub fn set_translator(module: &'static str, translator: impl Translator + 'static) {
384        let domain = domain_from_module(module);
385        TRANSLATORS
386            .write()
387            .unwrap()
388            .insert(domain, Box::new(translator));
389    }
390
391    pub fn unset_translator(module: &'static str) {
392        let domain = domain_from_module(module);
393        TRANSLATORS.write().unwrap().remove(domain);
394    }
395}
396
397/// Macro used to translate a string.
398///
399/// ```
400/// # #[macro_use] extern crate tr;
401/// // Prints "Hello world!", or a translated version depending on the locale
402/// println!("{}", tr!("Hello world!"));
403/// ```
404///
405/// The string to translate need to be a string literal, as it has to be extracted by
406/// the `xtr` tool. One can add more argument following a subset of rust formating
407///
408/// ```
409/// # #[macro_use] extern crate tr;
410/// let name = "Olivier";
411/// // Prints "Hello, Olivier!",  or a translated version of that.
412/// println!("{}", tr!("Hello, {}!", name));
413/// ```
414///
415/// Plural are using the `"singular" | "plural" % count` syntax. `{n}` will be replaced
416/// by the count.
417///
418/// ```
419/// # #[macro_use] extern crate tr;
420/// let number_of_items = 42;
421/// println!("{}", tr!("There is one item" | "There are {n} items" % number_of_items));
422/// ```
423///
424/// Normal formating rules can also be used:
425///
426/// ```
427/// # #[macro_use] extern crate tr;
428/// let number_of_items = 42;
429/// let folder_name = "/tmp";
430/// println!("{}", tr!("There is one item in folder {}"
431///        | "There are {n} items in folder {}" % number_of_items, folder_name));
432/// ```
433///
434///
435/// If the same string appears several time in the crate, it is necessary to add a
436/// disambiguation context, using the `"context" =>` syntax:
437///
438/// ```
439/// # #[macro_use] extern crate tr;
440/// // These two strings are both "Open" in english, but they may be different in a
441/// // foreign language. Hence, a context string is necessary.
442/// let action_name = tr!("File Menu" => "Open");
443/// let state = tr!("Document State" => "Open");
444/// ```
445///
446/// To enable the translation, one must first call the `tr_init!` macro once in the crate.
447/// To translate the strings, one can use the `xtr` utility to extract the string,
448/// and use the other GNU gettext tools to translate them.
449///
450#[macro_export]
451macro_rules! tr {
452    ($msgid:tt, $($tail:tt)* ) => {
453        $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
454            t.translate($msgid, None), $($tail)*))
455    };
456    ($msgid:tt) => {
457        $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
458            t.translate($msgid, None)))
459    };
460
461    ($msgctx:tt => $msgid:tt, $($tail:tt)* ) => {
462         $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
463            t.translate($msgid, Some($msgctx)), $($tail)*))
464    };
465    ($msgctx:tt => $msgid:tt) => {
466        $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
467            t.translate($msgid, Some($msgctx))))
468    };
469
470    ($msgid:tt | $plur:tt % $n:expr, $($tail:tt)* ) => {{
471        let n = $n;
472        $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
473            t.ntranslate(n as u64, $msgid, $plur, None), $($tail)*, n=n))
474    }};
475    ($msgid:tt | $plur:tt % $n:expr) => {{
476        let n = $n;
477        $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
478            t.ntranslate(n as u64, $msgid, $plur, None), n))
479
480    }};
481
482    ($msgctx:tt => $msgid:tt | $plur:tt % $n:expr, $($tail:tt)* ) => {{
483         let n = $n;
484         $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
485            t.ntranslate(n as u64, $msgid, $plur, Some($msgctx)), $($tail)*, n=n))
486    }};
487    ($msgctx:tt => $msgid:tt | $plur:tt % $n:expr) => {{
488         let n = $n;
489         $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
490            t.ntranslate(n as u64, $msgid, $plur, Some($msgctx)), n))
491    }};
492}
493
494/// Initialize the translation for a crate, using gettext's bindtextdomain
495///
496/// The macro should be called to specify the path in which the .mo files can be looked for.
497/// The argument is the string passed to bindtextdomain
498///
499/// The alternative is to call the set_translator! macro
500///
501/// This macro is available only if the feature "gettext-rs" is enabled
502#[cfg(feature = "gettext-rs")]
503#[macro_export]
504macro_rules! tr_init {
505    ($path:expr) => {
506        $crate::internal::init(module_path!(), $path)
507    };
508}
509
510/// Set the translator to be used for this crate.
511///
512/// The argument needs to be something implementing the [`Translator`] trait
513///
514/// For example, using the gettext crate (if the gettext feature is enabled)
515/// ```ignore
516/// let f = File::open("french.mo").expect("could not open the catalog");
517/// let catalog = Catalog::parse(f).expect("could not parse the catalog");
518/// set_translator!(catalog);
519/// ```
520#[macro_export]
521macro_rules! set_translator {
522    ($translator:expr) => {
523        $crate::internal::set_translator(module_path!(), $translator)
524    };
525}
526
527/// Clears the translator to be used for this crate.
528///
529/// Use this macro to return back to the source language.
530#[macro_export]
531macro_rules! unset_translator {
532    () => {
533        $crate::internal::unset_translator(module_path!())
534    };
535}
536
537#[cfg(feature = "gettext")]
538impl Translator for gettext::Catalog {
539    fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str> {
540        Cow::Borrowed(if let Some(ctx) = context {
541            self.pgettext(ctx, string)
542        } else {
543            self.gettext(string)
544        })
545    }
546    fn ntranslate<'a>(
547        &'a self,
548        n: u64,
549        singular: &'a str,
550        plural: &'a str,
551        context: Option<&'a str>,
552    ) -> Cow<'a, str> {
553        Cow::Borrowed(if let Some(ctx) = context {
554            self.npgettext(ctx, singular, plural, n)
555        } else {
556            self.ngettext(singular, plural, n)
557        })
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    #[test]
564    fn it_works() {
565        assert_eq!(tr!("Hello"), "Hello");
566        assert_eq!(tr!("ctx" => "Hello"), "Hello");
567        assert_eq!(tr!("Hello {}", "world"), "Hello world");
568        assert_eq!(tr!("ctx" => "Hello {}", "world"), "Hello world");
569
570        assert_eq!(
571            tr!("I have one item" | "I have {n} items" % 1),
572            "I have one item"
573        );
574        assert_eq!(
575            tr!("ctx" => "I have one item" | "I have {n} items" % 42),
576            "I have 42 items"
577        );
578        assert_eq!(
579            tr!("{} have one item" | "{} have {n} items" % 42, "I"),
580            "I have 42 items"
581        );
582        assert_eq!(
583            tr!("ctx" => "{0} have one item" | "{0} have {n} items" % 42, "I"),
584            "I have 42 items"
585        );
586
587        assert_eq!(
588            tr!("{} = {}", 255, format_args!("{:#x}", 255)),
589            "255 = 0xff"
590        );
591    }
592}