Repository: kellpossible/cargo-i18n (language: rust)

Issue:
Overriding/loading a language which is the same as src_locale the second time doesn't work
Unfortunately when loading a different language, and then trying to load the `src_locale` language again it doesn't work.

In `i18n_embed::select_single()` the logic is:

```rust
pub fn select_single(
    language_loader: &dyn LanguageLoader,
    i18n_embed: &dyn I18nEmbedDyn,
    language_id: &unic_langid::LanguageIdentifier,
) -> Result<(), I18nEmbedError> {
    if language_id != &language_loader.src_locale() {
        i18n_embed.load_language_file_dyn(language_id, language_loader)?;
    }

    info!("Selected languge \"{0}\"", language_id.to_string());
    Ok(())
}
```

unfortunately this skips loading the [empty catalog](https://docs.rs/gettext/0.4.0/gettext/struct.Catalog.html#method.empty) in `LanguageLoader`.

Probably `LanguageLoader` and `I18nEmbed` need to be refactored to properly support this situation. It may be a good time to move the responsibility for selecting/loading the file into the loader too.




Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 3196d9c..7f21987 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -61,7 +61,7 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
 
 [[package]]
 name = "cargo-i18n"
-version = "0.1.5"
+version = "0.1.6"
 dependencies = [
  "anyhow",
  "clap",
@@ -71,6 +71,7 @@ dependencies = [
  "i18n-build",
  "i18n-config",
  "i18n-embed",
+ "lazy_static",
  "log",
  "rust-embed",
  "tr",
@@ -220,12 +221,13 @@ dependencies = [
 
 [[package]]
 name = "i18n-build"
-version = "0.3.0"
+version = "0.3.1"
 dependencies = [
  "anyhow",
  "gettext",
  "i18n-config",
  "i18n-embed",
+ "lazy_static",
  "log",
  "rust-embed",
  "subprocess",
@@ -247,12 +249,13 @@ dependencies = [
 
 [[package]]
 name = "i18n-embed"
-version = "0.3.3"
+version = "0.4.0"
 dependencies = [
  "doc-comment",
  "fluent-langneg",
  "gettext",
  "i18n-embed-impl",
+ "lazy_static",
  "locale_config",
  "log",
  "rust-embed",
@@ -264,7 +267,7 @@ dependencies = [
 
 [[package]]
 name = "i18n-embed-impl"
-version = "0.3.1"
+version = "0.4.0"
 dependencies = [
  "i18n-config",
  "proc-macro2",
diff --git a/Cargo.toml b/Cargo.toml
index 7f8f4b9..866d161 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ license = "MIT"
 name = "cargo-i18n"
 readme = "README.md"
 repository = "https://github.com/kellpossible/cargo-i18n"
-version = "0.1.5"
+version = "0.1.6"
 
 [badges]
 maintenance = { status = "actively-developed" }
@@ -16,7 +16,7 @@ maintenance = { status = "actively-developed" }
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-i18n-embed = { version = "0.3", path = "./i18n-embed", features = ["desktop-requester"] }
+i18n-embed = { version = "0.4", path = "./i18n-embed", features = ["desktop-requester"] }
 i18n-build = { version = "0.3", path = "./i18n-build", features = ["localize"] }
 i18n-config = { version = "0.2", path = "./i18n-config" }
 anyhow = "1.0"
@@ -27,6 +27,7 @@ rust-embed = "5"
 unic-langid = "0.8"
 env_logger = "0.7.1"
 log = "0.4"
+lazy_static = "1.4.0"
 
 [dev-dependencies]
 doc-comment = "0.3"
diff --git a/README.md b/README.md
index 395daf3..3c7a486 100644
--- a/README.md
+++ b/README.md
@@ -89,19 +89,18 @@ i18n-embed = "0.3.1"
 A minimal example for how to embed the compiled translations into your application could be:
 
 ```rust
-use i18n_embed::{I18nEmbed, LanguageLoader, DesktopLanguageRequester};
+use i18n_embed::{I18nEmbed, language_loader, DesktopLanguageRequester};
 use rust_embed::RustEmbed;
 
 #[derive(RustEmbed, I18nEmbed)]
 #[folder = "i18n/mo"] // path to the compiled localization resources
 struct Translations;
 
-#[derive(LanguageLoader)]
-struct MyLanguageLoader;
+language_loader!(MyLanguageLoader);
 
 fn main() {
     let translations = Translations {};
-    let language_loader = MyLanguageLoader {};
+    let language_loader = MyLanguageLoader::new();
 
     // Use the language requester for the desktop platform (linux, windows, mac).
     // There is also a requester available for the web-sys WASM platform called
diff --git a/i18n-build/CHANGELOG.md b/i18n-build/CHANGELOG.md
index 55d4361..9ef6818 100644
--- a/i18n-build/CHANGELOG.md
+++ b/i18n-build/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog for `i18n-build`
 
+## v0.3.1
+
++ Update to `i18n-embed` version `0.4.0`.
+
 ## v0.3.0
 
 + Add support for `xtr` `add-location` option.
diff --git a/i18n-build/Cargo.toml b/i18n-build/Cargo.toml
index 7300963..f752ebc 100644
--- a/i18n-build/Cargo.toml
+++ b/i18n-build/Cargo.toml
@@ -8,7 +8,7 @@ license = "MIT"
 name = "i18n-build"
 readme = "README.md"
 repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-build"
-version = "0.3.0"
+version = "0.3.1"
 
 [package.metadata.docs.rs]
 all-features = true
@@ -24,14 +24,15 @@ anyhow = "1"
 thiserror = "1"
 tr = { version = "0.1", default-features = false, features = ["gettext"] }
 walkdir = "2"
-i18n-embed = { version = "0.3", path = "../i18n-embed", features = ["desktop-requester"], optional = true }
+i18n-embed = { version = "0.4", path = "../i18n-embed", features = ["desktop-requester"], optional = true }
 i18n-config = { version = "0.2", path = "../i18n-config" }
 gettext = { version = "0.4", optional = true }
 log = "0.4"
 rust-embed = "5"
+lazy_static = { version = "1.4.0", optional = true }
 
 [features]
 default = []
 
 # A feature to localize this library
-localize = ["i18n-embed", "gettext"]
\ No newline at end of file
+localize = ["i18n-embed", "gettext", "lazy_static"]
\ No newline at end of file
diff --git a/i18n-build/src/gettext_impl/mod.rs b/i18n-build/src/gettext_impl/mod.rs
index ff15c45..0a5666b 100644
--- a/i18n-build/src/gettext_impl/mod.rs
+++ b/i18n-build/src/gettext_impl/mod.rs
@@ -421,7 +421,8 @@ pub fn run<'a>(crt: &'a Crate) -> Result<()> {
     // in an infinite loop.
     let subcrates: Vec<Crate> = match &crt.i18n_config {
         Some(config) => {
-            let subcrates: Result<Vec<Crate>, I18nConfigError> = config.subcrates
+            let subcrates: Result<Vec<Crate>, I18nConfigError> = config
+                .subcrates
                 .iter()
                 .map(|subcrate_path| {
                     Crate::from(
@@ -433,7 +434,8 @@ pub fn run<'a>(crt: &'a Crate) -> Result<()> {
                 .collect();
 
             subcrates.with_context(|| {
-                let subcrate_path_strings: Vec<String> = config.subcrates
+                let subcrate_path_strings: Vec<String> = config
+                    .subcrates
                     .iter()
                     .map(|path| path.to_string_lossy().to_string())
                     .collect();
@@ -443,7 +445,7 @@ pub fn run<'a>(crt: &'a Crate) -> Result<()> {
                     subcrate_path_strings.join(", ")
                 )
             })?
-        },
+        }
         None => vec![],
     };
 
diff --git a/i18n-build/src/lib.rs b/i18n-build/src/lib.rs
index 24598da..bd0a699 100644
--- a/i18n-build/src/lib.rs
+++ b/i18n-build/src/lib.rs
@@ -25,7 +25,10 @@ use anyhow::Result;
 use i18n_config::Crate;
 
 #[cfg(feature = "localize")]
-use i18n_embed::{DefaultLocalizer, I18nEmbed, LanguageLoader, Localizer};
+use lazy_static::lazy_static;
+
+#[cfg(feature = "localize")]
+use i18n_embed::{language_loader, DefaultLocalizer, I18nEmbed, Localizer};
 
 #[cfg(feature = "localize")]
 use rust_embed::RustEmbed;
@@ -36,13 +39,15 @@ use rust_embed::RustEmbed;
 struct Translations;
 
 #[cfg(feature = "localize")]
-#[derive(LanguageLoader)]
-struct I18nBuildLanguageLoader;
+language_loader!(I18nBuildLanguageLoader);
 
 #[cfg(feature = "localize")]
-const LANGUAGE_LOADER: I18nBuildLanguageLoader = I18nBuildLanguageLoader {};
+lazy_static! {
+    static ref LANGUAGE_LOADER: I18nBuildLanguageLoader = I18nBuildLanguageLoader::new();
+}
+
 #[cfg(feature = "localize")]
-const TRANSLATIONS: Translations = Translations {};
+static TRANSLATIONS: Translations = Translations {};
 
 /// Run the i18n build process for the provided crate, which must
 /// contain an i18n config.
@@ -94,5 +99,6 @@ pub fn run(crt: Crate) -> Result<()> {
 /// ⚠️ *This API requires the following crate features to be activated: `localize`.*
 #[cfg(feature = "localize")]
 pub fn localizer() -> Box<dyn Localizer<'static>> {
-    Box::from(DefaultLocalizer::new(&LANGUAGE_LOADER, &TRANSLATIONS))
+    let localizer = DefaultLocalizer::new(&*LANGUAGE_LOADER, &TRANSLATIONS);
+    Box::from(localizer)
 }
diff --git a/i18n-config/src/lib.rs b/i18n-config/src/lib.rs
index c817bf6..b6f3761 100644
--- a/i18n-config/src/lib.rs
+++ b/i18n-config/src/lib.rs
@@ -378,7 +378,9 @@ impl GettextAddLocation {
 }
 
 impl Default for GettextAddLocation {
-    fn default() -> Self { GettextAddLocation::Full }
+    fn default() -> Self {
+        GettextAddLocation::Full
+    }
 }
 
 /// The data structure representing what is stored (and possible to
@@ -411,9 +413,9 @@ pub struct GettextConfig {
     pub msgid_bugs_address: Option<String>,
     /// Whether or not to perform string extraction using the `xtr` command.
     pub xtr: Option<bool>,
-    /// Generate ‘#: filename:line’ lines (default) in the pot files when 
-    /// running the `xtr` command. If the type is ‘full’ (the default), 
-    /// it generates the lines with both file name and line number. 
+    /// Generate ‘#: filename:line’ lines (default) in the pot files when
+    /// running the `xtr` command. If the type is ‘full’ (the default),
+    /// it generates the lines with both file name and line number.
     /// If it is ‘file’, the line number part is omitted. If it is ‘never’,
     ///  nothing is generated. [possible values: full, file, never].
     #[serde(default)]
diff --git a/i18n-embed/CHANGELOG.md b/i18n-embed/CHANGELOG.md
index 2bc52a3..df43dc3 100644
--- a/i18n-embed/CHANGELOG.md
+++ b/i18n-embed/CHANGELOG.md
@@ -1,5 +1,18 @@
 # Changelog for `i18n-embed`
 
+## v0.4.0
+
+Mostly a refactor of `LanguageLoader` and `I18nEmbed` to solve [issue #15](https://github.com/kellpossible/cargo-i18n/issues/15).
+
++ Replaced the derive macro for `LanguageLoader` with a new `language_loader!(StructName)` which creates a new struct with the specified `StructName` and implements `LanguageLoader` for it. This was done because `LanguageLoader` now needs to store state for the currently selected language, and deriving this automatically would be complicated.
++ Refactored `I18nEmbed` to move the `load_language_file` responsibility into `LanguageLoader` and add a new `load_language` method to `LanguageLoader`.
++ Refactored `I18nEmbedDyn` to also expose the `RustEmbed#get()` method, required for the new `LanguageLoader` changes.
++ Using `LanguageLoader` as a static now requires [lazy_static](https://crates.io/crates/lazy_static) or something similar because the `StructName#new()` constructor which is created for it in `language_loader!(StructName)` is not `const`.
+
+## v0.3.4
+
++ Made `WebLanguageRequester::requested_languages()` public.
+
 ## v0.3.3
 
 + Updated link to this changelog in the crate README.
diff --git a/i18n-embed/Cargo.toml b/i18n-embed/Cargo.toml
index 2682b47..1419332 100644
--- a/i18n-embed/Cargo.toml
+++ b/i18n-embed/Cargo.toml
@@ -9,7 +9,7 @@ license = "MIT"
 name = "i18n-embed"
 readme = "README.md"
 repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-embed"
-version = "0.3.3"
+version = "0.4.0"
 
 [package.metadata.docs.rs]
 all-features = true
@@ -20,7 +20,7 @@ all-features = true
 maintenance = { status = "actively-developed" }
 
 [dependencies]
-i18n-embed-impl = { version = "0.3", path = "./i18n-embed-impl" }
+i18n-embed-impl = { version = "0.4", path = "./i18n-embed-impl" }
 unic-langid = "0.8"
 locale_config = { version = "0.3", optional = true }
 fluent-langneg = "0.12"
@@ -33,6 +33,7 @@ thiserror = "1"
 
 [dev-dependencies]
 doc-comment = "0.3"
+lazy_static = "1.4.0"
 
 [features]
 default = ["gettext-system"]
diff --git a/i18n-embed/README.md b/i18n-embed/README.md
index d49f70c..38aa544 100644
--- a/i18n-embed/README.md
+++ b/i18n-embed/README.md
@@ -20,19 +20,18 @@ The `i18n-embed` crate has the following optional Cargo features:
 The following is an example for how to derive the required traits on structs, and localize your binary using this library:
 
 ```rust
-use i18n_embed::{I18nEmbed, LanguageLoader, DesktopLanguageRequester};
+use i18n_embed::{I18nEmbed, language_loader, DesktopLanguageRequester};
 use rust_embed::RustEmbed;
 
 #[derive(RustEmbed, I18nEmbed)]
 #[folder = "i18n/mo"] // path to the compiled localization resources
 struct Translations;
 
-#[derive(LanguageLoader)]
-struct MyLanguageLoader;
+language_loader!(MyLanguageLoader);
 
 fn main() {
     let translations = Translations {};
-    let language_loader = MyLanguageLoader {};
+    let language_loader = MyLanguageLoader::new();
 
     // Use the language requester for the desktop platform (linux, windows, mac).
     // There is also a requester available for the web-sys WASM platform called
diff --git a/i18n-embed/i18n-embed-impl/Cargo.toml b/i18n-embed/i18n-embed-impl/Cargo.toml
index c65767a..8a9c69b 100644
--- a/i18n-embed/i18n-embed-impl/Cargo.toml
+++ b/i18n-embed/i18n-embed-impl/Cargo.toml
@@ -7,7 +7,7 @@ license = "MIT"
 name = "i18n-embed-impl"
 readme = "../README.md"
 repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-embed"
-version = "0.3.1"
+version = "0.4.0"
 
 [badges]
 maintenance = { status = "actively-developed" }
diff --git a/i18n-embed/i18n-embed-impl/src/lib.rs b/i18n-embed/i18n-embed-impl/src/lib.rs
index 903a7ac..f9d111c 100644
--- a/i18n-embed/i18n-embed-impl/src/lib.rs
+++ b/i18n-embed/i18n-embed-impl/src/lib.rs
@@ -2,9 +2,9 @@ extern crate proc_macro;
 
 use crate::proc_macro::TokenStream;
 
-use std::path::PathBuf;
 use i18n_config::I18nConfig;
 use quote::quote;
+use std::path::PathBuf;
 use syn;
 
 /// A procedural macro to implement the `I18nEmbed` trait on a struct.
@@ -22,61 +22,63 @@ pub fn i18n_embed_derive(input: TokenStream) -> TokenStream {
     gen.into()
 }
 
-/// A procedural macro to implement the `LanguageLoader` trait on a struct.
+/// A procedural macro to create a struct and implement the `LanguageLoader` trait on it.
+/// 
+/// ## Example
 /// 
-/// The following struct level attributes available:
-///
-/// + (Optional) `#[config_file = "i18n.toml"]` - path to i18n config
-///   file relative to the crate root.
-#[proc_macro_derive(LanguageLoader, attributes(config_file))]
+/// ```ignore
+/// use i18n_embed::language_loader;
+/// 
+/// language_loader!(MyLanguageLoader);
+/// let my_language_loader = MyLanguageLoader::new();
+/// ```
+#[proc_macro]
 #[cfg(feature = "gettext-system")]
-pub fn language_loader_derive(input: TokenStream) -> TokenStream {
-    let ast: syn::DeriveInput = syn::parse(input).unwrap();
+pub fn language_loader(input: TokenStream) -> TokenStream {
+    let struct_name = syn::parse_macro_input!(input as syn::Ident);
 
-    let struct_name = &ast.ident;
-
-    let config_file_path = ast
-        .attrs
-        .iter()
-        .find(|value| value.path.is_ident("config_file"))
-        .map(|attr| {
-            let meta: syn::Meta = attr.parse_meta().unwrap();
-            let literal_value = match meta {
-                syn::Meta::NameValue(ref data) => &data.lit,
-                _ => panic!("#[derive(I18nEmbed)]'s attribute \"config_file\" should be formatted like: #[config_file = \"i18n.toml\"]")
-            };
-
-            let config_file_path = match literal_value {
-                syn::Lit::Str(ref val) => PathBuf::from(val.clone().value()),
-                _ => {
-                panic!("#[derive(I18nEmbed)]'s attribute \"config_file\" value must be a string literal");
-                }
-            };
-
-            config_file_path
-        }).unwrap_or(PathBuf::from("i18n.toml"));
+    let config_file_path = PathBuf::from("i18n.toml");
 
     if !config_file_path.exists() {
         panic!(format!(
-            "#[derive(RustEmbed)] folder '{}' does not exist. cwd: '{}'",
+            "The i18n configuration file '{}' does not exist in the current working directory '{}'",
             config_file_path.to_string_lossy(),
             std::env::current_dir().unwrap().to_str().unwrap()
         ));
     }
 
     let config = I18nConfig::from_file(&config_file_path).expect(&format!(
-        "#[derive(RustEmbed)] had a problem reading config file {0}",
+        "language_loader!() had a problem reading config file '{0}'",
         config_file_path.to_string_lossy()
     ));
     let src_locale = &config.src_locale;
 
     let gen = quote! {
-        impl LanguageLoader for #struct_name {
-            fn load_language_file(&self, file: std::borrow::Cow<[u8]>) {
+        struct #struct_name {
+            current_language: std::sync::RwLock<i18n_embed::unic_langid::LanguageIdentifier>,
+        }
+
+        impl #struct_name {
+            pub fn new() -> #struct_name {
+                #struct_name {
+                    current_language: std::sync::RwLock::new(#src_locale.parse().unwrap()),
+                }
+            }
+        }
+
+        impl i18n_embed::LanguageLoader for #struct_name {
+            fn load_language_file(&self, language_id: i18n_embed::unic_langid::LanguageIdentifier, file: std::borrow::Cow<[u8]>) {
                 let catalog = i18n_embed::gettext::Catalog::parse(&*file).expect("could not parse the catalog");
                 i18n_embed::tr::set_translator!(catalog);
+                *(self.current_language.write().unwrap()) = language_id;
             }
-        
+
+            fn load_src_locale(&self) {
+                let catalog = i18n_embed::gettext::Catalog::empty();
+                i18n_embed::tr::set_translator!(catalog);
+                *(self.current_language.write().unwrap()) = self.src_locale();
+            }
+
             fn domain(&self) -> &'static str {
                 i18n_embed::domain_from_module(module_path!())
             }
@@ -88,8 +90,13 @@ pub fn language_loader_derive(input: TokenStream) -> TokenStream {
             fn language_file_name(&self) -> String {
                 format!("{}.{}", self.domain(), "mo")
             }
+
+            /// Get the language which is currently loaded for this loader.
+            fn current_language(&self) -> i18n_embed::unic_langid::LanguageIdentifier {
+                self.current_language.read().unwrap().clone()
+            }
         }
     };
 
     gen.into()
-}
\ No newline at end of file
+}
diff --git a/i18n-embed/src/lib.rs b/i18n-embed/src/lib.rs
index 8513aa2..9a4bd8d 100644
--- a/i18n-embed/src/lib.rs
+++ b/i18n-embed/src/lib.rs
@@ -47,19 +47,18 @@
 //! first runs:
 //!
 //! ```
-//! use i18n_embed::{I18nEmbed, LanguageLoader, DesktopLanguageRequester};
+//! use i18n_embed::{I18nEmbed, language_loader, DesktopLanguageRequester};
 //! use rust_embed::RustEmbed;
 //!
 //! #[derive(RustEmbed, I18nEmbed)]
 //! #[folder = "i18n/mo"] // path to the compiled localization resources
 //! struct Translations;
 //!
-//! #[derive(LanguageLoader)]
-//! struct MyLanguageLoader;
+//! language_loader!(MyLanguageLoader);
 //!
 //! fn main() {
 //!     let translations = Translations {};
-//!     let language_loader = MyLanguageLoader {};
+//!     let language_loader = MyLanguageLoader::new();
 //!
 //!     // Use the language requester for the desktop platform (linux, windows, mac).
 //!     // There is also a requester available for the web-sys WASM platform called
@@ -82,23 +81,25 @@
 //! ```
 //! use std::rc::Rc;
 //! use i18n_embed::{
-//!     I18nEmbed, LanguageLoader, DesktopLanguageRequester,
+//!     I18nEmbed, language_loader, DesktopLanguageRequester,
 //!     LanguageRequester, DefaultLocalizer, Localizer};
 //! use rust_embed::RustEmbed;
+//! use lazy_static::lazy_static;
 //!
 //! #[derive(RustEmbed, I18nEmbed)]
 //! #[folder = "i18n/mo"] // path to the compiled localization resources
 //! struct Translations;
+//! const TRANSLATIONS: Translations = Translations {};
 //!
-//! #[derive(LanguageLoader)]
-//! struct MyLanguageLoader;
+//! language_loader!(MyLanguageLoader);
 //!
-//! const LANGUAGE_LOADER: MyLanguageLoader = MyLanguageLoader {};
-//! const TRANSLATIONS: Translations = Translations {};
+//! lazy_static! {
+//!     static ref LANGUAGE_LOADER: MyLanguageLoader = MyLanguageLoader::new();
+//! }
 //!
 //! fn main() {
 //!     let localizer = DefaultLocalizer::new(
-//!         &LANGUAGE_LOADER,
+//!         &*LANGUAGE_LOADER,
 //!         &TRANSLATIONS,
 //!     );
 //!
@@ -119,6 +120,10 @@
 //! The above example makes use of the
 //! [DefaultLocalizer](DefaultLocalizer), but you can also implement
 //! the [Localizer](Localizer) trait yourself for a custom solution.
+//! It also makes use of
+//! [lazy_static](https://crates.io/crates/lazy_static) to allow the
+//! [LanguageLoader](LanguageLoader) implementation to be stored
+//! statically, because its constructor is not `const`.
 //!
 //! ## Localizing Libraries
 //!
@@ -126,21 +131,24 @@
 //! you can follow this code pattern in the library itself:
 //!
 //! ```
+//! # #![feature(proc_macro_hygiene)]
 //! use std::rc::Rc;
 //! use i18n_embed::{
-//!    I18nEmbed, LanguageLoader, DesktopLanguageRequester,
+//!    I18nEmbed, language_loader, DesktopLanguageRequester,
 //!    LanguageRequester, DefaultLocalizer, Localizer};
 //! use rust_embed::RustEmbed;
-//! 
+//! use lazy_static::lazy_static;
+//!
 //! #[derive(RustEmbed, I18nEmbed)]
 //! #[folder = "i18n/mo"] // path to the compiled localization resources
 //! struct Translations;
+//! const TRANSLATIONS: Translations = Translations {};
 //!
-//! #[derive(LanguageLoader)]
-//! struct MyLanguageLoader;
+//! language_loader!(MyLanguageLoader);
 //!
-//! const LANGUAGE_LOADER: MyLanguageLoader = MyLanguageLoader {};
-//! const TRANSLATIONS: Translations = Translations {};
+//! lazy_static! {
+//!     static ref LANGUAGE_LOADER: MyLanguageLoader = MyLanguageLoader::new();
+//! }
 //!
 //! /// Localize this library, and select the language using the provided
 //! /// [LanguageRequester](LanguageRequester).
@@ -165,26 +173,30 @@
 //! for the library:
 //!
 //! ```
+//! # #![feature(proc_macro_hygiene)]
 //! use std::rc::Rc;
 //! use i18n_embed::{
-//!    I18nEmbed, LanguageLoader, DesktopLanguageRequester,
+//!    I18nEmbed, language_loader, DesktopLanguageRequester,
 //!    LanguageRequester, DefaultLocalizer, Localizer};
 //! use rust_embed::RustEmbed;
 //! use i18n_embed::I18nEmbedDyn;
+//! use lazy_static::lazy_static;
 //!
 //! #[derive(RustEmbed, I18nEmbed)]
 //! #[folder = "i18n/mo"] // path to the compiled localization resources
 //! struct Translations;
 //!
-//! #[derive(LanguageLoader)]
-//! struct MyLanguageLoader;
-//!
-//! const LANGUAGE_LOADER: MyLanguageLoader = MyLanguageLoader {};
 //! const TRANSLATIONS: Translations = Translations {};
 //!
+//! language_loader!(MyLanguageLoader);
+//!
+//! lazy_static! {
+//!     static ref LANGUAGE_LOADER: MyLanguageLoader = MyLanguageLoader::new();
+//! }
+//!
 //! pub fn localizer(embed: &'static dyn I18nEmbedDyn) -> Box<dyn Localizer<'static>> {
 //!     Box::from(DefaultLocalizer::new(
-//!         &LANGUAGE_LOADER,
+//!         &*LANGUAGE_LOADER,
 //!         embed
 //!     ))
 //! }
@@ -270,7 +282,7 @@ pub trait Localizer<'a> {
     fn select(
         &self,
         requested_languages: &Vec<unic_langid::LanguageIdentifier>,
-    ) -> Result<(), I18nEmbedError> {
+    ) -> Result<Option<unic_langid::LanguageIdentifier>, I18nEmbedError> {
         select(
             self.language_loader(),
             self.i18n_embed(),
@@ -315,6 +327,7 @@ struct LanguageRequesterImpl<'a> {
 }
 
 impl<'a> LanguageRequesterImpl<'a> {
+    /// Create a new [LanguageRequesterImpl](LanguageRequesterImpl).
     pub fn new() -> LanguageRequesterImpl<'a> {
         LanguageRequesterImpl {
             listeners: Vec::new(),
@@ -322,14 +335,13 @@ impl<'a> LanguageRequesterImpl<'a> {
         }
     }
 
-    pub fn set_languge_override(
+    /// Set an override for the requested language which is used when the
+    /// [LanguageRequesterImpl#poll()](LanguageRequester#poll()) method
+    /// is called. If `None`, then no override is used.
+    fn set_languge_override(
         &mut self,
         language_override: Option<unic_langid::LanguageIdentifier>,
     ) -> Result<(), I18nEmbedError> {
-        match &language_override {
-            Some(language_override) => self.poll_without_override(vec![language_override.clone()]),
-            None => Ok(()),
-        }?;
         self.language_override = language_override;
         Ok(())
     }
@@ -346,14 +358,16 @@ impl<'a> LanguageRequesterImpl<'a> {
         requested_languages: Vec<unic_langid::LanguageIdentifier>,
     ) -> Result<(), I18nEmbedError> {
         let mut errors: Vec<I18nEmbedError> = Vec::new();
+
         self.listeners.retain(|listener| match listener.upgrade() {
             Some(l) => {
                 match l.select(&requested_languages) {
-                    Ok(()) => {}
+                    Ok(_) => {}
                     Err(err) => {
                         errors.push(err);
                     }
                 }
+
                 true
             }
             None => false,
@@ -378,7 +392,10 @@ impl<'a> LanguageRequesterImpl<'a> {
         requested_languages: Vec<unic_langid::LanguageIdentifier>,
     ) -> Result<(), I18nEmbedError> {
         let languages = match &self.language_override {
-            Some(language) => vec![language.clone()],
+            Some(language) => {
+                debug!("Using language override: {}", language);
+                vec![language.clone()]
+            }
             None => requested_languages,
         };
 
@@ -408,6 +425,9 @@ pub trait LanguageRequester<'a> {
         &mut self,
         language_override: Option<unic_langid::LanguageIdentifier>,
     ) -> Result<(), I18nEmbedError>;
+    /// Get the language last selected for the [LanguageLoader](LanguageLoader)
+    /// by this [LanguageRequester](LanguageRequester).
+    // fn currently_selected_language(&self, language_loader: &'a dyn LanguageLoader) -> &Option<unic_langid::LanguageIdentifier>;
     // fn override_request(&self, language_id: unic_langid::LanguageIdentifier);
     // fn reset_override_request(&self);
     // /// Attach a listener to the system this requester is mediating
@@ -425,12 +445,13 @@ pub trait LanguageRequester<'a> {
 /// system by the the [LanguageRequester](LanguageRequester), and load
 /// it using the provided [LanguageLoader](LanguageLoader) from the
 /// languages embedded in [I18nEmbed](I18nEmbed) via
-/// [I18nEmbedDyn](I18nEmbedDyn).
+/// [I18nEmbedDyn](I18nEmbedDyn). Returns the language that was
+/// negotiated to be selected.
 pub fn select(
     language_loader: &dyn LanguageLoader,
     i18n_embed: &dyn I18nEmbedDyn,
     requested_languages: &Vec<unic_langid::LanguageIdentifier>,
-) -> Result<(), I18nEmbedError> {
+) -> Result<Option<unic_langid::LanguageIdentifier>, I18nEmbedError> {
     info!(
         "Selecting translations for domain \"{0}\"",
         language_loader.domain()
@@ -454,13 +475,10 @@ pub fn select(
     match supported_languages.get(0) {
         Some(language_id) => {
             select_single(language_loader, i18n_embed, language_id)?;
+            Ok(Some((*language_id).clone()))
         }
-        None => {
-            // do nothing
-        }
+        None => Ok(None),
     }
-
-    Ok(())
 }
 
 /// Load a language with `language_id` using the provided
@@ -471,10 +489,7 @@ pub fn select_single(
     i18n_embed: &dyn I18nEmbedDyn,
     language_id: &unic_langid::LanguageIdentifier,
 ) -> Result<(), I18nEmbedError> {
-    if language_id != &language_loader.src_locale() {
-        i18n_embed.load_language_file_dyn(language_id, language_loader)?;
-    }
-
+    language_loader.load_language(language_id, i18n_embed)?;
     info!("Selected languge \"{0}\"", language_id.to_string());
     Ok(())
 }
@@ -487,14 +502,44 @@ pub trait LanguageLoader {
     /// The locale used in the source code for the module this loader
     /// is responsible for.
     fn src_locale(&self) -> unic_langid::LanguageIdentifier;
-    /// The domain for the translation.
+    /// The domain for the translation that this loader is associated with.
     fn domain(&self) -> &'static str;
-    /// Load the language file corresponding to the module that this
-    /// loader requested in `module_path()`.
-    fn load_language_file(&self, file: Cow<[u8]>);
-    /// Calculate the language file name to use for the given
-    /// [LanguageLoader](LanguageLoader).
+    /// Load the language corresponding the specified `language_id`.
+    fn load_language(
+        &self,
+        language_id: &unic_langid::LanguageIdentifier,
+        i18n_embed: &dyn I18nEmbedDyn,
+    ) -> Result<(), I18nEmbedError> {
+        if language_id == &self.src_locale() {
+            self.load_src_locale();
+            return Ok(());
+        }
+        let language_id_string = language_id.to_string();
+
+        let file_path = format!("{}/{}", language_id_string, self.language_file_name());
+
+        let f = i18n_embed
+            .get_dyn(file_path.as_ref())
+            .ok_or(I18nEmbedError::LanguageNotAvailable(language_id_string))?;
+        info!(
+            "Loading language \"{0}\" from file \"{1}\"",
+            language_id.to_string(),
+            file_path
+        );
+        self.load_language_file(language_id.clone(), f);
+
+        Ok(())
+    }
+    /// Load the language `file` which is associated with the specified `language_id` and
+    /// set the current language, which can be retrieved via
+    /// [current_language()](LanguageLoader#current_language()).
+    fn load_language_file(&self, language_id: unic_langid::LanguageIdentifier, file: Cow<[u8]>);
+    /// Load the language associated with [src_locale()](LanguageLoader#src_locale()).
+    fn load_src_locale(&self);
+    /// The language file name to use for this loader.
     fn language_file_name(&self) -> String;
+    /// Get the language which is currently loaded for this loader.
+    fn current_language(&self) -> unic_langid::LanguageIdentifier;
 }
 
 /// A dynamic reference to a static [I18nEmbed](I18nEmbed) implementation.
@@ -506,14 +551,9 @@ pub trait I18nEmbedDyn {
         &self,
         language_loader: &'a dyn LanguageLoader,
     ) -> Result<Vec<unic_langid::LanguageIdentifier>, I18nEmbedError>;
-    /// A dynamic way to access the static
-    /// [I18nEmbed#load_language_file()](I18nEmbed#load_language_file())
-    /// method for a given [I18nEmbed](I18nEmbed) implementation.
-    fn load_language_file_dyn<'a>(
-        &self,
-        language_id: &unic_langid::LanguageIdentifier,
-        language_loader: &'a dyn LanguageLoader,
-    ) -> Result<(), I18nEmbedError>;
+    /// A dynamic way to access the static [RustEmbed#get()](RustEmbed#get())
+    /// for a given [I18nEmbed](I18nEmbed) implementation.
+    fn get_dyn(&self, file_path: &str) -> Option<std::borrow::Cow<'static, [u8]>>;
 }
 
 impl<T: I18nEmbed + ?Sized> I18nEmbedDyn for T {
@@ -523,12 +563,8 @@ impl<T: I18nEmbed + ?Sized> I18nEmbedDyn for T {
     ) -> Result<Vec<unic_langid::LanguageIdentifier>, I18nEmbedError> {
         T::available_languages(language_loader)
     }
-    fn load_language_file_dyn<'a>(
-        &self,
-        language_id: &unic_langid::LanguageIdentifier,
-        language_loader: &'a dyn LanguageLoader,
-    ) -> Result<(), I18nEmbedError> {
-        T::load_language_file(language_id, language_loader)
+    fn get_dyn(&self, file_path: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
+        T::get(file_path)
     }
 }
 
@@ -572,7 +608,10 @@ pub trait I18nEmbed: RustEmbed {
 
                 match language_file_name {
                     Some(language_file_name) => {
-                        debug!("Found language file: \"{0}\"", &filename);
+                        debug!(
+                            "Searching for available languages, found language file: \"{0}\"",
+                            &filename
+                        );
                         if language_file_name == language_loader.language_file_name() {
                             locale
                         } else {
@@ -595,30 +634,6 @@ pub trait I18nEmbed: RustEmbed {
             })
             .collect()
     }
-
-    /// Load the language file associated with the specified `language_id`
-    /// using the provided `language_loader`.
-    fn load_language_file<'a>(
-        language_id: &unic_langid::LanguageIdentifier,
-        language_loader: &'a dyn LanguageLoader,
-    ) -> Result<(), I18nEmbedError> {
-        let language_id_string = language_id.to_string();
-
-        let file_path = format!(
-            "{}/{}",
-            language_id_string,
-            language_loader.language_file_name()
-        );
-        let f = Self::get(file_path.as_ref())
-            .ok_or(I18nEmbedError::LanguageNotAvailable(language_id_string))?;
-        info!(
-            "Loading language \"{0}\" from file \"{1}\"",
-            language_id.to_string(),
-            file_path
-        );
-        language_loader.load_language_file(f);
-        Ok(())
-    }
 }
 
 /// A [LanguageRequester](LanguageRequester) for the desktop platform,
@@ -700,7 +715,7 @@ impl<'a> WebLanguageRequester<'a> {
         }
     }
 
-    fn requested_languages() -> Vec<unic_langid::LanguageIdentifier> {
+    pub fn requested_languages() -> Vec<unic_langid::LanguageIdentifier> {
         use fluent_langneg::convert_vec_str_to_langids_lossy;
         let window = web_sys::window().expect("no global `window` exists");
         let navigator = window.navigator();
diff --git a/i18n/po/ru/cargo_i18n.po b/i18n/po/ru/cargo_i18n.po
index 4c7e8b8..d23e3cd 100644
--- a/i18n/po/ru/cargo_i18n.po
+++ b/i18n/po/ru/cargo_i18n.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: cargo-i18n\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-04-10 12:07+0000\n"
+"POT-Creation-Date: 2020-04-19 14:57+0000\n"
 "Language: ru\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
@@ -10,14 +10,14 @@ msgstr ""
 "X-Generator: POEditor.com\n"
 
 #. The help message displayed when running `cargo i18n -h`.
-#: src/main.rs:28
+#: src/main.rs:31
 msgid "A Cargo sub-command to extract and build localization resources."
 msgstr "Подкоманда Cargo для извлечения и компилирования ресурсов локализации."
 
 #. The help message displayed when running `cargo i18n --help`.
 #. {0} is the short about message.
 #. {1} is a list of available languages. e.g. "en", "ru", etc
-#: src/main.rs:45
+#: src/main.rs:48
 msgid ""
 "{0}\n"
 "\n"
@@ -65,7 +65,7 @@ msgstr ""
 "\"RUST_LOG=debug cargo i18n\""
 
 #. The message displayed when running the binary outside of cargo using `cargo-18n`.
-#: src/main.rs:96
+#: src/main.rs:99
 msgid ""
 "This binary is designed to be executed as a cargo subcommand using \"cargo "
 "i18n\"."
@@ -74,7 +74,7 @@ msgstr ""
 "использованием \"cargo i18n\"."
 
 #. The help message for the `--path` command line argument.
-#: src/main.rs:108
+#: src/main.rs:111
 msgid ""
 "Path to the crate you want to localize (if not the current directory). The "
 "crate needs to contain \"i18n.toml\" in its root."
@@ -83,12 +83,12 @@ msgstr ""
 "каталог). Крэйтор должен содержать \"i18n.toml\" в своем корне."
 
 #. The help message for the `-c`, `--config-file-name` command line argument.
-#: src/main.rs:117
+#: src/main.rs:120
 msgid "The name of the i18n config file for this crate"
 msgstr "Имя файла конфигурации i18n для этого крэйтора."
 
 #. The help message for the `-l`, `--language` command line argument.
-#: src/main.rs:128
+#: src/main.rs:131
 msgid ""
 "Set the language to use for this application. Overrides the language "
 "selected automatically by your operating system."
diff --git a/src/main.rs b/src/main.rs
index 3011dc4..31d2c2e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,9 +3,10 @@ use clap::{crate_authors, crate_version, App, Arg, SubCommand};
 use i18n_build::run;
 use i18n_config::Crate;
 use i18n_embed::{
-    DefaultLocalizer, DesktopLanguageRequester, I18nEmbed, I18nEmbedDyn, LanguageLoader,
-    LanguageRequester, Localizer,
+    language_loader, DefaultLocalizer, DesktopLanguageRequester, I18nEmbed, I18nEmbedDyn,
+    LanguageLoader, LanguageRequester, Localizer,
 };
+use lazy_static::lazy_static;
 use rust_embed::RustEmbed;
 use std::path::Path;
 use std::rc::Rc;
@@ -16,11 +17,13 @@ use unic_langid::LanguageIdentifier;
 #[folder = "i18n/mo"]
 struct Translations;
 
-#[derive(LanguageLoader)]
-struct CargoI18nLanguageLoader;
+language_loader!(CargoI18nLanguageLoader);
 
-const LANGUAGE_LOADER: CargoI18nLanguageLoader = CargoI18nLanguageLoader {};
-const TRANSLATIONS: Translations = Translations {};
+lazy_static! {
+    static ref LANGUAGE_LOADER: CargoI18nLanguageLoader = CargoI18nLanguageLoader::new();
+}
+
+static TRANSLATIONS: Translations = Translations {};
 
 /// Produce the message to be displayed when running `cargo i18n -h`.
 fn short_about() -> String {
@@ -73,7 +76,7 @@ fn main() -> Result<()> {
     let mut language_requester = DesktopLanguageRequester::new();
 
     let cargo_i18n_localizer =
-        DefaultLocalizer::new(&LANGUAGE_LOADER, &TRANSLATIONS as &dyn I18nEmbedDyn);
+        DefaultLocalizer::new(&*LANGUAGE_LOADER, &TRANSLATIONS as &dyn I18nEmbedDyn);
 
     let cargo_i18n_localizer_rc: Rc<Box<dyn Localizer>> = Rc::new(Box::from(cargo_i18n_localizer));
     let i18n_build_localizer = Rc::new(i18n_build::localizer());


Test patch: