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

Issue:
i18n-embed-fl tries to load i18n.toml different from fluent_language_loader
`fluent_language_loader` uses:
https://github.com/kellpossible/cargo-i18n/blob/ff4ee1d42a25c49a9d0190c22d75fa4cbe64578e/i18n-embed/i18n-embed-impl/src/lib.rs#L88

while `fl!` uses:
https://github.com/kellpossible/cargo-i18n/blob/ff4ee1d42a25c49a9d0190c22d75fa4cbe64578e/i18n-embed-fl/src/lib.rs#L290-L296

In workspaces, for example, a `sample-workspace` contains `sample-project`, `fluent_language_loader` tries to load `sample-workspace/i18n.toml`, while `fl!` tries to load `sample-workspace/sample-project/i18n.toml`.


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 997f4b2..c61ef33 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -446,6 +446,16 @@ version = "0.2.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
 
+[[package]]
+name = "library-fluent"
+version = "0.1.0"
+dependencies = [
+ "i18n-embed",
+ "i18n-embed-fl",
+ "once_cell",
+ "rust-embed",
+]
+
 [[package]]
 name = "locale_config"
 version = "0.3.0"
@@ -537,6 +547,12 @@ dependencies = [
  "objc",
 ]
 
+[[package]]
+name = "once_cell"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
+
 [[package]]
 name = "ouroboros"
 version = "0.5.1"
@@ -673,9 +689,9 @@ checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
 
 [[package]]
 name = "rust-embed"
-version = "5.8.0"
+version = "5.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abcb6e9589c2b7875dc4cdfecc6289ea75700a52976b52ef4443d7aad46f7397"
+checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523"
 dependencies = [
  "rust-embed-impl",
  "rust-embed-utils",
@@ -684,9 +700,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed-impl"
-version = "5.8.0"
+version = "5.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc6a6c2785d73d8f0157d10a40223bbf0210f18aecb261d39b96802f9ccc69d"
+checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index f1b1aba..c3ef546 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,4 +39,7 @@ members = [
     "i18n-config",
     "i18n-embed",
     "i18n-embed-fl",
+
+    # Examples
+    "examples/library-fluent",
 ]
\ No newline at end of file
diff --git a/examples/library-fluent/Cargo.toml b/examples/library-fluent/Cargo.toml
new file mode 100644
index 0000000..2387a87
--- /dev/null
+++ b/examples/library-fluent/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "library-fluent"
+version = "0.1.0"
+authors = ["Luke Frisken <l.frisken@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+i18n-embed = { path = "../../i18n-embed", features = ["fluent-system"] }
+i18n-embed-fl = { path = "../../i18n-embed-fl" }
+once_cell = "1.5"
+rust-embed = "5.9"
\ No newline at end of file
diff --git a/examples/library-fluent/README.md b/examples/library-fluent/README.md
new file mode 100644
index 0000000..58cc72e
--- /dev/null
+++ b/examples/library-fluent/README.md
@@ -0,0 +1,5 @@
+# `library-fluent` `i18n-embed` Example
+
+This example demonstrates how to use [i18n-embed](../../i18n-embed/) in
+conjunction with the `fluent` localization system to localize a crate library
+that can be re-used in other projects.
diff --git a/examples/library-fluent/i18n.toml b/examples/library-fluent/i18n.toml
new file mode 100644
index 0000000..454b380
--- /dev/null
+++ b/examples/library-fluent/i18n.toml
@@ -0,0 +1,12 @@
+# (Required) The language identifier of the language used in the
+# source code for gettext system, and the primary fallback language
+# (for which all strings must be present) when using the fluent
+# system.
+fallback_language = "en"
+
+# (Optional) Use the fluent localization system.
+[fluent]
+# (Required) The path to the assets directory.
+# The paths inside the assets directory should be structured like so:
+# `assets_dir/{language}/{domain}.ftl`
+assets_dir = "i18n"
\ No newline at end of file
diff --git a/examples/library-fluent/i18n/en/library_fluent.ftl b/examples/library-fluent/i18n/en/library_fluent.ftl
new file mode 100644
index 0000000..3aad3b9
--- /dev/null
+++ b/examples/library-fluent/i18n/en/library_fluent.ftl
@@ -0,0 +1,1 @@
+hello-world = Hello World!
\ No newline at end of file
diff --git a/examples/library-fluent/i18n/fr/library_fluent.ftl b/examples/library-fluent/i18n/fr/library_fluent.ftl
new file mode 100644
index 0000000..867dcf9
--- /dev/null
+++ b/examples/library-fluent/i18n/fr/library_fluent.ftl
@@ -0,0 +1,1 @@
+hello-world = Bonjour le monde!
\ No newline at end of file
diff --git a/examples/library-fluent/src/lib.rs b/examples/library-fluent/src/lib.rs
new file mode 100644
index 0000000..dab5fc0
--- /dev/null
+++ b/examples/library-fluent/src/lib.rs
@@ -0,0 +1,44 @@
+use i18n_embed::{
+    fluent::{fluent_language_loader, FluentLanguageLoader},
+    DefaultLocalizer, LanguageLoader, Localizer,
+};
+use i18n_embed_fl::fl;
+use once_cell::sync::Lazy;
+use rust_embed::RustEmbed;
+
+#[derive(RustEmbed)]
+#[folder = "i18n/"]
+struct Localizations;
+
+static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
+    let loader: FluentLanguageLoader = fluent_language_loader!();
+
+    // Load the fallback langauge by default so that users of the
+    // library don't need to if they don't care about localization.
+    loader
+        .load_fallback_language(&Localizations)
+        .expect("Error while loading fallback language");
+
+    loader
+});
+
+macro_rules! fl {
+    ($message_id:literal) => {{
+        i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id)
+    }};
+
+    ($message_id:literal, $($args:expr),*) => {{
+        i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id, $($args), *)
+    }};
+}
+
+/// Get the hello world statement in whatever the currently selected
+/// localization is.
+pub fn hello_world() -> String {
+    fl!("hello-world")
+}
+
+// Get the `Localizer` to be used for localizing this library.
+pub fn localizer() -> Box<dyn Localizer> {
+    Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
+}
diff --git a/i18n-build/CHANGELOG.md b/i18n-build/CHANGELOG.md
index 6e1582e..605face 100644
--- a/i18n-build/CHANGELOG.md
+++ b/i18n-build/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog for `i18n-build`
 
