i18n_embed/
gettext.rs

1//! This module contains the types and functions to interact with the
2//! `gettext` localization system.
3//!
4//! Most important is the [GettextLanguageLoader].
5//!
6//! ⚠️ *This module requires the following crate features to be activated: `gettext-system`.*
7
8use crate::{domain_from_module, I18nAssets, I18nEmbedError, LanguageLoader};
9
10pub use i18n_embed_impl::gettext_language_loader;
11
12use gettext as gettext_system;
13use parking_lot::RwLock;
14use unic_langid::LanguageIdentifier;
15
16/// [LanguageLoader] implementation for the `gettext` localization
17/// system.
18///
19/// ⚠️ *This API requires the following crate features to be activated: `gettext-system`.*
20#[derive(Debug)]
21pub struct GettextLanguageLoader {
22    current_language: RwLock<LanguageIdentifier>,
23    module: &'static str,
24    fallback_language: LanguageIdentifier,
25}
26
27impl GettextLanguageLoader {
28    /// Create a new `GettextLanguageLoader`.
29    ///
30    /// # Example
31    ///
32    /// ```
33    /// use i18n_embed::gettext::GettextLanguageLoader;
34    ///
35    /// GettextLanguageLoader::new(module_path!(), "en".parse().unwrap());
36    /// ```
37    pub fn new(module: &'static str, fallback_language: unic_langid::LanguageIdentifier) -> Self {
38        Self {
39            current_language: RwLock::new(fallback_language.clone()),
40            module,
41            fallback_language,
42        }
43    }
44
45    fn load_src_language(&self) {
46        let catalog = gettext_system::Catalog::empty();
47        tr::internal::set_translator(self.module, catalog);
48        *(self.current_language.write()) = self.fallback_language().clone();
49    }
50}
51
52impl LanguageLoader for GettextLanguageLoader {
53    /// The fallback language for the module this loader is responsible
54    /// for.
55    fn fallback_language(&self) -> &LanguageIdentifier {
56        &self.fallback_language
57    }
58
59    /// The domain for the translation that this loader is associated with.
60    fn domain(&self) -> &'static str {
61        domain_from_module(self.module)
62    }
63
64    /// The language file name to use for this loader's domain.
65    fn language_file_name(&self) -> String {
66        format!("{}.mo", self.domain())
67    }
68
69    /// Get the language which is currently loaded for this loader.
70    fn current_language(&self) -> LanguageIdentifier {
71        self.current_language.read().clone()
72    }
73
74    /// Load the languages `language_ids` using the resources packaged
75    /// in the `i18n_assets` in order of fallback preference. This
76    /// also sets the [LanguageLoader::current_language()] to the
77    /// first in the `language_ids` slice. You can use
78    /// [select()](super::select()) to determine which fallbacks are
79    /// actually available for an arbitrary slice of preferences.
80    ///
81    /// **Note:** Gettext doesn't support loading multiple languages
82    /// as multiple fallbacks. We only load the first of the requested
83    /// languages, and the fallback is the src language.
84    #[allow(single_use_lifetimes)]
85    fn load_languages(
86        &self,
87        i18n_assets: &dyn I18nAssets,
88        language_ids: &[unic_langid::LanguageIdentifier],
89    ) -> Result<(), I18nEmbedError> {
90        let language_id = language_ids
91            .iter()
92            .next()
93            .ok_or(I18nEmbedError::RequestedLanguagesEmpty)?;
94
95        if language_id == self.fallback_language() {
96            self.load_src_language();
97            return Ok(());
98        }
99        let (path, files) = self.language_files(language_id, i18n_assets);
100        let file = match files.as_slice() {
101            [first_file] => first_file,
102            [first_file, ..] => {
103                log::warn!(
104                    "Gettext system does not yet support merging language files for {path:?}"
105                );
106                first_file
107            }
108            [] => {
109                log::error!(
110                    target:"i18n_embed::gettext", 
111                    "{} Setting current_language to fallback locale: \"{}\".", 
112                    I18nEmbedError::LanguageNotAvailable(path, language_id.clone()),
113                    self.fallback_language);
114                self.load_src_language();
115                return Ok(());
116            }
117        };
118
119        let catalog = gettext_system::Catalog::parse(&**file).expect("could not parse the catalog");
120        tr::internal::set_translator(self.module, catalog);
121        *(self.current_language.write()) = language_id.clone();
122
123        Ok(())
124    }
125
126    fn reload(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> {
127        self.load_languages(i18n_assets, &[self.current_language()])
128    }
129}