diff --git a/.gitignore b/.gitignore index ec9fb69..9809653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target **/*.rs.bk .vscode -/i18n/pot \ No newline at end of file +/i18n/pot +.neoconf.json diff --git a/Cargo.lock b/Cargo.lock index 0501e07..52f2772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -27,9 +27,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" @@ -46,7 +46,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -56,7 +56,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -81,6 +81,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" name = "bin" version = "0.1.0" dependencies = [ + "env_logger 0.11.3", "i18n-embed", "library-fluent", ] @@ -131,7 +132,7 @@ dependencies = [ "anyhow", "clap", "doc-comment", - "env_logger", + "env_logger 0.10.1", "gettext", "i18n-build", "i18n-config", @@ -200,6 +201,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -320,6 +336,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.1" @@ -333,6 +359,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -346,7 +385,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", ] [[package]] @@ -402,6 +453,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.29" @@ -568,11 +628,11 @@ dependencies = [ [[package]] name = "i18n-embed" -version = "0.14.1" +version = "0.14.0" dependencies = [ "arc-swap", "doc-comment", - "env_logger", + "env_logger 0.10.1", "fluent", "fluent-langneg", "fluent-syntax", @@ -583,6 +643,7 @@ dependencies = [ "locale_config", "log", "maplit", + "notify", "parking_lot", "pretty_assertions", "rust-embed", @@ -600,7 +661,7 @@ version = "0.8.0" dependencies = [ "dashmap", "doc-comment", - "env_logger", + "env_logger 0.10.1", "find-crate", "fluent", "fluent-syntax", @@ -639,6 +700,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "intl-memoizer" version = "0.5.1" @@ -666,7 +747,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -678,6 +759,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -731,9 +832,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "malloc_buf" @@ -756,6 +857,37 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "objc" version = "0.2.7" @@ -811,7 +943,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -966,7 +1098,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1030,9 +1162,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" dependencies = [ "dashmap", "futures", @@ -1044,9 +1176,9 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" dependencies = [ "proc-macro2", "quote", @@ -1276,6 +1408,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.88" @@ -1388,7 +1526,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.3", ] [[package]] @@ -1397,13 +1544,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +dependencies = [ + "windows_aarch64_gnullvm 0.52.3", + "windows_aarch64_msvc 0.52.3", + "windows_i686_gnu 0.52.3", + "windows_i686_msvc 0.52.3", + "windows_x86_64_gnu 0.52.3", + "windows_x86_64_gnullvm 0.52.3", + "windows_x86_64_msvc 0.52.3", ] [[package]] @@ -1412,42 +1574,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" + [[package]] name = "winnow" version = "0.5.19" diff --git a/i18n-build/CHANGELOG.md b/i18n-build/CHANGELOG.md index 4320b2f..1ebff77 100644 --- a/i18n-build/CHANGELOG.md +++ b/i18n-build/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog for `i18n-build` +## main + ++ Update i18n-embed to version `0.15`. + ## v0.9.0 ### Internal diff --git a/i18n-build/src/lib.rs b/i18n-build/src/lib.rs index 464fe29..fa2876e 100644 --- a/i18n-build/src/lib.rs +++ b/i18n-build/src/lib.rs @@ -74,7 +74,7 @@ mod localize_feature { #[derive(RustEmbed)] #[folder = "i18n/mo"] - struct Translations; + pub struct Translations; lazy_static! { static ref LANGUAGE_LOADER: GettextLanguageLoader = gettext_language_loader!(); diff --git a/i18n-config/src/gettext.rs b/i18n-config/src/gettext.rs index 6f8675a..7e66dcf 100644 --- a/i18n-config/src/gettext.rs +++ b/i18n-config/src/gettext.rs @@ -99,9 +99,10 @@ impl GettextConfig { } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum GettextAddLocation { + #[default] Full, File, Never, @@ -116,9 +117,3 @@ impl GettextAddLocation { } } } - -impl Default for GettextAddLocation { - fn default() -> Self { - GettextAddLocation::Full - } -} diff --git a/i18n-embed-fl/README.md b/i18n-embed-fl/README.md index db5d670..71531a2 100644 --- a/i18n-embed-fl/README.md +++ b/i18n-embed-fl/README.md @@ -47,7 +47,7 @@ struct Localizations; let loader: FluentLanguageLoader = fluent_language_loader!(); loader - .load_languages(&Localizations, &[loader.fallback_language()]) + .load_languages(&Localizations, &[loader.fallback_language().clone()]) .unwrap(); assert_eq!( diff --git a/i18n-embed-fl/src/lib.rs b/i18n-embed-fl/src/lib.rs index c19c50c..c57c408 100644 --- a/i18n-embed-fl/src/lib.rs +++ b/i18n-embed-fl/src/lib.rs @@ -206,7 +206,7 @@ lazy_static::lazy_static! { /// /// let loader: FluentLanguageLoader = fluent_language_loader!(); /// loader -/// .load_languages(&Localizations, &[loader.fallback_language()]) +/// .load_languages(&Localizations, &[loader.fallback_language().clone()]) /// .unwrap(); /// /// // Invoke the fl!() macro to obtain the translated message, and @@ -249,7 +249,7 @@ lazy_static::lazy_static! { /// # struct Localizations; /// # let loader: FluentLanguageLoader = fluent_language_loader!(); /// # loader -/// # .load_languages(&Localizations, &[loader.fallback_language()]) +/// # .load_languages(&Localizations, &[loader.fallback_language().clone()]) /// # .unwrap(); /// let calc_james = || "James".to_string(); /// pretty_assertions::assert_eq!( @@ -293,7 +293,7 @@ lazy_static::lazy_static! { /// # struct Localizations; /// # let loader: FluentLanguageLoader = fluent_language_loader!(); /// # loader -/// # .load_languages(&Localizations, &[loader.fallback_language()]) +/// # .load_languages(&Localizations, &[loader.fallback_language().clone()]) /// # .unwrap(); /// use std::collections::HashMap; /// @@ -323,7 +323,7 @@ lazy_static::lazy_static! { /// # struct Localizations; /// # let loader: FluentLanguageLoader = fluent_language_loader!(); /// # loader -/// # .load_languages(&Localizations, &[loader.fallback_language()]) +/// # .load_languages(&Localizations, &[loader.fallback_language().clone()]) /// # .unwrap(); /// use std::collections::HashMap; /// @@ -378,14 +378,14 @@ pub fn fl(input: TokenStream) -> TokenStream { }); let assets_dir = Path::new(&crate_paths.crate_dir).join(fluent_config.assets_dir); - let assets = FileSystemAssets::new(assets_dir); + let assets = FileSystemAssets::try_new(assets_dir).unwrap(); let fallback_language: LanguageIdentifier = config.fallback_language; let loader = FluentLanguageLoader::new(&domain, fallback_language.clone()); loader - .load_languages(&assets, &[&fallback_language]) + .load_languages(&assets, &[fallback_language.clone()]) .unwrap_or_else(|err| match err { i18n_embed::I18nEmbedError::LanguageNotAvailable(file, language_id) => { if fallback_language != language_id { diff --git a/i18n-embed-fl/tests/fl_macro.rs b/i18n-embed-fl/tests/fl_macro.rs index daa8e8a..41a59ed 100644 --- a/i18n-embed-fl/tests/fl_macro.rs +++ b/i18n-embed-fl/tests/fl_macro.rs @@ -14,7 +14,7 @@ struct Localizations; fn with_args_hashmap() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader - .load_languages(&Localizations, &[loader.fallback_language()]) + .load_languages(&Localizations, &[loader.fallback_language().clone()]) .unwrap(); let mut args: HashMap<&str, &str> = HashMap::new(); @@ -27,7 +27,7 @@ fn with_args_hashmap() { fn with_args_hashmap_expr() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader - .load_languages(&Localizations, &[loader.fallback_language()]) + .load_languages(&Localizations, &[loader.fallback_language().clone()]) .unwrap(); let args_expr = || { @@ -47,7 +47,7 @@ fn with_loader_expr() { let loader = || { let loader: FluentLanguageLoader = fluent_language_loader!(); loader - .load_languages(&Localizations, &[loader.fallback_language()]) + .load_languages(&Localizations, &[loader.fallback_language().clone()]) .unwrap(); loader }; @@ -59,7 +59,7 @@ fn with_loader_expr() { fn with_one_arg_lit() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader - .load_languages(&Localizations, &[loader.fallback_language()]) + .load_languages(&Localizations, &[loader.fallback_language().clone()]) .unwrap(); pretty_assertions::assert_eq!( @@ -72,7 +72,7 @@ fn with_one_arg_lit() { fn with_attr() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader - .load_languages(&Localizations, &[loader.fallback_language()]) + .load_languages(&Localizations, &[loader.fallback_language().clone()]) .unwrap(); pretty_assertions::assert_eq!("Hello, attribute!", fl!(loader, "hello-attr", "text")); @@ -82,7 +82,7 @@ fn with_attr() { fn with_attr_and_args() { let loader: FluentLanguageLoader = fluent_language_loader!(); loader - .load_languages(&Localizations, &[loader.fallback_language()]) + .load_languages(&Localizations, &[loader.fallback_language().clone()]) .unwrap(); pretty_assertions::assert_eq!( diff --git a/i18n-embed/CHANGELOG.md b/i18n-embed/CHANGELOG.md index 278c9ac..92205e0 100644 --- a/i18n-embed/CHANGELOG.md +++ b/i18n-embed/CHANGELOG.md @@ -2,6 +2,32 @@ ## master +### New Features + ++ New `autoreload` crate feature. + + `RustEmbedNotifyAssets` - A wrapper for `rust_embed::RustEmbed` that supports notifications when files have changed on the file system. + + `FileSystemAssets::notify_changes_enabled()` - A new method to enable watching for changes. ++ `AssetsMultiplexor` - A way to multiplex implmentations of [`I18nAssets`] where assets are multiplexed by a priority. + +### Breaking + ++ Modified `I18nAssets` trait. + + Support multiple files referencing the same asset (to allow a heirachy of overrides). + + Support for subscribing to updates to assets. ++ Remove deprecated methods for `LanguageConfig`, Please use `lang(...).get_attr_args(...)` etc instead. + + `LanguageConfig::get_lang()` + + `LanguageConfig::get_lang_args_concrete()` + + `LanguageConfig::get_lang_args_fluent()` + + `LanguageConfig::get_lang_args()` + + `LanguageConfig::get_lang_attr()` + + `LanguageConfig::get_lang_attr_args_concrete()` + + `LanguageConfig::get_lang_attr_args_fluent()` + + `LanguageConfig::get_lang_attr_args()` + + `LanguageConfig::lang()` - Please use `select_languages(...)` instead. ++ Extra bounds on the arguments for `DefaultLocalizer::new()` (`Send + Sync + 'static`) to allow it to be used with `autoreload` feature. ++ `LanguageLoader::load_languages()` now accepts `&[unic_langid::LanguageIdentifier]` instead of `&[&unic_langid::LanguageIdentifier]`. ++ `LanguageLoader:reload()` - Added a new trait method which is used to reload the currently loaded languages. + ### Fixes + Fallback to `std::env::var("CARGO_PKG_NAME")` Fixes [#97](https://github.com/kellpossible/cargo-i18n/issues/97) diff --git a/i18n-embed/Cargo.toml b/i18n-embed/Cargo.toml index 484d414..7b8ef8a 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.14.1" +version = "0.14.0" [package.metadata.docs.rs] all-features = true @@ -28,6 +28,7 @@ intl-memoizer = "0.5" lazy_static = { workspace = true } locale_config = { version = "0.3", optional = true } log = { workspace = true } +notify = { version = "6.1.1", optional = true } parking_lot = { version = "0.12", optional = true } rust-embed = { workspace = true, optional = true } thiserror = { workspace = true } @@ -41,7 +42,7 @@ doc-comment = { workspace = true } env_logger = { workspace = true } maplit = "1.0" pretty_assertions = { workspace = true } -serial_test = "2.0" +serial_test = "3.0" [features] default = ["rust-embed"] @@ -53,3 +54,5 @@ desktop-requester = ["locale_config"] web-sys-requester = ["web-sys"] filesystem-assets = ["walkdir"] + +autoreload = ["notify"] diff --git a/i18n-embed/examples/desktop-bin/Cargo.toml b/i18n-embed/examples/desktop-bin/Cargo.toml index 1d74762..fbce8af 100644 --- a/i18n-embed/examples/desktop-bin/Cargo.toml +++ b/i18n-embed/examples/desktop-bin/Cargo.toml @@ -9,3 +9,4 @@ edition = "2018" [dependencies] library-fluent = { path = "../library-fluent/" } i18n-embed = { workspace = true, features = ["fluent-system", "desktop-requester"]} +env_logger = "0.11.3" diff --git a/i18n-embed/examples/desktop-bin/src/main.rs b/i18n-embed/examples/desktop-bin/src/main.rs index 980b4a8..82ac153 100644 --- a/i18n-embed/examples/desktop-bin/src/main.rs +++ b/i18n-embed/examples/desktop-bin/src/main.rs @@ -1,13 +1,19 @@ -use i18n_embed::DesktopLanguageRequester; +use std::time::Duration; + +use i18n_embed::{DesktopLanguageRequester, Localizer}; use library_fluent::{hello_world, localizer}; fn main() { - let library_localizer = localizer(); + env_logger::init(); + let library_localizer = localizer().with_autoreload().unwrap(); let requested_languages = DesktopLanguageRequester::requested_languages(); if let Err(error) = library_localizer.select(&requested_languages) { eprintln!("Error while loading languages for library_fluent {error}"); } - println!("{}", hello_world()); + loop { + println!("{}", hello_world()); + std::thread::sleep(Duration::from_secs(1)); + } } diff --git a/i18n-embed/examples/library-fluent/Cargo.toml b/i18n-embed/examples/library-fluent/Cargo.toml index b80a801..66ddddc 100644 --- a/i18n-embed/examples/library-fluent/Cargo.toml +++ b/i18n-embed/examples/library-fluent/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -i18n-embed = { workspace = true, features = ["fluent-system"] } +i18n-embed = { workspace = true, features = ["fluent-system", "autoreload"] } i18n-embed-fl = { workspace = true } once_cell = { workspace = true } rust-embed = { workspace = true } diff --git a/i18n-embed/examples/library-fluent/i18n/en/library_fluent.ftl b/i18n-embed/examples/library-fluent/i18n/en/library_fluent.ftl index 3aad3b9..09e3bda 100644 --- a/i18n-embed/examples/library-fluent/i18n/en/library_fluent.ftl +++ b/i18n-embed/examples/library-fluent/i18n/en/library_fluent.ftl @@ -1 +1 @@ -hello-world = Hello World! \ No newline at end of file +hello-world = Hello World! diff --git a/i18n-embed/examples/library-fluent/src/lib.rs b/i18n-embed/examples/library-fluent/src/lib.rs index dab5fc0..fa5b049 100644 --- a/i18n-embed/examples/library-fluent/src/lib.rs +++ b/i18n-embed/examples/library-fluent/src/lib.rs @@ -1,6 +1,6 @@ use i18n_embed::{ fluent::{fluent_language_loader, FluentLanguageLoader}, - DefaultLocalizer, LanguageLoader, Localizer, + DefaultLocalizer, LanguageLoader, RustEmbedNotifyAssets, }; use i18n_embed_fl::fl; use once_cell::sync::Lazy; @@ -8,7 +8,11 @@ use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "i18n/"] -struct Localizations; +pub struct LocalizationsEmbed; + +pub static LOCALIZATIONS: Lazy> = Lazy::new(|| { + RustEmbedNotifyAssets::new(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("i18n/")) +}); static LANGUAGE_LOADER: Lazy = Lazy::new(|| { let loader: FluentLanguageLoader = fluent_language_loader!(); @@ -16,7 +20,7 @@ static LANGUAGE_LOADER: Lazy = Lazy::new(|| { // 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) + .load_fallback_language(&*LOCALIZATIONS) .expect("Error while loading fallback language"); loader @@ -39,6 +43,6 @@ pub fn hello_world() -> String { } // Get the `Localizer` to be used for localizing this library. -pub fn localizer() -> Box { - Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) +pub fn localizer() -> DefaultLocalizer<'static> { + DefaultLocalizer::new(&*LANGUAGE_LOADER, &*LOCALIZATIONS) } diff --git a/i18n-embed/examples/library-fluent/tests/with_localizer.rs b/i18n-embed/examples/library-fluent/tests/with_localizer.rs index 830a8d6..9ceb721 100644 --- a/i18n-embed/examples/library-fluent/tests/with_localizer.rs +++ b/i18n-embed/examples/library-fluent/tests/with_localizer.rs @@ -1,3 +1,4 @@ +use i18n_embed::Localizer; use library_fluent::{hello_world, localizer}; use std::collections::HashSet; @@ -9,7 +10,7 @@ use std::iter::FromIterator; fn test_available_languages() { let localizer = localizer(); assert_eq!( - &localizer.language_loader().fallback_language().to_string(), + &localizer.language_loader.fallback_language().to_string(), "en" ); diff --git a/i18n-embed/src/assets.rs b/i18n-embed/src/assets.rs index ad4cbb0..6a91ce3 100644 --- a/i18n-embed/src/assets.rs +++ b/i18n-embed/src/assets.rs @@ -1,27 +1,110 @@ use std::borrow::Cow; +use rust_embed::RustEmbed; + +use crate::I18nEmbedError; + /// A trait to handle the retrieval of localization assets. pub trait I18nAssets { - /// Get a localization asset (returns `None` if the asset does not - /// exist, or unable to obtain the asset due to a non-critical - /// error). - fn get_file(&self, file_path: &str) -> Option>; - /// Get an iterator over the filenames of the localization assets. - fn filenames_iter(&self) -> Box>; + /// Get localization asset files that correspond to the specified `file_path`. Returns an empty + /// [`Vec`] if the asset does not exist, or unable to obtain the asset due to a non-critical + /// error. + fn get_files(&self, file_path: &str) -> Vec>; + /// Get an iterator over the file paths of the localization assets. There may be duplicates + /// where multiple files exist for the same file path. + fn filenames_iter(&self) -> Box + '_>; + /// A method to allow users of this trait to subscribe to change events, and reload assets when + /// they have changed. The subscription will be cancelled when the returned [`Watcher`] is + /// dropped. + /// + /// **NOTE**: The implementation of this method is optional, don't rely on it functioning for all + /// implementations. + fn subscribe_changed( + &self, + #[allow(unused_variables)] changed: std::sync::Arc, + ) -> Result, I18nEmbedError> { + Ok(Box::new(())) + } } -#[cfg(feature = "rust-embed")] +impl Watcher for () {} + impl I18nAssets for T where - T: rust_embed::RustEmbed + 'static, + T: RustEmbed, { - fn get_file(&self, file_path: &str) -> Option> { - Self::get(file_path).map(|file| file.data) + fn get_files(&self, file_path: &str) -> Vec> { + Self::get(file_path) + .map(|file| file.data) + .into_iter() + .collect() } fn filenames_iter(&self) -> Box> { Box::new(Self::iter().map(|filename| filename.to_string())) } + + #[allow(unused_variables)] + fn subscribe_changed( + &self, + changed: std::sync::Arc, + ) -> Result, I18nEmbedError> { + Ok(Box::new(())) + } +} + +/// A wrapper for [`rust_embed::RustEmbed`] that supports notifications when files have changed on +/// the file system. A wrapper is required to provide `base_dir` as this is unavailable in the type +/// derived by the [`rust_embed::RustEmbed`] macro. +/// +/// ⚠️ *This type requires the following crate features to be activated: `autoreload`.* +#[cfg(feature = "autoreload")] +#[derive(Debug)] +pub struct RustEmbedNotifyAssets { + base_dir: std::path::PathBuf, + embed: core::marker::PhantomData, +} + +#[cfg(feature = "autoreload")] +impl RustEmbedNotifyAssets { + /// Construct a new [`RustEmbedNotifyAssets`]. + pub fn new(base_dir: impl Into) -> Self { + Self { + base_dir: base_dir.into(), + embed: core::marker::PhantomData, + } + } +} + +#[cfg(feature = "autoreload")] +impl I18nAssets for RustEmbedNotifyAssets +where + T: RustEmbed, +{ + fn get_files(&self, file_path: &str) -> Vec> { + T::get(file_path) + .map(|file| file.data) + .into_iter() + .collect() + } + + fn filenames_iter(&self) -> Box> { + Box::new(T::iter().map(|filename| filename.to_string())) + } + + fn subscribe_changed( + &self, + changed: std::sync::Arc, + ) -> Result, I18nEmbedError> { + let base_dir = &self.base_dir; + if base_dir.is_dir() { + log::debug!("Watching for changed files in {:?}", self.base_dir); + notify_watcher(base_dir, changed).map_err(Into::into) + } else { + log::debug!("base_dir {base_dir:?} does not yet exist, unable to watch for changes"); + Ok(Box::new(())) + } + } } /// An [I18nAssets] implementation which pulls assets from the OS @@ -30,46 +113,125 @@ where #[derive(Debug)] pub struct FileSystemAssets { base_dir: std::path::PathBuf, + #[cfg(feature = "autoreload")] + notify_changes_enabled: bool, } #[cfg(feature = "filesystem-assets")] impl FileSystemAssets { /// Create a new `FileSystemAssets` instance, all files will be - /// read from within the specified base directory. Will panic if - /// the specified `base_dir` does not exist, or is not a valid - /// directory. - pub fn new>(base_dir: P) -> Self { + /// read from within the specified base directory. + pub fn try_new>(base_dir: P) -> Result { let base_dir = base_dir.into(); if !base_dir.exists() { - panic!("specified `base_dir` ({:?}) does not exist", base_dir); + return Err(I18nEmbedError::DirectoryDoesNotExist(base_dir)); } if !base_dir.is_dir() { - panic!("specified `base_dir` ({:?}) is not a directory", base_dir); + return Err(I18nEmbedError::PathIsNotDirectory(base_dir)); } - Self { base_dir } + Ok(Self { + base_dir, + #[cfg(feature = "autoreload")] + notify_changes_enabled: false, + }) + } + + /// Enable the notification of changes in the [`I18nAssets`] implementation. + #[cfg(feature = "autoreload")] + pub fn notify_changes_enabled(mut self, enabled: bool) -> Self { + self.notify_changes_enabled = enabled; + self } } +/// An error that occurs during notification of changes when the `autoreload feature is enabled.` +/// +/// ⚠️ *This type requires the following crate features to be activated: `filesystem-assets`.* +#[cfg(feature = "autoreload")] +#[derive(Debug)] +pub struct NotifyError(notify::Error); + +#[cfg(feature = "autoreload")] +impl From for NotifyError { + fn from(value: notify::Error) -> Self { + Self(value) + } +} + +#[cfg(feature = "autoreload")] +impl From for I18nEmbedError { + fn from(value: notify::Error) -> Self { + Self::Notify(value.into()) + } +} + +#[cfg(feature = "autoreload")] +impl std::fmt::Display for NotifyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "autoreload")] +impl std::error::Error for NotifyError {} + +#[cfg(feature = "autoreload")] +fn notify_watcher( + base_dir: &std::path::Path, + changed: std::sync::Arc, +) -> notify::Result> { + let mut watcher = notify::recommended_watcher(move |event_result| { + let event: notify::Event = match event_result { + Ok(event) => event, + Err(error) => { + log::error!("{error}"); + return; + } + }; + match event.kind { + notify::EventKind::Any + | notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) + | notify::EventKind::Other => changed(), + _ => {} + } + })?; + + notify::Watcher::watch(&mut watcher, base_dir, notify::RecursiveMode::Recursive)?; + + Ok(Box::new(watcher)) +} + +/// An entity that watches for changes to localization resources. +/// +/// NOTE: Currently we rely in the implicit [`Drop`] implementation to remove file system watches, +/// in the future ther may be new methods added to this trait. +pub trait Watcher {} + +#[cfg(feature = "autoreload")] +impl Watcher for notify::RecommendedWatcher {} + #[cfg(feature = "filesystem-assets")] impl I18nAssets for FileSystemAssets { - fn get_file(&self, file_path: &str) -> Option> { + fn get_files(&self, file_path: &str) -> Vec> { let full_path = self.base_dir.join(file_path); if !(full_path.is_file() && full_path.exists()) { - return None; + return Vec::new(); } match std::fs::read(full_path) { - Ok(contents) => Some(Cow::from(contents)), + Ok(contents) => vec![Cow::from(contents)], Err(e) => { log::error!( target: "i18n_embed::assets", "Unexpected error while reading localization asset file: {}", e); - None + Vec::new() } } } @@ -85,9 +247,9 @@ impl I18nAssets for FileSystemAssets { Some(filename) => Some(filename.to_string()), None => { log::error!( - target: "i18n_embed::assets", - "Filename {:?} is not valid UTF-8.", - f.file_name()); + target: "i18n_embed::assets", + "Filename {:?} is not valid UTF-8.", + f.file_name()); None } } @@ -97,12 +259,89 @@ impl I18nAssets for FileSystemAssets { } Err(err) => { log::error!( - target: "i18n_embed::assets", - "Unexpected error while gathering localization asset filenames: {}", - err); + target: "i18n_embed::assets", + "Unexpected error while gathering localization asset filenames: {}", + err); None } }), ) } + + /// See [`FileSystemAssets::notify_changes_enabled`] to enable this implementation. + /// ⚠️ *This method requires the following crate features to be activated: `autoreload`.* + #[cfg(feature = "autoreload")] + fn subscribe_changed( + &self, + changed: std::sync::Arc, + ) -> Result, I18nEmbedError> { + if self.notify_changes_enabled { + notify_watcher(&self.base_dir, changed).map_err(Into::into) + } else { + Ok(Box::new(())) + } + } +} + +/// A way to multiplex implmentations of [`I18nAssets`]. +pub struct AssetsMultiplexor { + /// Assets that are multiplexed, ordered from most to least priority. + assets: Vec>, +} + +impl std::fmt::Debug for AssetsMultiplexor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AssetsMultiplexor") + .field( + "assets", + &self.assets.iter().map(|_| "").collect::>(), + ) + .finish() + } +} + +impl AssetsMultiplexor { + /// Construct a new [`AssetsMultiplexor`]. `assets` are specified in order of priority of + /// processing for the [`crate::LanguageLoader`]. + pub fn new( + assets: impl IntoIterator>, + ) -> Self { + Self { + assets: assets.into_iter().collect(), + } + } +} + +#[allow(dead_code)] // We rely on the Drop implementation of the Watcher to remove the file system watch. +struct Watchers(Vec>); + +impl Watcher for Watchers {} + +impl I18nAssets for AssetsMultiplexor { + fn get_files(&self, file_path: &str) -> Vec> { + self.assets + .iter() + .flat_map(|assets| assets.get_files(file_path)) + .collect() + } + + fn filenames_iter(&self) -> Box + '_> { + Box::new( + self.assets + .iter() + .flat_map(|assets| assets.filenames_iter()), + ) + } + + fn subscribe_changed( + &self, + changed: std::sync::Arc, + ) -> Result, I18nEmbedError> { + let watchers: Vec<_> = self + .assets + .iter() + .map(|assets| assets.subscribe_changed(changed.clone())) + .collect::>()?; + Ok(Box::new(Watchers(watchers))) + } } diff --git a/i18n-embed/src/fluent.rs b/i18n-embed/src/fluent.rs index 480f9c0..ac1f2fb 100644 --- a/i18n-embed/src/fluent.rs +++ b/i18n-embed/src/fluent.rs @@ -51,7 +51,10 @@ impl Debug for LanguageBundle { #[derive(Debug)] struct LanguageConfig { - language_bundles: Vec, + /// Storage for language localization resources. Outer `Vec` is per language (as specified in + /// [`LanguageConfig::language_map`]), inner Vec is for storage of multiple bundles per + /// language, in order of priority (highest to lowest). + language_bundles: Vec>, /// This maps a `LanguageIdentifier` to the index inside the /// `language_bundles` vector. language_map: HashMap, @@ -155,6 +158,7 @@ impl FluentLanguageLoader { .indices .iter() .map(|&idx| &language_config.language_bundles[idx]) + .flat_map(|language_bundles| language_bundles.iter()) .find_map(|language_bundle| language_bundle .bundle .get_message(message_id) @@ -224,7 +228,9 @@ impl FluentLanguageLoader { let language_config = inner.language_config.read(); let current_language = self.current_language_impl(&inner); - language_config.language_bundles.iter().find_map(|language_bundle| { + language_config.language_bundles.iter() + .flat_map(|language_bundles| language_bundles.iter()) + .find_map(|language_bundle| { language_bundle .bundle .get_message(message_id) @@ -278,150 +284,17 @@ impl FluentLanguageLoader { ) } - /// Get a localized message referenced by the `message_id`. - #[deprecated( - since = "0.13.6", - note = "Please use `select_languages(...).get(...)` instead" - )] - pub fn get_lang(&self, lang: &[&LanguageIdentifier], message_id: &str) -> String { - self.select_languages(lang).get(message_id) - } - - /// A non-generic version of [FluentLanguageLoader::get_lang_args()]. - #[deprecated( - since = "0.13.6", - note = "Please use `select_languages(...).get_args_concrete(...)` instead" - )] - pub fn get_lang_args_concrete<'source>( - &self, - lang: &[&LanguageIdentifier], - message_id: &str, - args: HashMap<&'source str, FluentValue<'source>>, - ) -> String { - self.select_languages(lang) - .get_args_concrete(message_id, args) - } - - /// A non-generic version of [FluentLanguageLoader::get_lang_args()] - /// accepting [FluentArgs] instead of a [HashMap]. - #[deprecated( - since = "0.13.6", - note = "Please use `select_languages(...).get_args_fluent(...)` instead" - )] - pub fn get_lang_args_fluent<'args>( - &self, - lang: &[&LanguageIdentifier], - message_id: &str, - args: Option<&'args FluentArgs<'args>>, - ) -> String { - self.select_languages(lang) - .get_args_fluent(message_id, args) - } - - /// Get a localized message for the given language identifiers, referenced - /// by the `message_id` and formatted with the specified `args`. - #[deprecated( - since = "0.13.6", - note = "Please use `select_languages(...).get_args(...)` instead" - )] - pub fn get_lang_args<'a, S, V>( - &self, - lang: &[&LanguageIdentifier], - id: &str, - args: HashMap, - ) -> String - where - S: Into> + Clone, - V: Into> + Clone, - { - self.select_languages(lang).get_args(id, args) - } - - /// Get a localized attribute referenced by the `Message_id` and `attribute_id`. - #[deprecated( - since = "0.13.6", - note = "Please use `select_languages(...).get_attr(...)` instead" - )] - pub fn get_lang_attr( - &self, - lang: &[&LanguageIdentifier], - message_id: &str, - attribute_id: &str, - ) -> String { - self.select_languages(lang) - .get_attr(message_id, attribute_id) - } - - /// A non-generic version of [FluentLanguageLoader::get_lang_attr_args()]. - #[deprecated( - since = "0.13.6", - note = "Please use `select_languages(...).get_attr_args_concrete(...)` instead" - )] - pub fn get_lang_attr_args_concrete<'source>( - &self, - lang: &[&LanguageIdentifier], - message_id: &str, - attribute_id: &str, - args: HashMap<&'source str, FluentValue<'source>>, - ) -> String { - self.select_languages(lang) - .get_attr_args_concrete(message_id, attribute_id, args) - } - - /// A non-generic version of [FluentLanguageLoader::get_lang_attr_args()] - /// accepting [FluentArgs] instead of a [HashMap]. - #[deprecated( - since = "0.13.6", - note = "Please use `select_languages(...).get_attr_args_fluent(...)` instead" - )] - pub fn get_lang_attr_args_fluent<'args>( - &self, - lang: &[&LanguageIdentifier], - message_id: &str, - attribute_id: &str, - args: Option<&'args FluentArgs<'args>>, - ) -> String { - self.select_languages(lang) - .get_attr_args_fluent(message_id, attribute_id, args) - } - - /// Get a localized attribute referenced by the `message_id` and `attribute_id` - /// and formatted with the `args`. - #[deprecated( - since = "0.13.6", - note = "Please use `lang(...).get_attr_args(...)` instead" - )] - pub fn get_lang_attr_args<'a, S, V>( - &self, - lang: &[&LanguageIdentifier], - message_id: &str, - attribute_id: &str, - args: HashMap, - ) -> String - where - S: Into> + Clone, - V: Into> + Clone, - { - self.select_languages(lang) - .get_attr_args(message_id, attribute_id, args) - } - /// available in any of the languages currently loaded (including /// the fallback language). pub fn has(&self, message_id: &str) -> bool { - let mut has_message = false; - self.inner .load() .language_config .read() .language_bundles .iter() - .for_each(|language_bundle| { - has_message |= language_bundle.bundle.has_message(message_id) - }); - - has_message + .flat_map(|language_bundles| language_bundles.iter()) + .any(|language_bundle| language_bundle.bundle.has_message(message_id)) } /// Determines if an attribute associated with the specified `message_id` @@ -439,6 +312,7 @@ impl FluentLanguageLoader { .read() .language_bundles .iter() + .flat_map(|bundles| bundles.iter()) .find_map(|bundle| { bundle .bundle @@ -463,6 +337,7 @@ impl FluentLanguageLoader { .read() .language_bundles .iter() + .flat_map(|language_bundles| language_bundles.iter()) .find_map(|language_bundle| language_bundle.bundle.get_message(message_id)) .map(closure) } @@ -481,6 +356,7 @@ impl FluentLanguageLoader { let mut iter = config_lock .language_bundles .iter() + .flat_map(|language_bundles| language_bundles.iter()) .filter(|language_bundle| &language_bundle.language == language) .flat_map(|language_bundle| { language_bundle @@ -523,20 +399,12 @@ impl FluentLanguageLoader { .language_config .write() .language_bundles - .as_mut_slice() + .iter_mut() + .flat_map(|bundles| bundles.iter_mut()) { f(&mut bundle.bundle); } } - /// Create a new loader with a subset of currently loaded languages. - /// This is a rather cheap operation and does not require any - /// extensive copy operations. Cheap does not mean free so you - /// should not call this message repeatedly in order to translate - /// multiple strings for the same language. - #[deprecated(since = "0.13.7", note = "Please use `select_languages(...)` instead")] - pub fn lang>(&self, languages: &[LI]) -> FluentLanguageLoader { - self.select_languages(languages) - } /// Create a new loader with a subset of currently loaded languages. /// This is a rather cheap operation and does not require any @@ -624,29 +492,35 @@ impl LanguageLoader for FluentLanguageLoader { /// first in the `language_ids` slice. You can use /// [select()](super::select()) to determine which fallbacks are /// actually available for an arbitrary slice of preferences. - fn load_languages( + #[allow(single_use_lifetimes)] + fn load_languages<'a>( &self, i18n_assets: &dyn I18nAssets, - language_ids: &[&unic_langid::LanguageIdentifier], + language_ids: &[unic_langid::LanguageIdentifier], ) -> Result<(), I18nEmbedError> { - if language_ids.is_empty() { + let mut language_ids = language_ids.iter().peekable(); + if language_ids.peek().is_none() { return Err(I18nEmbedError::RequestedLanguagesEmpty); } // The languages to load - let mut load_language_ids: Vec = - language_ids.iter().map(|id| (**id).clone()).collect(); + let language_ids: Vec = + language_ids.map(|id| (*id).clone()).collect(); + let mut load_language_ids: Vec = language_ids.clone(); if !load_language_ids.contains(&self.fallback_language) { load_language_ids.push(self.fallback_language.clone()); } + let language_bundles: Vec> = load_language_ids.iter().map(|language| { + let (path, files) = self.language_files(language, i18n_assets); - let mut language_bundles = Vec::with_capacity(language_ids.len()); - - for language in &load_language_ids { - let (path, file) = self.language_file(language, i18n_assets); - - if let Some(file) = file { + if files.is_empty() { + log::debug!(target:"i18n_embed::fluent", "Unable to find language file: \"{0}\" for language: \"{1}\"", path, language); + if language == &self.fallback_language { + return Err(I18nEmbedError::LanguageNotAvailable(path, language.clone())); + } + } + files.into_iter().map(|file| { log::debug!(target:"i18n_embed::fluent", "Loaded language file: \"{0}\" for language: \"{1}\"", path, language); let file_string = String::from_utf8(file.to_vec()) @@ -665,27 +539,25 @@ impl LanguageLoader for FluentLanguageLoader { } }; - let language_bundle = LanguageBundle::new(language.clone(), resource); - - language_bundles.push(language_bundle); - } else { - log::debug!(target:"i18n_embed::fluent", "Unable to find language file: \"{0}\" for language: \"{1}\"", path, language); - if language == &self.fallback_language { - return Err(I18nEmbedError::LanguageNotAvailable(path, language.clone())); - } - } - } + Ok(LanguageBundle::new(language.clone(), resource)) + }).collect::, I18nEmbedError>>() + }).collect::>()?; self.inner.swap(Arc::new(FluentLanguageLoaderInner { current_languages: CurrentLanguages { - languages: language_ids.iter().map(|&lang| lang.to_owned()).collect(), + languages: language_ids, indices: (0..load_language_ids.len()).collect(), }, language_config: Arc::new(RwLock::new(LanguageConfig { language_map: language_bundles .iter() .enumerate() - .map(|(i, language_bundle)| (language_bundle.language.clone(), i)) + .map(|(i, language_bundles)| { + ( + language_bundles.first().expect("Expect there to be at least bundle in a set of bundles per language").language.clone(), + i + ) + }) .collect(), language_bundles, })), @@ -693,6 +565,13 @@ impl LanguageLoader for FluentLanguageLoader { Ok(()) } + + fn reload(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> { + self.load_languages( + i18n_assets, + &self.inner.load().current_languages.languages.clone(), + ) + } } fn hash_map_to_fluent_args<'args, K, V>(map: HashMap) -> Option> diff --git a/i18n-embed/src/gettext.rs b/i18n-embed/src/gettext.rs index 1ff0ef0..5d94380 100644 --- a/i18n-embed/src/gettext.rs +++ b/i18n-embed/src/gettext.rs @@ -81,23 +81,31 @@ impl LanguageLoader for GettextLanguageLoader { /// **Note:** Gettext doesn't support loading multiple languages /// as multiple fallbacks. We only load the first of the requested /// languages, and the fallback is the src language. + #[allow(single_use_lifetimes)] fn load_languages( &self, i18n_assets: &dyn I18nAssets, - language_ids: &[&unic_langid::LanguageIdentifier], + language_ids: &[unic_langid::LanguageIdentifier], ) -> Result<(), I18nEmbedError> { - let language_id = *language_ids - .get(0) + let language_id = language_ids + .iter() + .next() .ok_or(I18nEmbedError::RequestedLanguagesEmpty)?; if language_id == self.fallback_language() { self.load_src_language(); return Ok(()); } - - let (_path, file) = match self.language_file(language_id, i18n_assets) { - (path, Some(f)) => (path, f), - (path, None) => { + let (path, files) = self.language_files(language_id, i18n_assets); + let file = match files.as_slice() { + [first_file] => first_file, + [first_file, ..] => { + log::warn!( + "Gettext system does not yet support merging language files for {path:?}" + ); + first_file + } + [] => { log::error!( target:"i18n_embed::gettext", "{} Setting current_language to fallback locale: \"{}\".", @@ -108,10 +116,14 @@ impl LanguageLoader for GettextLanguageLoader { } }; - let catalog = gettext_system::Catalog::parse(&*file).expect("could not parse the catalog"); + let catalog = gettext_system::Catalog::parse(&**file).expect("could not parse the catalog"); tr::internal::set_translator(self.module, catalog); *(self.current_language.write()) = language_id.clone(); Ok(()) } + + fn reload(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> { + self.load_languages(i18n_assets, &[self.current_language()]) + } } diff --git a/i18n-embed/src/lib.rs b/i18n-embed/src/lib.rs index 3946f24..37e836d 100644 --- a/i18n-embed/src/lib.rs +++ b/i18n-embed/src/lib.rs @@ -371,9 +371,9 @@ //! } //! //! /// Get the `Localizer` to be used for localizing this library, -//! /// using the provided embeddes source of language files `embed`. +//! /// using the provided embedded source of language files `embed`. //! # #[allow(unused)] -//! pub fn localizer(embed: &dyn I18nAssets) -> Arc { +//! pub fn localizer<'a>(embed: &'a (dyn I18nAssets + Send + Sync + 'static)) -> Arc { //! Arc::new(DefaultLocalizer::new( //! &*LANGUAGE_LOADER, //! embed @@ -475,6 +475,15 @@ pub enum I18nEmbedError { #[cfg(feature = "gettext-system")] #[error(transparent)] Gettext(#[from] gettext_system::Error), + #[cfg(feature = "autoreload")] + #[error(transparent)] + Notify(#[from] assets::NotifyError), + #[cfg(feature = "filesystem-assets")] + #[error("The directory {0:?} does not exist")] + DirectoryDoesNotExist(std::path::PathBuf), + #[cfg(feature = "filesystem-assets")] + #[error("The path {0:?} is not a directory")] + PathIsNotDirectory(std::path::PathBuf), } fn error_vec_to_string(errors: &[I18nEmbedError]) -> String { @@ -515,10 +524,11 @@ pub trait Localizer { /// A simple default implemenation of the [Localizer](Localizer) trait. pub struct DefaultLocalizer<'a> { - /// The [LanguageLoader] used by this localizer. - pub language_loader: &'a dyn LanguageLoader, /// The source of assets used by this localizer. - pub i18n_assets: &'a dyn I18nAssets, + pub i18n_assets: &'a (dyn I18nAssets + Send + Sync + 'static), + /// The [LanguageLoader] used by this localizer. + pub language_loader: &'a (dyn LanguageLoader + Send + Sync + 'static), + watchers: Vec>, } impl Debug for DefaultLocalizer<'_> { @@ -533,27 +543,45 @@ impl Debug for DefaultLocalizer<'_> { #[allow(single_use_lifetimes)] impl<'a> Localizer for DefaultLocalizer<'a> { - fn language_loader(&self) -> &'_ dyn LanguageLoader { - self.language_loader - } fn i18n_assets(&self) -> &'_ dyn I18nAssets { self.i18n_assets } + fn language_loader(&self) -> &'_ dyn LanguageLoader { + self.language_loader + } } impl<'a> DefaultLocalizer<'a> { /// Create a new [DefaultLocalizer](DefaultLocalizer). pub fn new( - language_loader: &'a dyn LanguageLoader, - i18n_assets: &'a dyn I18nAssets, - ) -> DefaultLocalizer<'a> { - DefaultLocalizer { - language_loader, + language_loader: &'a (dyn LanguageLoader + Send + Sync + 'static), + i18n_assets: &'a (dyn I18nAssets + Send + Sync + 'static), + ) -> Self { + Self { i18n_assets, + language_loader, + watchers: Vec::new(), } } } +impl DefaultLocalizer<'static> { + /// Create a new [DefaultLocalizer](DefaultLocalizer). + pub fn with_autoreload(mut self) -> Result { + let assets = self.i18n_assets; + let loader = self.language_loader; + let watcher = self + .i18n_assets + .subscribe_changed(std::sync::Arc::new(move || { + if let Err(error) = loader.reload(assets) { + log::error!("Error autoreloading assets: {error:?}") + } + }))?; + self.watchers.push(watcher); + Ok(self) + } +} + /// Select the most suitable available language in order of preference /// by `requested_languages`, and load it using the provided /// [LanguageLoader] from the languages available in [I18nAssets]. @@ -567,7 +595,7 @@ pub fn select( i18n_assets: &dyn I18nAssets, requested_languages: &[unic_langid::LanguageIdentifier], ) -> Result, I18nEmbedError> { - debug!( + log::info!( "Selecting translations for domain \"{0}\"", language_loader.domain() ); @@ -583,15 +611,17 @@ pub fn select( NegotiationStrategy::Filtering, ); - debug!("Requested Languages: {:?}", requested_languages); - debug!("Available Languages: {:?}", available_languages); - debug!("Supported Languages: {:?}", supported_languages); + log::debug!("Requested Languages: {:?}", requested_languages); + log::debug!("Available Languages: {:?}", available_languages); + log::debug!("Supported Languages: {:?}", supported_languages); + let supported_languages: Vec = + supported_languages.into_iter().cloned().collect(); if !supported_languages.is_empty() { - language_loader.load_languages(i18n_assets, supported_languages.as_slice())?; + language_loader.load_languages(i18n_assets, &supported_languages)?; } - Ok(supported_languages.into_iter().cloned().collect()) + Ok(supported_languages) } /// A language resource file, and its associated `language`. @@ -615,20 +645,21 @@ pub trait LanguageLoader { fn domain(&self) -> &str; /// The language file name to use for this loader's domain. fn language_file_name(&self) -> String; - /// The computed path to the language file, and `Cow` of the file - /// itself if it exists. - fn language_file<'a>( + /// The computed path to the language files, and data contained within the files at that path + /// itself if they exist. There can be multiple files at a given path, in order of preference + /// from high to low. + fn language_files<'a>( &self, language_id: &unic_langid::LanguageIdentifier, i18n_assets: &'a dyn I18nAssets, - ) -> (String, Option>) { + ) -> (String, Vec>) { let language_id_string = language_id.to_string(); let file_path = format!("{}/{}", language_id_string, self.language_file_name()); log::debug!("Attempting to load language file: \"{}\"", &file_path); - let file = i18n_assets.get_file(file_path.as_ref()); - (file_path, file) + let files = i18n_assets.get_files(file_path.as_ref()); + (file_path, files) } /// Calculate the languages which are available to be loaded. @@ -643,7 +674,7 @@ pub trait LanguageLoader { let components: Vec> = path.components().collect(); - let locale: Option = match components.get(0) { + let locale: Option = match components.first() { Some(Component::Normal(s)) => { Some(s.to_str().expect("path should be valid utf-8").to_string()) } @@ -701,12 +732,15 @@ pub trait LanguageLoader { /// Load all available languages with [`LanguageLoader::load_languages()`]. fn load_available_languages(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> { let available_languages = self.available_languages(i18n_assets)?; - self.load_languages(i18n_assets, &available_languages.iter().collect::>()) + self.load_languages(i18n_assets, &available_languages) } /// Get the language which is currently loaded for this loader. fn current_language(&self) -> unic_langid::LanguageIdentifier; + /// Reload the currently loaded languages. + fn reload(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError>; + /// 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 @@ -716,12 +750,12 @@ pub trait LanguageLoader { fn load_languages( &self, i18n_assets: &dyn I18nAssets, - language_ids: &[&unic_langid::LanguageIdentifier], + 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()]) + self.load_languages(i18n_assets, &[self.fallback_language().clone()]) } } diff --git a/i18n-embed/src/requester.rs b/i18n-embed/src/requester.rs index 94e6e35..b30d880 100644 --- a/i18n-embed/src/requester.rs +++ b/i18n-embed/src/requester.rs @@ -197,7 +197,7 @@ impl<'a> LanguageRequesterImpl<'a> { impl Default for LanguageRequesterImpl<'_> { fn default() -> Self { - Self::new() + LanguageRequesterImpl::new() } } @@ -235,7 +235,7 @@ pub struct DesktopLanguageRequester<'a> { #[cfg(feature = "desktop-requester")] impl<'a> LanguageRequester<'a> for DesktopLanguageRequester<'a> { fn requested_languages(&self) -> Vec { - Self::requested_languages() + DesktopLanguageRequester::requested_languages() } fn add_listener(&mut self, listener: Weak) { diff --git a/i18n-embed/tests/loader.rs b/i18n-embed/tests/loader.rs index 5ed7c6f..f863f23 100644 --- a/i18n-embed/tests/loader.rs +++ b/i18n-embed/tests/loader.rs @@ -20,7 +20,7 @@ mod fluent { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_us]).unwrap(); + loader.load_languages(&Localizations, &[en_us]).unwrap(); pretty_assertions::assert_eq!("Hello World Localization!", loader.get("hello-world")); } @@ -31,7 +31,7 @@ mod fluent { let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_gb]).unwrap(); + loader.load_languages(&Localizations, &[en_gb]).unwrap(); pretty_assertions::assert_eq!("Hello World Localisation!", loader.get("hello-world")); pretty_assertions::assert_eq!("only US", loader.get("only-us")); } @@ -44,9 +44,7 @@ mod fluent { let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader - .load_languages(&Localizations, &[&ru, &en_gb]) - .unwrap(); + loader.load_languages(&Localizations, &[ru, en_gb]).unwrap(); pretty_assertions::assert_eq!("Привет Мир Локализация!", loader.get("hello-world")); pretty_assertions::assert_eq!("only GB", loader.get("only-gb")); pretty_assertions::assert_eq!("only US", loader.get("only-us")); @@ -60,7 +58,7 @@ mod fluent { let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&ru]).unwrap(); + loader.load_languages(&Localizations, &[ru]).unwrap(); let args = maplit::hashmap! { "userName" => "Tanya" @@ -76,7 +74,7 @@ mod fluent { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_us]).unwrap(); + loader.load_languages(&Localizations, &[en_us]).unwrap(); pretty_assertions::assert_eq!("World (US version)!", loader.get_attr("with-attr", "attr")); } @@ -86,7 +84,7 @@ mod fluent { let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_gb]).unwrap(); + loader.load_languages(&Localizations, &[en_gb]).unwrap(); let args = maplit::hashmap! { "name" => "Joe Doe" }; @@ -103,7 +101,7 @@ mod fluent { let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&ru]).unwrap(); + loader.load_languages(&Localizations, &[ru]).unwrap(); assert!(loader.has("only-ru-args")); assert!(loader.has("only-us")); @@ -115,7 +113,7 @@ mod fluent { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_us]).unwrap(); + loader.load_languages(&Localizations, &[en_us]).unwrap(); loader.set_use_isolating(false); let args = maplit::hashmap! { "thing" => "thing" @@ -129,7 +127,7 @@ mod fluent { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_us]).unwrap(); + loader.load_languages(&Localizations, &[en_us]).unwrap(); let args = maplit::hashmap! { "thing" => "thing" }; @@ -142,7 +140,7 @@ mod fluent { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_us]).unwrap(); + loader.load_languages(&Localizations, &[en_us]).unwrap(); let msg = loader.get("multi-line"); assert_eq!( @@ -158,7 +156,7 @@ mod fluent { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", ru.clone()); - loader.load_languages(&Localizations, &[&ru]).unwrap(); + loader.load_languages(&Localizations, &[ru]).unwrap(); let msg = loader.get("multi-line"); assert_eq!( @@ -174,7 +172,7 @@ mod fluent { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); - loader.load_languages(&Localizations, &[&en_us]).unwrap(); + loader.load_languages(&Localizations, &[en_us]).unwrap(); let args = maplit::hashmap! { "argOne" => "1", @@ -197,7 +195,7 @@ mod fluent { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", ru.clone()); - loader.load_languages(&Localizations, &[&ru]).unwrap(); + loader.load_languages(&Localizations, &[ru]).unwrap(); let args = maplit::hashmap! { "argOne" => "1", @@ -224,13 +222,13 @@ mod fluent { let loader = FluentLanguageLoader::new("test", en_us); loader - .load_languages(&Localizations, &[&ru, &en_gb]) + .load_languages(&Localizations, &[ru.clone(), en_gb]) .unwrap(); - let msg = loader.select_languages(&[&ru]).get("only-ru"); + let msg = loader.select_languages(&[ru.clone()]).get("only-ru"); assert_eq!("только русский", msg); - let msg = loader.select_languages(&[&ru]).get("only-gb"); + let msg = loader.select_languages(&[ru]).get("only-gb"); assert_eq!("only GB (US Version)", msg); } @@ -243,7 +241,7 @@ mod fluent { let loader = FluentLanguageLoader::new("test", en_us); loader - .load_languages(&Localizations, &[&ru, &en_gb]) + .load_languages(&Localizations, &[ru.clone(), en_gb]) .unwrap(); let args = maplit::hashmap! { @@ -252,7 +250,7 @@ mod fluent { }; let msg = loader - .select_languages(&[&ru]) + .select_languages(&[ru]) .get_args("multi-line-args", args); assert_eq!( "Это многострочное сообщение с параметрами.\n\n\ @@ -273,13 +271,15 @@ mod fluent { let loader = FluentLanguageLoader::new("test", en_us); loader - .load_languages(&Localizations, &[&ru, &en_gb]) + .load_languages(&Localizations, &[ru.clone(), en_gb.clone()]) .unwrap(); - let msg = loader.select_languages(&[&ru, &en_gb]).get("only-gb"); + let msg = loader + .select_languages(&[ru.clone(), en_gb.clone()]) + .get("only-gb"); assert_eq!("only GB", msg); - let msg = loader.select_languages(&[&ru, &en_gb]).get("only-us"); + let msg = loader.select_languages(&[ru, en_gb]).get("only-us"); assert_eq!("only US", msg); } @@ -292,7 +292,7 @@ mod fluent { let loader = FluentLanguageLoader::new("test", en_us); loader - .load_languages(&Localizations, &[&ru, &en_gb]) + .load_languages(&Localizations, &[ru.clone(), en_gb.clone()]) .unwrap(); let args = maplit::hashmap! { @@ -300,12 +300,12 @@ mod fluent { }; let msg = loader - .select_languages(&[&ru]) + .select_languages(&[ru.clone()]) .get_args("only-gb-args", args.clone()); assert_eq!("Hello \u{2068}username\u{2069}! (US Version)", msg); let msg = loader - .select_languages(&[&ru, &en_gb]) + .select_languages(&[ru, en_gb]) .get_args("only-gb-args", args.clone()); assert_eq!("Hello \u{2068}username\u{2069}!", msg); } @@ -357,10 +357,10 @@ mod gettext { let ru: LanguageIdentifier = "ru".parse().unwrap(); let en: LanguageIdentifier = "en".parse().unwrap(); - loader.load_languages(&Localizations, &[&ru]).unwrap(); + loader.load_languages(&Localizations, &[ru]).unwrap(); // It should replace the ru with en - loader.load_languages(&Localizations, &[&en]).unwrap(); + loader.load_languages(&Localizations, &[en]).unwrap(); pretty_assertions::assert_eq!("only en", tr("only en")); pretty_assertions::assert_eq!("only ru", tr("only ru")); @@ -376,7 +376,7 @@ mod gettext { let ru: LanguageIdentifier = "ru".parse().unwrap(); assert!(Localizations::get("ru/i18n_embed.mo").is_some()); - loader.load_languages(&Localizations, &[&ru]).unwrap(); + loader.load_languages(&Localizations, &[ru]).unwrap(); pretty_assertions::assert_eq!("только ру", tr("only ru")); pretty_assertions::assert_eq!("only en", tr("only en")); diff --git a/src/main.rs b/src/main.rs index 76cf2b9..c2db176 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,7 @@ use i18n_build::run; use i18n_config::Crate; use i18n_embed::{ gettext::{gettext_language_loader, GettextLanguageLoader}, - DefaultLocalizer, DesktopLanguageRequester, I18nAssets, LanguageLoader, LanguageRequester, - Localizer, + DefaultLocalizer, DesktopLanguageRequester, LanguageLoader, LanguageRequester, Localizer, }; use lazy_static::lazy_static; use rust_embed::RustEmbed; @@ -32,7 +31,7 @@ fn short_about() -> String { tr!("A Cargo sub-command to extract and build localization resources.") } -fn available_languages(localizer: &Arc) -> Result> { +fn available_languages(localizer: &dyn Localizer) -> Result> { Ok(localizer .available_languages()? .iter() @@ -76,11 +75,11 @@ fn main() -> Result<()> { env_logger::init(); let mut language_requester = DesktopLanguageRequester::new(); - let cargo_i18n_localizer = - DefaultLocalizer::new(&*LANGUAGE_LOADER, &TRANSLATIONS as &dyn I18nAssets); + let cargo_i18n_localizer: DefaultLocalizer<'static> = + DefaultLocalizer::new(&*LANGUAGE_LOADER, &TRANSLATIONS); let cargo_i18n_localizer_rc: Arc = Arc::new(cargo_i18n_localizer); - let i18n_build_localizer_rc = Arc::new(i18n_build::localizer()) as Arc; + let i18n_build_localizer_rc: Arc = Arc::new(i18n_build::localizer()); language_requester.add_listener(Arc::downgrade(&cargo_i18n_localizer_rc)); language_requester.add_listener(Arc::downgrade(&i18n_build_localizer_rc)); @@ -88,7 +87,7 @@ fn main() -> Result<()> { let fallback_locale: &'static str = String::leak(LANGUAGE_LOADER.fallback_language().to_string()); - let available_languages = available_languages(&cargo_i18n_localizer_rc)?; + let available_languages = available_languages(&*cargo_i18n_localizer_rc)?; let available_languages_slice: Vec<&'static str> = available_languages .into_iter() .map(|l| String::leak(l) as &str) @@ -138,7 +137,7 @@ fn main() -> Result<()> { .long("language") .short('l') .num_args(1) - .default_value(&*fallback_locale) + .default_value(fallback_locale) .value_parser(PossibleValuesParser::new(available_languages_slice)) ) )