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}