i18n_embed/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2//! Traits and macros to conveniently embed localization assets into
3//! your application binary or library in order to localize it at
4//! runtime. Works in unison with
5//! [cargo-i18n](https://crates.io/crates/cargo_i18n).
6//!
7//! This library recommends that you make use of
8//! [rust-embed](https://crates.io/crates/rust-embed) to perform the
9//! actual embedding of the language files, unfortunately using this
10//! currently requires you to manually add it as a dependency to your
11//! project and implement its trait on your struct in addition to
12//! [I18nAssets](I18nAssets). `RustEmbed` will not compile if the
13//! target `folder` path is invalid, so it is recommended to either
14//! run `cargo i18n` before building your project, or committing the
15//! localization assets into source control to ensure that the the
16//! folder exists and project can build without requiring `cargo
17//! i18n`.
18//!
19//! # Optional Features
20//!
21//! The `i18n-embed` crate has the following optional Cargo features:
22//!
23//! + `rust-embed` (Enabled by default)
24//!   + Enable an automatic implementation of [I18nAssets] for any
25//!     type that also implements `RustEmbed`.
26//! + `fluent-system`
27//!   + Enable support for the
28//!     [fluent](https://www.projectfluent.org/) localization system
29//!     via the `fluent::FluentLanguageLoader` in this crate.
30//! + `gettext-system`
31//!   + Enable support for the
32//!     [gettext](https://www.gnu.org/software/gettext/) localization
33//!     system using the [tr macro](https://docs.rs/tr/0.1.3/tr/) and
34//!     the [gettext crate](https://docs.rs/gettext/0.4.0/gettext/)
35//!     via the `gettext::GettextLanguageLoader` in this crate.
36//! + `desktop-requester`
37//!   + Enables a convenience implementation of
38//!     [LanguageRequester](LanguageRequester) trait called
39//!     `DesktopLanguageRequester for the desktop platform (windows,
40//!     mac, linux), which makes use of the
41//!     [sys-locale](https://crates.io/crates/sys-locale) crate
42//!     for resolving the current system locale.
43//! + `web-sys-requester`
44//!   + Enables a convenience implementation of
45//!     [LanguageRequester](LanguageRequester) trait called
46//!     `WebLanguageRequester` which makes use of the
47//!     [web-sys](https://crates.io/crates/web-sys) crate for
48//!     resolving the language being requested by the user's web
49//!     browser in a WASM context.
50//!
51//! # Examples
52//!
53//! ## Fluent Localization System
54//!
55//! The following is a simple example for how to localize your binary
56//! using this library when it first runs, using the `fluent`
57//! localization system, directly instantiating the
58//! `FluentLanguageLoader`.
59//!
60//! First you'll need the following features enabled in your
61//! `Cargo.toml`:
62//!
63//! ```toml
64//! [dependencies]
65//! i18n-embed = { version = "VERSION", features = ["fluent-system", "desktop-requester"]}
66//! rust-embed = "8"
67//! ```
68//!
69//! Set up a minimal `i18n.toml` in your crate root to use with
70//! `cargo-i18n` (see [cargo
71//! i18n](https://github.com/kellpossible/cargo-i18n#configuration)
72//! for more information on the configuration file format):
73//!
74//! ```toml
75//! # (Required) The language identifier of the language used in the
76//! # source code for gettext system, and the primary fallback language
77//! # (for which all strings must be present) when using the fluent
78//! # system.
79//! fallback_language = "en-GB"
80//!
81//! # Use the fluent localization system.
82//! [fluent]
83//! # (Required) The path to the assets directory.
84//! # The paths inside the assets directory should be structured like so:
85//! # `assets_dir/{language}/{domain}.ftl`
86//! assets_dir = "i18n"
87//! ```
88//!
89//! Next, you want to create your localization resources, per language
90//! fluent (`.ftl`) files. `language` needs to conform to the [Unicode
91//! Language
92//! Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
93//! standard, and will be parsed via the [unic_langid
94//! crate](https://docs.rs/unic-langid/0.9.0/unic_langid/):
95//!
96//! ```txt
97//! my_crate/
98//!   Cargo.toml
99//!   i18n.toml
100//!   src/
101//!   i18n/
102//!     {language}/
103//!       {domain}.ftl
104//! ```
105//!
106//! Then in your Rust code:
107//!
108//! ```
109//! # #[cfg(all(feature = "desktop-requester", feature = "fluent-system"))]
110//! # {
111//! use i18n_embed::{DesktopLanguageRequester, fluent::{
112//!     FluentLanguageLoader, fluent_language_loader
113//! }};
114//! use rust_embed::RustEmbed;
115//!
116//! #[derive(RustEmbed)]
117//! #[folder = "i18n"] // path to the compiled localization resources
118//! struct Localizations;
119//!
120//! # #[allow(dead_code)]
121//! fn main() {
122//!     let language_loader: FluentLanguageLoader = fluent_language_loader!();
123//!
124//!     // Use the language requester for the desktop platform (linux, windows, mac).
125//!     // There is also a requester available for the web-sys WASM platform called
126//!     // WebLanguageRequester, or you can implement your own.
127//!     let requested_languages = DesktopLanguageRequester::requested_languages();
128//!
129//!     let _result = i18n_embed::select(
130//!         &language_loader, &Localizations, &requested_languages);
131//!
132//!     // continue on with your application
133//! }
134//! # }
135//! ```
136//!
137//! To access localizations, you can use `FluentLanguageLoader`'s
138//! methods directly, or, for added compile-time checks/safety, you
139//! can use the [fl!() macro](https://crates.io/crates/i18n-embed-fl).
140//!
141//! Having an `i18n.toml` configuration file enables you to do the
142//! following:
143//!
144//! + Use the [cargo i18n](https://crates.io/crates/cargo-i18n) tool
145//!   to perform validity checks (not yet implemented).
146//! + Integrate with a code-base using the `gettext` localization
147//!   system.
148//! + Use the `fluent::fluent_language_loader!()` macro to pull the
149//!   configuration in at compile time to create the
150//!   `fluent::FluentLanguageLoader`.
151//! + Use the [fl!() macro](https://crates.io/crates/i18n-embed-fl) to
152//!   have added compile-time safety when accessing messages.
153//!
154//! ## Gettext Localization System
155//!
156//! The following is a simple example for how to localize your binary
157//! using this library when it first runs, using the `gettext`
158//! localization system. Please note that the `gettext` localization
159//! system is technically inferior to `fluent` [in a number of
160//! ways](https://github.com/projectfluent/fluent/wiki/Fluent-vs-gettext),
161//! however there are always legacy reasons, and the
162//! developer/translator ecosystem around `gettext` is mature.
163//!
164//! The `gettext::GettextLanguageLoader` in this example is
165//! instantiated using the `gettext::gettext_language_loader!()`
166//! macro, which automatically determines the correct module for the
167//! crate, and pulls settings in from the `i18n.toml` configuration
168//! file.
169//!
170//! First you'll need the following features enabled in your
171//! `Cargo.toml`:
172//!
173//! ```toml
174//! [dependencies]
175//! i18n-embed = { version = "VERSION", features = ["gettext-system", "desktop-requester"]}
176//! rust-embed = "8"
177//! ```
178//!
179//! Set up a minimal `i18n.toml` in your crate root to use with
180//! `cargo-i18n` (see [cargo
181//! i18n](https://github.com/kellpossible/cargo-i18n#configuration)
182//! for more information on the configuration file format):
183//!
184//! ```toml
185//! # (Required) The language identifier of the language used in the
186//! # source code for gettext system, and the primary fallback language
187//! # (for which all strings must be present) when using the fluent
188//! # system.
189//! fallback_language = "en"
190//!
191//! # Use the gettext localization system.
192//! [gettext]
193//! # (Required) The languages that the software will be translated into.
194//! target_languages = ["es"]
195//!
196//! # (Required) Path to the output directory, relative to `i18n.toml` of
197//! # the crate being localized.
198//! output_dir = "i18n"
199//! ```
200//!
201//! Install and run [cargo i18n](https://crates.io/crates/cargo-i18n)
202//! for your crate to generate the language specific `po` and `mo`
203//! files, ready to be translated. It is recommended to add the
204//! `i18n/pot` folder to your repository gitignore.
205//!
206//! Then in your Rust code:
207//!
208//! ```
209//! # #[cfg(all(feature = "gettext-system", feature = "desktop-requester"))]
210//! # {
211//! use i18n_embed::{DesktopLanguageRequester, gettext::{
212//!     gettext_language_loader
213//! }};
214//! use rust_embed::RustEmbed;
215//!
216//! #[derive(RustEmbed)]
217//! // path to the compiled localization resources,
218//! // as determined by i18n.toml settings
219//! #[folder = "i18n/mo"]
220//! struct Localizations;
221//!
222//! # #[allow(dead_code)]
223//! fn main() {
224//!     // Create the GettextLanguageLoader, pulling in settings from `i18n.toml`
225//!     // at compile time using the macro.
226//!     let language_loader = gettext_language_loader!();
227//!
228//!     // Use the language requester for the desktop platform (linux, windows, mac).
229//!     // There is also a requester available for the web-sys WASM platform called
230//!     // WebLanguageRequester, or you can implement your own.
231//!     let requested_languages = DesktopLanguageRequester::requested_languages();
232//!
233//!     let _result = i18n_embed::select(
234//!         &language_loader, &Localizations, &requested_languages);
235//!
236//!     // continue on with your application
237//! }
238//! # }
239//! ```
240//!
241//! ## Automatic Updating Selection
242//!
243//! Depending on the platform, you can also make use of the
244//! [LanguageRequester](LanguageRequester)'s ability to monitor
245//! changes to the currently requested language, and automatically
246//! update the selected language using a [Localizer](Localizer):
247//!
248//! ```
249//! # #[cfg(all(feature = "fluent-system", feature = "desktop-requester"))]
250//! # {
251//! use std::sync::{Arc, OnceLock};
252//! use i18n_embed::{
253//!     DesktopLanguageRequester, LanguageRequester,
254//!     DefaultLocalizer, Localizer, fluent::FluentLanguageLoader     
255//! };
256//! use rust_embed::RustEmbed;
257//! use unic_langid::LanguageIdentifier;
258//!
259//! #[derive(RustEmbed)]
260//! #[folder = "i18n/ftl"] // path to localization resources
261//! struct Localizations;
262//!
263//! pub fn language_loader() -> &'static FluentLanguageLoader {
264//!     static LANGUAGE_LOADER: OnceLock<FluentLanguageLoader> = OnceLock::new();
265//!
266//!     LANGUAGE_LOADER.get_or_init(|| {
267//!         // Usually you could use the fluent_language_loader!() macro
268//!         // to pull values from i18n.toml configuration and current
269//!         // module here at compile time, but instantiating the loader
270//!         // manually here instead so the example compiles.
271//!         let fallback: LanguageIdentifier = "en-US".parse().unwrap();
272//!         FluentLanguageLoader::new("test", fallback)
273//!     })
274//! }
275//!
276//! # #[allow(dead_code)]
277//! fn main() {
278//!     let localizer = DefaultLocalizer::new(&*language_loader(), &Localizations);
279//!
280//!     let localizer_arc: Arc<dyn Localizer> = Arc::new(localizer);
281//!
282//!     let mut language_requester = DesktopLanguageRequester::new();
283//!     language_requester.add_listener(Arc::downgrade(&localizer_arc));
284//!
285//!     // Manually check the currently requested system language,
286//!     // and update the listeners. NOTE: Support for this across systems
287//!     // currently varies, it may not change when the system requested
288//!     // language changes during runtime without restarting your application.
289//!     // In the future some platforms may also gain support for
290//!     // automatic triggering when the requested display language changes.
291//!     language_requester.poll().unwrap();
292//!
293//!     // continue on with your application
294//! }
295//! # }
296//! ```
297//!
298//! The above example makes use of the
299//! [DefaultLocalizer](DefaultLocalizer), but you can also implement
300//! the [Localizer](Localizer) trait yourself for a custom solution.
301//! It also makes use of
302//! [OnceLock](https://doc.rust-lang.org/beta/std/sync/struct.OnceLock.html) to allow the
303//! [LanguageLoader](LanguageLoader) implementation to be stored
304//! statically, because its constructor is not `const`.
305//!
306//! ## Localizing Libraries
307//!
308//! If you wish to create a localizable library using `i18n-embed`,
309//! you can follow this code pattern in the library itself:
310//!
311//! ```
312//! # #[cfg(feature = "fluent-system")]
313//! # {
314//! use std::sync::{Arc, OnceLock};
315//! use i18n_embed::{
316//!     DefaultLocalizer, Localizer, LanguageLoader,
317//!     fluent::{
318//!         fluent_language_loader, FluentLanguageLoader     
319//! }};
320//! use rust_embed::RustEmbed;
321//!
322//! #[derive(RustEmbed)]
323//! #[folder = "i18n/mo"] // path to the compiled localization resources
324//! struct Localizations;
325//!
326//! fn language_loader() -> &'static FluentLanguageLoader {
327//!     static LANGUAGE_LOADER: OnceLock<FluentLanguageLoader> = OnceLock::new();
328//!
329//!     LANGUAGE_LOADER.get_or_init(|| {
330//!        let loader = fluent_language_loader!();
331//!
332//!         // Load the fallback language by default so that users of the
333//!         // library don't need to if they don't care about localization.
334//!         // This isn't required for the `gettext` localization system.
335//!         loader.load_fallback_language(&Localizations)
336//!             .expect("Error while loading fallback language");
337//!
338//!         loader
339//!     })
340//! }
341//!
342//! // Get the `Localizer` to be used for localizing this library.
343//! # #[allow(unused)]
344//! pub fn localizer() -> Arc<dyn Localizer> {
345//!     Arc::new(DefaultLocalizer::new(&*language_loader(), &Localizations))
346//! }
347//! # }
348//! ```
349//!
350//! People using this library can call `localize()` to obtain a
351//! [Localizer](Localizer), and add this as a listener to their chosen
352//! [LanguageRequester](LanguageRequester).
353//!
354//! ## Localizing Sub-crates
355//!
356//! If you want to localize a sub-crate in your project, and want to
357//! extract strings from this sub-crate and store/embed them in one
358//! location in the parent crate, you can use the following pattern
359//! for the library:
360//!
361//! ```
362//! #[cfg(feature = "gettext-system")]
363//! # {
364//! use std::sync::{Arc, OnceLock};
365//! use i18n_embed::{
366//!     DefaultLocalizer, Localizer, gettext::{
367//!     gettext_language_loader, GettextLanguageLoader     
368//! }};
369//! use i18n_embed::I18nAssets;
370//!
371//! fn language_loader() -> &'static GettextLanguageLoader {
372//!     static LANGUAGE_LOADER: OnceLock<GettextLanguageLoader> = OnceLock::new();
373//!
374//!     LANGUAGE_LOADER.get_or_init(|| gettext_language_loader!())
375//! }
376//!
377//! /// Get the `Localizer` to be used for localizing this library,
378//! /// using the provided embedded source of language files `embed`.
379//! # #[allow(unused)]
380//! pub fn localizer<'a>(embed: &'a (dyn I18nAssets + Send + Sync + 'static)) -> Arc<dyn Localizer + 'a> {
381//!     Arc::new(DefaultLocalizer::new(
382//!         &*language_loader(),
383//!         embed
384//!     ))
385//! }
386//! # }
387//! ```
388//!
389//! For the above example, you can enable the following options in the
390//! sub-crate's `i18n.toml` to ensure that the localization resources
391//! are extracted and merged with the parent crate's `pot` file:
392//!
393//! ```toml
394//! # ...
395//!
396//! [gettext]
397//!
398//! # ...
399//!
400//! # (Optional) If this crate is being localized as a subcrate, store the final
401//! # localization artifacts (the module pot and mo files) with the parent crate's
402//! # output. Currently crates which contain subcrates with duplicate names are not
403//! # supported.
404//! extract_to_parent = true
405//!
406//! # (Optional) If a subcrate has extract_to_parent set to true, then merge the
407//! # output pot file of that subcrate into this crate's pot file.
408//! collate_extracted_subcrates = true
409//! ```
410
411#![doc(test(
412    no_crate_inject,
413    attr(deny(warnings, rust_2018_idioms, single_use_lifetimes))
414))]
415#![forbid(unsafe_code)]
416#![warn(
417    missing_debug_implementations,
418    missing_docs,
419    rust_2018_idioms,
420    single_use_lifetimes,
421    unreachable_pub
422)]
423
424mod assets;
425mod requester;
426mod util;
427
428#[cfg(feature = "fluent-system")]
429pub mod fluent;
430
431#[cfg(feature = "gettext-system")]
432pub mod gettext;
433
434pub use assets::*;
435pub use requester::*;
436pub use util::*;
437
438#[cfg(doctest)]
439#[macro_use]
440extern crate doc_comment;
441
442#[cfg(all(doctest, feature = "desktop-requester", feature = "fluent-system"))]
443doctest!("../README.md");
444
445#[cfg(any(feature = "gettext-system", feature = "fluent-system"))]
446#[allow(unused_imports)]
447#[macro_use]
448extern crate i18n_embed_impl;
449#[cfg(feature = "gettext-system")]
450extern crate gettext as gettext_system;
451
452use std::{
453    borrow::Cow,
454    fmt::Debug,
455    path::{Component, Path},
456    string::FromUtf8Error,
457};
458
459use fluent_langneg::{negotiate_languages, NegotiationStrategy};
460use log::{debug, error};
461use thiserror::Error;
462
463pub use unic_langid;
464
465/// An error that occurs in this library.
466#[derive(Error, Debug)]
467#[allow(missing_docs)]
468pub enum I18nEmbedError {
469    #[error("Error parsing a language identifier string \"{0}\"")]
470    ErrorParsingLocale(String, #[source] unic_langid::LanguageIdentifierError),
471    #[error("Error reading language file \"{0}\" as utf8.")]
472    ErrorParsingFileUtf8(String, #[source] FromUtf8Error),
473    #[error("The slice of requested languages cannot be empty.")]
474    RequestedLanguagesEmpty,
475    #[error("The language file \"{0}\" for the language \"{1}\" is not available.")]
476    LanguageNotAvailable(String, unic_langid::LanguageIdentifier),
477    #[error("There are multiple errors: {}", error_vec_to_string(.0))]
478    Multiple(Vec<I18nEmbedError>),
479    #[cfg(feature = "gettext-system")]
480    #[error(transparent)]
481    Gettext(#[from] gettext_system::Error),
482    #[cfg(feature = "autoreload")]
483    #[error(transparent)]
484    Notify(#[from] assets::NotifyError),
485    #[cfg(feature = "filesystem-assets")]
486    #[error("The directory {0:?} does not exist")]
487    DirectoryDoesNotExist(std::path::PathBuf),
488    #[cfg(feature = "filesystem-assets")]
489    #[error("The path {0:?} is not a directory")]
490    PathIsNotDirectory(std::path::PathBuf),
491}
492
493fn error_vec_to_string(errors: &[I18nEmbedError]) -> String {
494    let strings: Vec<String> = errors.iter().map(|e| format!("{e}")).collect();
495    strings.join(", ")
496}
497
498/// This trait provides dynamic access to an
499/// [LanguageLoader](LanguageLoader) and an [I18nAssets](I18nAssets),
500/// which are used together to localize a library/crate on demand.
501pub trait Localizer {
502    /// The [LanguageLoader] used by this localizer.
503    fn language_loader(&self) -> &'_ dyn LanguageLoader;
504
505    /// The source of localization assets used by this localizer
506    fn i18n_assets(&self) -> &'_ dyn I18nAssets;
507
508    /// The available languages that can be selected by this localizer.
509    fn available_languages(&self) -> Result<Vec<unic_langid::LanguageIdentifier>, I18nEmbedError> {
510        self.language_loader()
511            .available_languages(self.i18n_assets())
512    }
513
514    /// Automatically the language currently requested by the system
515    /// by the the [LanguageRequester](LanguageRequester)), and load
516    /// it using the provided [LanguageLoader](LanguageLoader).
517    fn select(
518        &self,
519        requested_languages: &[unic_langid::LanguageIdentifier],
520    ) -> Result<Vec<unic_langid::LanguageIdentifier>, I18nEmbedError> {
521        select(
522            self.language_loader(),
523            self.i18n_assets(),
524            requested_languages,
525        )
526    }
527}
528
529/// A simple default implementation of the [Localizer](Localizer) trait.
530pub struct DefaultLocalizer<'a> {
531    /// The source of assets used by this localizer.
532    pub i18n_assets: &'a (dyn I18nAssets + Send + Sync + 'static),
533    /// The [LanguageLoader] used by this localizer.
534    pub language_loader: &'a (dyn LanguageLoader + Send + Sync + 'static),
535    watchers: Vec<Box<dyn Watcher + Send + Sync + 'static>>,
536}
537
538impl Debug for DefaultLocalizer<'_> {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        write!(
541            f,
542            "DefaultLocalizer(language_loader: {:p}, i18n_assets: {:p})",
543            self.language_loader, self.i18n_assets,
544        )
545    }
546}
547
548#[allow(single_use_lifetimes)]
549impl<'a> Localizer for DefaultLocalizer<'a> {
550    fn i18n_assets(&self) -> &'_ dyn I18nAssets {
551        self.i18n_assets
552    }
553    fn language_loader(&self) -> &'_ dyn LanguageLoader {
554        self.language_loader
555    }
556}
557
558impl<'a> DefaultLocalizer<'a> {
559    /// Create a new [DefaultLocalizer](DefaultLocalizer).
560    pub fn new(
561        language_loader: &'a (dyn LanguageLoader + Send + Sync + 'static),
562        i18n_assets: &'a (dyn I18nAssets + Send + Sync + 'static),
563    ) -> Self {
564        Self {
565            i18n_assets,
566            language_loader,
567            watchers: Vec::new(),
568        }
569    }
570}
571
572impl DefaultLocalizer<'static> {
573    /// Create a new [DefaultLocalizer](DefaultLocalizer).
574    pub fn with_autoreload(mut self) -> Result<Self, I18nEmbedError> {
575        let assets = self.i18n_assets;
576        let loader = self.language_loader;
577        let watcher = self
578            .i18n_assets
579            .subscribe_changed(std::sync::Arc::new(move || {
580                if let Err(error) = loader.reload(assets) {
581                    log::error!("Error autoreloading assets: {error:?}")
582                }
583            }))?;
584        self.watchers.push(watcher);
585        Ok(self)
586    }
587}
588
589/// Select the most suitable available language in order of preference
590/// by `requested_languages`, and load it using the provided
591/// [LanguageLoader] from the languages available in [I18nAssets].
592/// Returns the available languages that were negotiated as being the
593/// most suitable to be selected, and were loaded by
594/// [LanguageLoader::load_languages()]. If there were no available
595/// languages, then no languages will be loaded and the returned
596/// `Vec` will be empty.
597pub fn select(
598    language_loader: &dyn LanguageLoader,
599    i18n_assets: &dyn I18nAssets,
600    requested_languages: &[unic_langid::LanguageIdentifier],
601) -> Result<Vec<unic_langid::LanguageIdentifier>, I18nEmbedError> {
602    log::info!(
603        "Selecting translations for domain \"{0}\"",
604        language_loader.domain()
605    );
606
607    let available_languages: Vec<unic_langid::LanguageIdentifier> =
608        language_loader.available_languages(i18n_assets)?;
609    let default_language: &unic_langid::LanguageIdentifier = language_loader.fallback_language();
610
611    let supported_languages = negotiate_languages(
612        requested_languages,
613        &available_languages,
614        Some(default_language),
615        NegotiationStrategy::Filtering,
616    );
617
618    log::debug!("Requested Languages: {:?}", requested_languages);
619    log::debug!("Available Languages: {:?}", available_languages);
620    log::debug!("Supported Languages: {:?}", supported_languages);
621
622    let supported_languages: Vec<unic_langid::LanguageIdentifier> =
623        supported_languages.into_iter().cloned().collect();
624    if !supported_languages.is_empty() {
625        language_loader.load_languages(i18n_assets, &supported_languages)?;
626    }
627
628    Ok(supported_languages)
629}
630
631/// A language resource file, and its associated `language`.
632#[derive(Debug)]
633pub struct LanguageResource<'a> {
634    /// The language which this resource is associated with.
635    pub language: unic_langid::LanguageIdentifier,
636    /// The data for the file containing the localizations.
637    pub file: Cow<'a, [u8]>,
638}
639
640/// A trait used by [I18nAssets](I18nAssets) to load a language file for
641/// a specific rust module using a specific localization system. The
642/// trait is designed such that the loader could be swapped during
643/// runtime, or contain state if required.
644pub trait LanguageLoader {
645    /// The fallback language for the module this loader is responsible
646    /// for.
647    fn fallback_language(&self) -> &unic_langid::LanguageIdentifier;
648    /// The domain for the translation that this loader is associated with.
649    fn domain(&self) -> &str;
650    /// The language file name to use for this loader's domain.
651    fn language_file_name(&self) -> String;
652    /// The computed path to the language files, and data contained within the files at that path
653    /// itself if they exist. There can be multiple files at a given path, in order of preference
654    /// from high to low.
655    fn language_files<'a>(
656        &self,
657        language_id: &unic_langid::LanguageIdentifier,
658        i18n_assets: &'a dyn I18nAssets,
659    ) -> (String, Vec<Cow<'a, [u8]>>) {
660        let language_id_string = language_id.to_string();
661        let file_path = format!("{}/{}", language_id_string, self.language_file_name());
662
663        log::debug!("Attempting to load language file: \"{}\"", &file_path);
664
665        let files = i18n_assets.get_files(file_path.as_ref());
666        (file_path, files)
667    }
668
669    /// Calculate the languages which are available to be loaded.
670    fn available_languages(
671        &self,
672        i18n_assets: &dyn I18nAssets,
673    ) -> Result<Vec<unic_langid::LanguageIdentifier>, I18nEmbedError> {
674        let mut language_strings: Vec<String> = i18n_assets
675            .filenames_iter()
676            .filter_map(|filename| {
677                let path: &Path = Path::new(&filename);
678
679                let components: Vec<Component<'_>> = path.components().collect();
680
681                let locale: Option<String> = match components.first() {
682                    Some(Component::Normal(s)) => {
683                        Some(s.to_str().expect("path should be valid utf-8").to_string())
684                    }
685                    _ => None,
686                };
687
688                let language_file_name: Option<String> =
689                    components.get(1).and_then(|component| match component {
690                        Component::Normal(s) => {
691                            Some(s.to_str().expect("path should be valid utf-8").to_string())
692                        }
693                        _ => None,
694                    });
695
696                match language_file_name {
697                    Some(language_file_name) => {
698                        debug!(
699                            "Searching for available languages, found language file: \"{0}\"",
700                            &filename
701                        );
702                        if language_file_name == self.language_file_name() {
703                            locale
704                        } else {
705                            None
706                        }
707                    }
708                    None => None,
709                }
710            })
711            .collect();
712
713        let fallback_locale = self.fallback_language().to_string();
714
715        // For systems such as gettext which have a locale in the
716        // source code, this language will not be found in the
717        // localization assets, and should be the fallback_locale, so
718        // it needs to be added manually here.
719        if !language_strings
720            .iter()
721            .any(|language| language == &fallback_locale)
722        {
723            language_strings.insert(0, fallback_locale);
724        }
725
726        language_strings
727            .into_iter()
728            .map(|language: String| {
729                language
730                    .parse()
731                    .map_err(|err| I18nEmbedError::ErrorParsingLocale(language, err))
732            })
733            .collect()
734    }
735
736    /// Load all available languages with [`LanguageLoader::load_languages()`].
737    fn load_available_languages(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> {
738        let available_languages = self.available_languages(i18n_assets)?;
739        self.load_languages(i18n_assets, &available_languages)
740    }
741
742    /// Get the language which is currently loaded for this loader.
743    fn current_language(&self) -> unic_langid::LanguageIdentifier;
744
745    /// Reload the currently loaded languages.
746    fn reload(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError>;
747
748    /// Load the languages `language_ids` using the resources packaged
749    /// in the `i18n_embed` in order of fallback preference. This also
750    /// sets the [LanguageLoader::current_language()] to the first in
751    /// the `language_ids` slice. You can use [select()] to determine
752    /// which fallbacks are actually available for an arbitrary slice
753    /// of preferences.
754    fn load_languages(
755        &self,
756        i18n_assets: &dyn I18nAssets,
757        language_ids: &[unic_langid::LanguageIdentifier],
758    ) -> Result<(), I18nEmbedError>;
759
760    /// Load the [LanguageLoader::fallback_language()].
761    fn load_fallback_language(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> {
762        self.load_languages(i18n_assets, &[self.fallback_language().clone()])
763    }
764}
765
766/// Populate gettext database with strings for use with tests.
767#[cfg(all(test, feature = "gettext-system"))]
768mod gettext_test_string {
769    fn _test_strings() {
770        tr::tr!("only en");
771        tr::tr!("only ru");
772        tr::tr!("only es");
773        tr::tr!("only fr");
774    }
775}