+## master
+
+### Internal Changes
+
++ Fix clippy warnings.
+
 ## v0.5.4
 
 + Update `i18n-embed` to version `0.10`.
diff --git a/i18n-build/src/gettext_impl/mod.rs b/i18n-build/src/gettext_impl/mod.rs
index bed265f..7d1abde 100644
--- a/i18n-build/src/gettext_impl/mod.rs
+++ b/i18n-build/src/gettext_impl/mod.rs
@@ -139,7 +139,7 @@ pub fn run_xtr(
         msgcat_args.push(Box::from(path.as_os_str()));
     }
 
-    let combined_pot_file_path = crate_module_pot_file_path(crt, pot_dir)?;
+    let combined_pot_file_path = crate_module_pot_file_path(crt, pot_dir);
 
     run_msgcat(&pot_paths, &combined_pot_file_path)
         .context("There was a problem while trying to run the \"msgcat\" command.")?;
@@ -147,11 +147,11 @@ pub fn run_xtr(
     Ok(())
 }
 
-fn crate_module_pot_file_path<P: AsRef<Path>>(crt: &Crate<'_>, pot_dir: P) -> Result<PathBuf> {
-    Ok(pot_dir
+fn crate_module_pot_file_path<P: AsRef<Path>>(crt: &Crate<'_>, pot_dir: P) -> PathBuf {
+    pot_dir
         .as_ref()
         .join(crt.module_name())
-        .with_extension("pot"))
+        .with_extension("pot")
 }
 
 /// Run the gettext utils `msgcat` command to concatinate pot files
@@ -466,21 +466,19 @@ pub fn run(crt: &Crate) -> Result<()> {
         assert!(crt.gettext_config_or_err()?.collate_extracted_subcrates);
         concatinate_crates.insert(0, crt);
 
-        let concatinate_crate_paths_result: Result<Vec<PathBuf>, _> = concatinate_crates
+        let concatinate_crate_paths: Vec<PathBuf> = concatinate_crates
             .iter()
             .map(|concat_crt: &&Crate| crate_module_pot_file_path(concat_crt, &pot_dir))
             .collect();
 
-        let concatinate_crate_paths = concatinate_crate_paths_result?;
-
-        let output_pot_path = crate_module_pot_file_path(crt, &pot_dir)?;
+        let output_pot_path = crate_module_pot_file_path(crt, &pot_dir);
         run_msgcat(concatinate_crate_paths, output_pot_path)?;
 
         // remove this crate from the list because we don't want to delete it's pot file
         concatinate_crates.remove(0);
 
         for subcrate in concatinate_crates {
-            let subcrate_output_pot_path = crate_module_pot_file_path(subcrate, &pot_dir)?;
+            let subcrate_output_pot_path = crate_module_pot_file_path(subcrate, &pot_dir);
             util::remove_file_or_error(subcrate_output_pot_path)?;
         }
     }
diff --git a/i18n-config/CHANGELOG.md b/i18n-config/CHANGELOG.md
index f6c4b8e..a58ae47 100644
--- a/i18n-config/CHANGELOG.md
+++ b/i18n-config/CHANGELOG.md
@@ -1,4 +1,10 @@
-# v0.4.0
+# `i18n-config` Changelog
+
+## master
+
+### New Features
+
++ New `locate_crate_paths()` function for use in procedural macros.
 
 ## v0.4.0
 
diff --git a/i18n-config/src/lib.rs b/i18n-config/src/lib.rs
index 7421cc7..f91378d 100644
--- a/i18n-config/src/lib.rs
+++ b/i18n-config/src/lib.rs
@@ -43,6 +43,8 @@ pub enum I18nConfigError {
         "There is no i18n config file present for the parent crate of {0}. Required because {1}."
     )]
     NoParentI18nConfig(String, String),
+    #[error("Cannot read `CARGO_MANIFEST_DIR` environment variable.")]
+    CannotReadCargoManifestDir,
 }
 
 /// Represents a rust crate.
@@ -380,3 +382,29 @@ impl I18nConfig {
         Ok(config)
     }
 }
+
+/// Important i18n-config paths related to the current crate.
+pub struct CratePaths {
+    /// The current crate directory path (where the `Cargo.toml` is
+    /// located).
+    pub crate_dir: PathBuf,
+    /// The current i18n config file path
+    pub i18n_config_file: PathBuf,
+}
+
+/// Locate the current crate's directory and `i18n.toml` config file.
+/// This is intended to be called by a procedural macro during crate
+/// compilation.
+pub fn locate_crate_paths() -> Result<CratePaths, I18nConfigError> {
+    let crate_dir = Path::new(
+        &std::env::var_os("CARGO_MANIFEST_DIR")
+            .ok_or(I18nConfigError::CannotReadCargoManifestDir)?,
+    )
+    .to_path_buf();
+    let i18n_config_file = crate_dir.join("i18n.toml").to_path_buf();
+
+    Ok(CratePaths {
+        crate_dir,
+        i18n_config_file,
+    })
+}
diff --git a/i18n-embed-fl/CHANGELOG.md b/i18n-embed-fl/CHANGELOG.md
index 1bc2247..3be6666 100644
--- a/i18n-embed-fl/CHANGELOG.md
+++ b/i18n-embed-fl/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog for `i18n-embed-fl`
 
+## master
+
+### Internal Changes
+
++ Refactoring during the fix for [#60](https://github.com/kellpossible/cargo-i18n/issues/60).
+
 ## v0.3.1
 
 ### Internal Changes
diff --git a/i18n-embed-fl/src/lib.rs b/i18n-embed-fl/src/lib.rs
index 53bd54e..7546021 100644
--- a/i18n-embed-fl/src/lib.rs
+++ b/i18n-embed-fl/src/lib.rs
@@ -287,19 +287,16 @@ pub fn fl(input: TokenStream) -> TokenStream {
     let domain_data = if let Some(domain_data) = DOMAINS.get(&domain) {
         domain_data
     } else {
-        let crate_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap_or_else(|| {
-            panic!(
-                "fl!() had a problem reading `CARGO_MANIFEST_DIR` \
-            environment variable"
-            )
-        });
-        let config_file_path = Path::new(&crate_dir).join("i18n.toml");
+        let crate_paths = i18n_config::locate_crate_paths()
+            .unwrap_or_else(|error| panic!("fl!() is unable to locate crate paths: {}", error));
+
+        let config_file_path = &crate_paths.i18n_config_file;
 
         let config = i18n_config::I18nConfig::from_file(&config_file_path).unwrap_or_else(|err| {
             abort! {
                 proc_macro2::Span::call_site(),
                 format!(
-                    "fl!() had a problem reading config file {0:?}: {1}",
+                    "fl!() had a problem reading i18n config file {0:?}: {1}",
                     config_file_path, err);
                 help = "Try creating the `i18n.toml` configuration file.";
             }
@@ -309,7 +306,7 @@ pub fn fl(input: TokenStream) -> TokenStream {
             abort! {
                 proc_macro2::Span::call_site(),
                 format!(
-                    "fl!() had a problem parsing config file {0:?}: \
+                    "fl!() had a problem parsing i18n config file {0:?}: \
                     there is no `[fluent]` subsection.",
                     config_file_path);
                 help = "Add the `[fluent]` subsection to `i18n.toml`, \
@@ -317,7 +314,7 @@ pub fn fl(input: TokenStream) -> TokenStream {
             }
         });
 
-        let assets_dir = Path::new(&crate_dir).join(fluent_config.assets_dir);
+        let assets_dir = Path::new(&crate_paths.crate_dir).join(fluent_config.assets_dir);
         let assets = FileSystemAssets::new(assets_dir);
 
         let fallback_language: LanguageIdentifier = config.fallback_language;
diff --git a/i18n-embed/CHANGELOG.md b/i18n-embed/CHANGELOG.md
index 972da2a..e38d492 100644
--- a/i18n-embed/CHANGELOG.md
+++ b/i18n-embed/CHANGELOG.md
@@ -1,5 +1,25 @@
 # Changelog for `i18n-embed`
 
+## master
+
+### Documentation
+
++ Updated/improved examples, including an improvement for how to expose localization from a library using the `fluent` system, ensuring that the fallback language is loaded by default.
++ Updated examples with new `LanguageRequester::add_listener()` signature now using `std::sync::Arc` and `std::sync::Weak`.
+
+### Breaking Changes
+
++ Fix for [#60](https://github.com/kellpossible/cargo-i18n/issues/60) where `i18n-embed-fl` loads `i18n.toml` from a different path to `fluent_language_loader!()`. For subcrates in a workspace previously `fluent_language_loader!()` and `gettext_language_loader!()` were searching for `i18n.toml` in the crate root of the workspace, rather than the current subcrate. This was not the expected behaviour. This fix could be considered a breaking change for libraries that inadvertently relied on that behaviour.
++ For `LanguageRequester::add_listener()` change the `listener` type to `std::sync::Weak`, to better support temporary dependencies that may come from another thread. This should not be a performance bottleneck. This also affects `DesktopLanguageRequester` and `WebLanguageRequester`.
+
+### New Features
+
++ New `LanguageRequester::add_listener_ref()` method to add permenant listeners of type `&dyn Localizer`. This also affects `DesktopLanguageRequester` and `WebLanguageRequester`.
+
+### Internal Changes
+
++ Fix clippy warnings.
+
 ## v0.10.2
 
 + Add workaround for [#57](https://github.com/kellpossible/cargo-i18n/issues/57) for until <https://github.com/projectfluent/fluent-rs/issues/213> is solved.
diff --git a/i18n-embed/README.md b/i18n-embed/README.md
index 4d3cd07..171e1cf 100644
--- a/i18n-embed/README.md
+++ b/i18n-embed/README.md
@@ -74,10 +74,9 @@ use rust_embed::RustEmbed;
 
 #[derive(RustEmbed)]
 #[folder = "i18n"] // path to the compiled localization resources
-struct Translations;
+struct Localizations;
 
 fn main() {
-    let translations = Translations {};
     let language_loader: FluentLanguageLoader = fluent_language_loader!();
 
     // Use the language requester for the desktop platform (linux, windows, mac).
@@ -85,7 +84,7 @@ fn main() {
     // WebLanguageRequester, or you can implement your own.
     let requested_languages = DesktopLanguageRequester::requested_languages();
     let _result = i18n_embed::select(
-        &language_loader, &translations, &requested_languages);
+        &language_loader, &Localizations, &requested_languages);
 
     // continue on with your application
 }
diff --git a/i18n-embed/i18n-embed-impl/src/lib.rs b/i18n-embed/i18n-embed-impl/src/lib.rs
index 767539a..9611404 100644
--- a/i18n-embed/i18n-embed-impl/src/lib.rs
+++ b/i18n-embed/i18n-embed-impl/src/lib.rs
@@ -29,15 +29,30 @@ pub fn gettext_language_loader(_: proc_macro::TokenStream) -> proc_macro::TokenS
     let i18n_embed_crate_ident =
         syn::Ident::new(&i18n_embed_crate_name, proc_macro2::Span::call_site());
 
-    let config_file_path = std::path::PathBuf::from("i18n.toml");
+    let config_file_path = i18n_config::locate_crate_paths()
+        .unwrap_or_else(|error| {
+            panic!(
+                "gettext_language_loader!() is unable to locate i18n config file: {}",
+                error
+            )
+        })
+        .i18n_config_file;
 
     let config = i18n_config::I18nConfig::from_file(&config_file_path).unwrap_or_else(|err| {
         panic!(
-            "gettext_language_loader!() had a problem reading config file {0:?}: {1}",
-            config_file_path, err
+            "gettext_language_loader!() had a problem reading i18n config file {0:?}: {1}",
+            std::fs::canonicalize(&config_file_path).unwrap_or(config_file_path.clone()),
+            err
         )
     });
 
+    if !config.gettext.is_some() {
+        panic!(
+            "gettext_language_loader!() had a problem parsing i18n config file {0:?}: there is no `[gettext]` section",
+            std::fs::canonicalize(&config_file_path).unwrap_or(config_file_path.clone())
+        )
+    }
+
     let fallback_language = syn::LitStr::new(
         &config.fallback_language.to_string(),
         proc_macro2::Span::call_site(),
@@ -85,14 +100,30 @@ pub fn fluent_language_loader(_: proc_macro::TokenStream) -> proc_macro::TokenSt
     let i18n_embed_crate_ident =
         syn::Ident::new(&i18n_embed_crate_name, proc_macro2::Span::call_site());
 
-    let config_file_path = std::path::PathBuf::from("i18n.toml");
+    let config_file_path = i18n_config::locate_crate_paths()
+        .unwrap_or_else(|error| {
+            panic!(
+                "fluent_language_loader!() is unable to locate i18n config file: {}",
+                error
+            )
+        })
+        .i18n_config_file;
 
     let config = i18n_config::I18nConfig::from_file(&config_file_path).unwrap_or_else(|err| {
         panic!(
-            "fluent_language_loader!() had a problem reading config file {0:?}: {1}",
-            config_file_path, err
+            "fluent_language_loader!() had a problem reading i18n config file {0:?}: {1}",
+            std::fs::canonicalize(&config_file_path).unwrap_or(config_file_path.clone()),
+            err
         )
     });
+
+    if !config.fluent.is_some() {
+        panic!(
+            "fluent_language_loader!() had a problem parsing i18n config file {0:?}: there is no `[fluent]` section",
+            std::fs::canonicalize(&config_file_path).unwrap_or(config_file_path.clone())
+        )
+    }
+
     let fallback_language = syn::LitStr::new(
         &config.fallback_language.to_string(),
         proc_macro2::Span::call_site(),
diff --git a/i18n-embed/i18n.toml b/i18n-embed/i18n.toml
index 40cabe9..3862cdf 100644
--- a/i18n-embed/i18n.toml
+++ b/i18n-embed/i18n.toml
@@ -16,4 +16,14 @@ target_languages = ["es", "ru", "fr"]
 # the crate being localized.
 output_dir = "i18n"
 
-msgid_bugs_address = "l.frisken@gmail.com"
\ No newline at end of file
+# (Optional) The reporting address for msgid bugs. This is the email address or
+# URL to which the translators shall report bugs in the untranslated
+# strings.
+msgid_bugs_address = "l.frisken@gmail.com"
+
+# (Optional) Use the fluent localization system.
+[fluent]
+# (Required) The path to the assets directory.
+# The paths inside the assets directory should be structured like so:
+# `assets_dir/{language}/{domain}.ftl`
+assets_dir = "i18n"
\ No newline at end of file
diff --git a/i18n-embed/src/lib.rs b/i18n-embed/src/lib.rs
index 37bfcf9..758425b 100644
--- a/i18n-embed/src/lib.rs
+++ b/i18n-embed/src/lib.rs
@@ -112,12 +112,10 @@
 //!
 //! #[derive(RustEmbed)]
 //! #[folder = "i18n"] // path to the compiled localization resources
-//! struct Translations;
+//! struct Localizations;
 //!
 //!
 //! fn main() {
-//!     let translations = Translations {};
-//!
 //!     let language_loader: FluentLanguageLoader = fluent_language_loader!();
 //!
 //!     // Use the language requester for the desktop platform (linux, windows, mac).
@@ -126,7 +124,7 @@
 //!     let requested_languages = DesktopLanguageRequester::requested_languages();
 //!
 //!     let _result = i18n_embed::select(
-//!         &language_loader, &translations, &requested_languages);
+//!         &language_loader, &Localizations, &requested_languages);
 //!
 //!     // continue on with your application
 //! }
@@ -213,12 +211,10 @@
 //! // path to the compiled localization resources,
 //! // as determined by i18n.toml settings
 //! #[folder = "i18n/mo"]
-//! struct Translations;
+//! struct Localizations;
 //!
 //!
 //! fn main() {
-//!     let translations = Translations {};
-//!
 //!     // Create the GettextLanguageLoader, pulling in settings from `i18n.toml`
 //!     // at compile time using the macro.
 //!     let language_loader = gettext_language_loader!();
@@ -229,7 +225,7 @@
 //!     let requested_languages = DesktopLanguageRequester::requested_languages();
 //!
 //!     let _result = i18n_embed::select(
-//!         &language_loader, &translations, &requested_languages);
+//!         &language_loader, &Localizations, &requested_languages);
 //!
 //!     // continue on with your application
 //! }
@@ -243,7 +239,7 @@
 //! update the selected language using a [Localizer](Localizer):
 //!
 //! ```
-//! use std::rc::Rc;
+//! use std::sync::Arc;
 //! use i18n_embed::{
 //!     DesktopLanguageRequester, LanguageRequester,
 //!     DefaultLocalizer, Localizer, fluent::FluentLanguageLoader     
@@ -253,9 +249,7 @@
 //!
 //! #[derive(RustEmbed)]
 //! #[folder = "i18n/ftl"] // path to localization resources
-//! struct Translations;
-//!
-//! const TRANSLATIONS: Translations = Translations {};
+//! struct Localizations;
 //!
 //! lazy_static! {
 //!     static ref LANGUAGE_LOADER: FluentLanguageLoader = {
@@ -269,12 +263,12 @@
 //! }
 //!
 //! fn main() {
-//!     let localizer = DefaultLocalizer::new(&*LANGUAGE_LOADER, &TRANSLATIONS);
+//!     let localizer = DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations);
 //!
-//!     let localizer_rc: Rc<dyn Localizer> = Rc::new(localizer);
+//!     let localizer_arc: Arc<dyn Localizer> = Arc::new(localizer);
 //!
 //!     let mut language_requester = DesktopLanguageRequester::new();
-//!     language_requester.add_listener(Rc::downgrade(&localizer_rc));
+//!     language_requester.add_listener(Arc::downgrade(&localizer_arc));
 //!
 //!     // Manually check the currently requested system language,
 //!     // and update the listeners. NOTE: Support for this across systems
@@ -302,27 +296,36 @@
 //! you can follow this code pattern in the library itself:
 //!
 //! ```
+//! use std::sync::Arc;
 //! use i18n_embed::{
-//!     DefaultLocalizer, Localizer,
-//!     gettext::{
-//!         gettext_language_loader, GettextLanguageLoader     
+//!     DefaultLocalizer, Localizer, LanguageLoader,
+//!     fluent::{
+//!         fluent_language_loader, FluentLanguageLoader     
 //! }};
 //! use rust_embed::RustEmbed; use lazy_static::lazy_static;
 //!
 //! #[derive(RustEmbed)]
 //! #[folder = "i18n/mo"] // path to the compiled localization resources
-//! struct Translations;
-//! const TRANSLATIONS: Translations = Translations {};
+//! struct Localizations;
 //!
 //! lazy_static! {
-//!     static ref LANGUAGE_LOADER: GettextLanguageLoader =
-//!         gettext_language_loader!();
+//!     static ref LANGUAGE_LOADER: FluentLanguageLoader = {
+//!         let loader = fluent_language_loader!();
+//!
+//!         // Load the fallback langauge by default so that users of the
+//!         // library don't need to if they don't care about localization.
+//!         // This isn't required for the `gettext` localization system.
+//!         loader.load_fallback_language(&Localizations)
+//!             .expect("Error while loading fallback language");
+//!
+//!         loader
+//!     };
 //! }
 //!
 //! // Get the `Localizer` to be used for localizing this library.
 //! # #[allow(unused)]
-//! pub fn localizer() -> Box<dyn Localizer> {
-//!     Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &TRANSLATIONS))
+//! pub fn localizer() -> Arc<dyn Localizer> {
+//!     Arc::new(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
 //! }
 //! ```
 //!
@@ -338,6 +341,7 @@
 //! for the library:
 //!
 //! ```
+//! use std::sync::Arc;
 //! use i18n_embed::{
 //!     DefaultLocalizer, Localizer, gettext::{
 //!     gettext_language_loader, GettextLanguageLoader     
@@ -353,8 +357,8 @@
 //! /// Get the `Localizer` to be used for localizing this library,
 //! /// using the provided embeddes source of language files `embed`.
 //! # #[allow(unused)]
-//! pub fn localizer(embed: &dyn I18nAssets) -> Box<dyn Localizer + '_> {
-//!     Box::from(DefaultLocalizer::new(
+//! pub fn localizer(embed: &dyn I18nAssets) -> Arc<dyn Localizer + '_> {
+//!     Arc::new(DefaultLocalizer::new(
 //!         &*LANGUAGE_LOADER,
 //!         embed
 //!     ))
@@ -621,12 +625,9 @@ pub trait LanguageLoader {
                 let components: Vec<Component<'_>> = path.components().collect();
 
                 let locale: Option<String> = match components.get(0) {
-                    Some(language_component) => match language_component {
-                        Component::Normal(s) => {
-                            Some(s.to_str().expect("path should be valid utf-8").to_string())
-                        }
-                        _ => None,
-                    },
+                    Some(Component::Normal(s)) => {
+                        Some(s.to_str().expect("path should be valid utf-8").to_string())
+                    }
                     _ => None,
                 };
 
@@ -682,6 +683,7 @@ pub trait LanguageLoader {
 
     /// Get the language which is currently loaded for this loader.
     fn current_language(&self) -> unic_langid::LanguageIdentifier;
+
     /// Load the languages `language_ids` using the resources packaged
     /// in the `i18n_embed` in order of fallback preference. This also
     /// sets the [LanguageLoader::current_language()] to the first in
@@ -693,6 +695,11 @@ pub trait LanguageLoader {
         i18n_assets: &dyn I18nAssets,
         language_ids: &[&unic_langid::LanguageIdentifier],
     ) -> Result<(), I18nEmbedError>;
+
+    /// Load the [LanguageLoader::fallback_language()].
+    fn load_fallback_language(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> {
+        self.load_languages(i18n_assets, &[self.fallback_language()])
+    }
 }
 
 /// Populate gettext database with strings for use with tests.
diff --git a/i18n-embed/src/requester.rs b/i18n-embed/src/requester.rs
index cc021e5..0544c7e 100644
--- a/i18n-embed/src/requester.rs
+++ b/i18n-embed/src/requester.rs
@@ -1,18 +1,32 @@
 use crate::{I18nEmbedError, Localizer};
-use std::{collections::HashMap, rc::Weak};
+use std::{collections::HashMap, sync::Weak};
 
 /// A trait used by [I18nAssets](crate::I18nAssets) to ascertain which
 /// languages are being requested.
-pub trait LanguageRequester {
+pub trait LanguageRequester<'a> {
     /// Add a listener to this `LanguageRequester`. When the system
     /// reports that the currently requested languages has changed,
     /// each listener will have its
-    /// [Localizer#select()](Localizer#select()) method called.
+    /// [Localizer#select()](Localizer#select()) method called. [Weak]
+    /// is used so that when the [Arc](std::sync::Arc) that it references
+    /// is dropped, the listener will also be removed next time this
+    /// requester is polled/updates.
     ///
     /// If you haven't already selected a language for the localizer
     /// you are adding here, you may want to manually call
     /// [#poll()](#poll()) after adding the listener/s.
-    fn add_listener(&mut self, localizer: Weak<dyn Localizer>);
+    fn add_listener(&mut self, listener: Weak<dyn Localizer>);
+    /// Add a listener to this `LanguageRequester`. When the system
+    /// reports that the currently requested languages has changed,
+    /// each listener will have its
+    /// [Localizer#select()](Localizer#select()) method called. As
+    /// opposed to [LanguageRequester::add_listener()], this listener
+    /// will not be removed.
+    ///
+    /// If you haven't already selected a language for the localizer
+    /// you are adding here, you may want to manually call
+    /// [#poll()](#poll()) after adding the listener/s.
+    fn add_listener_ref(&mut self, listener: &'a dyn Localizer);
     /// Poll the system's currently selected language, and call
     /// [Localizer#select()](Localizer#select()) on each of the
     /// listeners.
@@ -41,16 +55,18 @@ pub trait LanguageRequester {
 
 /// Provide the functionality for overrides and listeners for a
 /// [LanguageRequester](LanguageRequester) implementation.
-pub struct LanguageRequesterImpl {
-    listeners: Vec<Weak<dyn Localizer>>,
+pub struct LanguageRequesterImpl<'a> {
+    arc_listeners: Vec<Weak<dyn Localizer>>,
+    ref_listeners: Vec<&'a dyn Localizer>,
     language_override: Option<unic_langid::LanguageIdentifier>,
 }
 
-impl LanguageRequesterImpl {
+impl<'a> LanguageRequesterImpl<'a> {
     /// Create a new [LanguageRequesterImpl](LanguageRequesterImpl).
-    pub fn new() -> LanguageRequesterImpl {
+    pub fn new() -> LanguageRequesterImpl<'a> {
         LanguageRequesterImpl {
-            listeners: Vec::new(),
+            arc_listeners: Vec::new(),
+            ref_listeners: Vec::new(),
             language_override: None,
         }
     }
@@ -68,8 +84,14 @@ impl LanguageRequesterImpl {
 
     /// Add a weak reference to a [Localizer], which listens to
     /// changes to the current language.
-    pub fn add_listener(&mut self, localizer: Weak<dyn Localizer>) {
-        self.listeners.push(localizer);
+    pub fn add_listener(&mut self, listener: Weak<dyn Localizer>) {
+        self.arc_listeners.push(listener);
+    }
+
+    /// Add a reference to [Localizer], which listens to changes to
+    /// the current language.
+    pub fn add_listener_ref(&mut self, listener: &'a dyn Localizer) {
+        self.ref_listeners.push(listener);
     }
 
     /// With the provided `requested_languages` call
@@ -81,19 +103,23 @@ impl LanguageRequesterImpl {
     ) -> Result<(), I18nEmbedError> {
         let mut errors: Vec<I18nEmbedError> = Vec::new();
 
-        self.listeners.retain(|listener| match listener.upgrade() {
-            Some(l) => {
-                match l.select(&requested_languages) {
-                    Ok(_) => {}
-                    Err(err) => {
-                        errors.push(err);
+        self.arc_listeners
+            .retain(|listener| match listener.upgrade() {
+                Some(arc_listener) => {
+                    if let Err(error) = arc_listener.select(&requested_languages) {
+                        errors.push(error);
                     }
+
+                    true
                 }
+                None => false,
+            });
 
-                true
+        for boxed_listener in &self.ref_listeners {
+            if let Err(error) = boxed_listener.select(&requested_languages) {
+                errors.push(error);
             }
-            None => false,
-        });
+        }
 
         if errors.is_empty() {
             Ok(())
@@ -130,9 +156,10 @@ impl LanguageRequesterImpl {
         &self,
     ) -> Result<Vec<unic_langid::LanguageIdentifier>, I18nEmbedError> {
         let mut available_languages = std::collections::HashSet::new();
-        for weak_listener in &self.listeners {
-            if let Some(localizer) = weak_listener.upgrade() {
-                localizer
+
+        for weak_arc_listener in &self.arc_listeners {
+            if let Some(arc_listener) = weak_arc_listener.upgrade() {
+                arc_listener
                     .available_languages()?
                     .iter()
                     .for_each(|language| {
@@ -141,6 +168,15 @@ impl LanguageRequesterImpl {
             }
         }
 
+        for boxed_listener in &self.ref_listeners {
+            boxed_listener
+                .available_languages()?
+                .iter()
+                .for_each(|language| {
+                    available_languages.insert(language.clone());
+                })
+        }
+
         Ok(available_languages.into_iter().collect())
     }
 
@@ -148,7 +184,7 @@ impl LanguageRequesterImpl {
     /// (value) per domain (key).
     pub fn current_languages(&self) -> HashMap<String, unic_langid::LanguageIdentifier> {
         let mut current_languages = HashMap::new();
-        for weak_listener in &self.listeners {
+        for weak_listener in &self.arc_listeners {
             if let Some(localizer) = weak_listener.upgrade() {
                 let loader = localizer.language_loader();
                 current_languages.insert(loader.domain().to_string(), loader.current_language());
@@ -159,16 +195,16 @@ impl LanguageRequesterImpl {
     }
 }
 
-impl Default for LanguageRequesterImpl {
+impl Default for LanguageRequesterImpl<'_> {
     fn default() -> Self {
         Self::new()
     }
 }
 
-impl std::fmt::Debug for LanguageRequesterImpl {
+impl std::fmt::Debug for LanguageRequesterImpl<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let listeners_debug: String = self
-            .listeners
+            .arc_listeners
             .iter()
             .map(|l| match l.upgrade() {
                 Some(l) => format!("{:p}", l),
@@ -192,18 +228,22 @@ impl std::fmt::Debug for LanguageRequesterImpl {
 /// ⚠️ *This API requires the following crate features to be activated: `desktop-requester`.*
 #[cfg(feature = "desktop-requester")]
 #[derive(Debug)]
-pub struct DesktopLanguageRequester {
-    implementation: LanguageRequesterImpl,
+pub struct DesktopLanguageRequester<'a> {
+    implementation: LanguageRequesterImpl<'a>,
 }
 
 #[cfg(feature = "desktop-requester")]
-impl LanguageRequester for DesktopLanguageRequester {
+impl<'a> LanguageRequester<'a> for DesktopLanguageRequester<'a> {
     fn requested_languages(&self) -> Vec<unic_langid::LanguageIdentifier> {
         Self::requested_languages()
     }
 
-    fn add_listener(&mut self, localizer: Weak<dyn Localizer>) {
-        self.implementation.add_listener(localizer)
+    fn add_listener(&mut self, listener: Weak<dyn Localizer>) {
+        self.implementation.add_listener(listener)
+    }
+
+    fn add_listener_ref(&mut self, listener: &'a dyn Localizer) {
+        self.implementation.add_listener_ref(listener)
     }
 
     fn set_language_override(
@@ -227,16 +267,16 @@ impl LanguageRequester for DesktopLanguageRequester {
 }
 
 #[cfg(feature = "desktop-requester")]
-impl<'a> Default for DesktopLanguageRequester {
+impl Default for DesktopLanguageRequester<'_> {
     fn default() -> Self {
         DesktopLanguageRequester::new()
     }
 }
 
 #[cfg(feature = "desktop-requester")]
-impl DesktopLanguageRequester {
+impl DesktopLanguageRequester<'_> {
     /// Create a new `DesktopLanguageRequester`.
-    pub fn new() -> DesktopLanguageRequester {
+    pub fn new() -> Self {
         DesktopLanguageRequester {
             implementation: LanguageRequesterImpl::new(),
         }
@@ -252,33 +292,16 @@ impl DesktopLanguageRequester {
 
         let ids: Vec<unic_langid::LanguageIdentifier> = current_locale
             .tags_for("messages")
-            .filter_map(|tag: LanguageRange<'_>| {
-                let tag_string = tag.to_string();
-
-                if tag_string.is_empty() {
-                    log::debug!(
-                        "System has an empty requested locale for messages. \
-                    In this situation on Linux the `LANG` environment variable \
-                    may be empty."
-                    );
+            .filter_map(|tag: LanguageRange<'_>| match tag.to_string().parse() {
+                Ok(tag) => Some(tag),
+                Err(err) => {
+                    log::error!("Unable to parse your locale: {:?}", err);
                     None
-                } else {
-                    match tag_string.parse() {
-                        Ok(tag) => Some(tag),
-                        Err(err) => {
-                            log::error!(
-                                "Unable to parse system's currently requested locale `{}`: {}",
-                                tag_string,
-                                err
-                            );
-                            None
-                        }
-                    }
                 }
             })
             .collect();
 
-        log::info!("System requested locales: {:?}", ids);
+        log::info!("Current Locale: {:?}", ids);
 
         ids
     }
@@ -289,14 +312,14 @@ impl DesktopLanguageRequester {
 /// ⚠️ *This API requires the following crate features to be activated: `web-sys-requester`.*
 #[cfg(feature = "web-sys-requester")]
 #[derive(Debug)]
-pub struct WebLanguageRequester {
-    implementation: LanguageRequesterImpl,
+pub struct WebLanguageRequester<'a> {
+    implementation: LanguageRequesterImpl<'a>,
 }
 
 #[cfg(feature = "web-sys-requester")]
-impl WebLanguageRequester {
+impl WebLanguageRequester<'_> {
     /// Create a new `WebLanguageRequester`.
-    pub fn new() -> WebLanguageRequester {
+    pub fn new() -> Self {
         WebLanguageRequester {
             implementation: LanguageRequesterImpl::new(),
         }
@@ -321,20 +344,24 @@ impl WebLanguageRequester {
 }
 
 #[cfg(feature = "web-sys-requester")]
-impl Default for WebLanguageRequester {
+impl Default for WebLanguageRequester<'_> {
     fn default() -> Self {
         WebLanguageRequester::new()
     }
 }
 
 #[cfg(feature = "web-sys-requester")]
-impl LanguageRequester for WebLanguageRequester {
+impl<'a> LanguageRequester<'a> for WebLanguageRequester<'a> {
     fn requested_languages(&self) -> Vec<unic_langid::LanguageIdentifier> {
         Self::requested_languages()
     }
 
-    fn add_listener(&mut self, localizer: Weak<dyn Localizer>) {
-        self.implementation.add_listener(localizer)
+    fn add_listener(&mut self, listener: Weak<dyn Localizer>) {
+        self.implementation.add_listener(listener)
+    }
+
+    fn add_listener_ref(&mut self, listener: &'a dyn Localizer) {
+        self.implementation.add_listener_ref(listener)
     }
 
     fn poll(&mut self) -> Result<(), I18nEmbedError> {
diff --git a/src/main.rs b/src/main.rs
index af42a61..a68941e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,8 +9,7 @@ use i18n_embed::{
 };
 use lazy_static::lazy_static;
 use rust_embed::RustEmbed;
-use std::path::Path;
-use std::rc::Rc;
+use std::{path::Path, sync::Arc};
 use tr::tr;
 use unic_langid::LanguageIdentifier;
 
@@ -30,7 +29,7 @@ fn short_about() -> String {
     tr!("A Cargo sub-command to extract and build localization resources.")
 }
 
-fn available_languages(localizer: &Rc<dyn Localizer>) -> Result<Vec<String>> {
+fn available_languages(localizer: &Arc<dyn Localizer>) -> Result<Vec<String>> {
     Ok(localizer
         .available_languages()?
         .iter()
@@ -77,11 +76,11 @@ fn main() -> Result<()> {
     let cargo_i18n_localizer =
         DefaultLocalizer::new(&*LANGUAGE_LOADER, &TRANSLATIONS as &dyn I18nAssets);
 
-    let cargo_i18n_localizer_rc: Rc<dyn Localizer> = Rc::new(cargo_i18n_localizer);
-    let i18n_build_localizer_rc = Rc::new(i18n_build::localizer()) as Rc<dyn Localizer>;
+    let cargo_i18n_localizer_rc: Arc<dyn Localizer> = Arc::new(cargo_i18n_localizer);
+    let i18n_build_localizer_rc = Arc::new(i18n_build::localizer()) as Arc<dyn Localizer>;
 
-    language_requester.add_listener(Rc::downgrade(&cargo_i18n_localizer_rc));
-    language_requester.add_listener(Rc::downgrade(&i18n_build_localizer_rc));
+    language_requester.add_listener(Arc::downgrade(&cargo_i18n_localizer_rc));
+    language_requester.add_listener(Arc::downgrade(&i18n_build_localizer_rc));
     language_requester.poll()?;
 
     let fallback_locale = LANGUAGE_LOADER.fallback_language().to_string();


Test patch:
diff --git a/examples/library-fluent/tests/no_localizer.rs b/examples/library-fluent/tests/no_localizer.rs
new file mode 100644
index 0000000..a798dae
--- /dev/null
+++ b/examples/library-fluent/tests/no_localizer.rs
@@ -0,0 +1,8 @@
+use library_fluent::hello_world;
+
+/// Test using the library without using the provided
+/// [`localizer()`](library_fluent::localizer()) method.
+#[test]
+fn test_no_localizer() {
+    assert_eq!(&hello_world(), "Hello World!")
+}
diff --git a/examples/library-fluent/tests/with_localizer.rs b/examples/library-fluent/tests/with_localizer.rs
new file mode 100644
index 0000000..b738a20
--- /dev/null
+++ b/examples/library-fluent/tests/with_localizer.rs
@@ -0,0 +1,37 @@
+use library_fluent::{hello_world, localizer};
+
+/// Test that the expected languages and fallback language are
+/// available.
+#[test]
+fn test_available_languages() {
+    let localizer = localizer();
+    assert_eq!(
+        &localizer.language_loader().fallback_language().to_string(),
+        "en"
+    );
+
+    let available_ids: Vec<String> = localizer
+        .available_languages()
+        .unwrap()
+        .into_iter()
+        .map(|id| id.to_string())
+        .collect();
+
+    let expected_available_ids: Vec<String> = vec!["en".to_string(), "fr".to_string()];
+
+    assert_eq!(available_ids, expected_available_ids)
+}
+
+/// Test loading the `en` language.
+#[test]
+fn test_select_english() {
+    localizer().select(&["en".parse().unwrap()]).unwrap();
+    assert_eq!(&hello_world(), "Hello World!")
+}
+
+/// Test loading the `fr` language.
+#[test]
+fn test_select_french() {
+    localizer().select(&["fr".parse().unwrap()]).unwrap();
+    assert_eq!(&hello_world(), "Bonjour le monde!")
+}