i18n-embed-0.14.1/.cargo_vcs_info.json0000644000000001500000000000100127440ustar { "git": { "sha1": "6b7bfcf2a66a537c61e49794932347b32aca0b17" }, "path_in_vcs": "i18n-embed" }i18n-embed-0.14.1/.gitignore000064400000000000000000000000561046102023000135310ustar 00000000000000/target **/*.rs.bk .vscode Cargo.lock i18n/poti18n-embed-0.14.1/CHANGELOG.md000064400000000000000000000311771046102023000133620ustar 00000000000000# Changelog for `i18n-embed` ## v0.14.1 ## Internal + Relax the version constraint on `arc-swap`. ## v0.14.0 ### Internal + Bump dependencies and use workspace dependencies. ## v0.13.9 ## New Features - Add option to override the default domain name for fluent assets. ## v0.13.8 ### Internal - Made `arc-swap` an optional dependency, it's only required for `fluent-system` implementation. ## v0.13.7 ### New Features - A new `LanguageLoader::load_available_languages()` method to load all available languages. - A new `FluentLanguageLoader::select_languages()` method (renamed `FluentLanguageLoader::lang()`). - A new `FluentLanguageLoader::select_languages_negotiate()` method to select languages based on a negotiation strategy using the available languges. ### Deprecated - `FluentLanguageLoader::lang()` is deprecated in favour of renamed `FluentLanguageLoader::select_languages()`. ## v0.13.6 ### New Features - A single new method called `FluentLanguageLoader::lang()` thanks to [@bikeshedder](https://github.com/bikeshedder)! This methods allows creating a shallow copy of the FluentLanguageLoader which can than be used just like the original loader but with a different current language setting. That makes it possible to use the fl! macro without any changes and is a far more elegant implementation than adding multiple get_lang\* methods as done in #84. ### Deprecated - `FluentLanguageLoader::get_lang*` methods have been deprecated in favour of `FluentLanguageLoader::lang()`. ## v0.13.5 ### New Features - Support fluent attributes [#98](https://github.com/kellpossible/cargo-i18n/pull/98) thanks to [@Almost-Senseless-Coder](https://github.com/Almost-Senseless-Coder)! - New methods on `FluentLanguageLoader`: - `get_attr` - `get_attr_args_concrete` - `get_attr_args_fluent` - `get_attr_args` - `get_lang_attr` - `get_lang_attr_args_concrete` - `get_lang_attr_args_fluent` - `get_lang_attr_args` ### Internal - Bump `env_logger` dev dependency to version `0.10`. - Fix clippy warnings. ## v0.13.4 ### New Features - Implement `FluentLanguageLoader::get_lang(...)` methods. This enables the use of the fluent language loader without using the global current language setting, which is useful for web servers. Closes [#59](https://github.com/kellpossible/cargo-i18n/issues/59). ## v0.13.3 - Update `rust-embed` to `6.3` to address [RUSTSEC-2021-0126](https://rustsec.org/advisories/RUSTSEC-2021-0126.html). ## v0.13.2 ### Internal - Use conditional compilation correctly for doctests. - Update `parking_lot` to version `0.12`. ## v0.13.1 ### New Features - New `FluentLanguageLoader::with_bundles_mut()` method to allow mutable access to bundles. ### Internal Changes - Bumped `pretty_assertions` version `1.0`. - Fixed clippy lints. ## v0.13.0 ### Breaking Changes - Update `rust-embed` to version `6`. - Update `fluent` to version `0.16`. ## v0.12.1 ### Documentation - Updated crate description. - Don't reference specific `i18n-embed` version number. ## v0.12.0 ### Documentation - Added [`bin`](./examples/bin/) example which explains how to consume the [`lib-fluent`](./examples/lib-fluent) example library in a desktop CLI application. ### Breaking Changes - Updated `fluent` to version `0.15`. ### Internal Changes - Updated `FluentLanguageLoader` to use a thread safe [IntlLangMemoizer](https://docs.rs/intl-memoizer/0.5.1/intl_memoizer/concurrent/struct.IntlLangMemoizer.html) as per the notes on [FluentBundle's concurrency](https://docs.rs/fluent-bundle/0.15.0/fluent_bundle/bundle/struct.FluentBundle.html#concurrency). This was required to solve a compilation error in `i18n-embed-fl` and may also fix problems for other downstream users who were expecting `FluentLangaugeLoader` to be `Send + Sync`. It might impact performance for those who are not using this in multi-threaded context, please report this, and in which case support for switching the `IntlLangMemoizer` added. ## v0.11.0 ### 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. - Update `i18-embed-impl` to version `0.7.0`. ## v0.10.2 - Add workaround for [#57](https://github.com/kellpossible/cargo-i18n/issues/57) for until is solved. ## v0.10.1 - Update references to `i18n-embed` version in readme and source code examples. ## v0.10.0 ### Fixes - More gracefully handle the situation on Linux where LANG environment variable is not set due to [rust-locale/locale_config#6](https://github.com/rust-locale/locale_config/issues/6). Fixes [#49](https://github.com/kellpossible/cargo-i18n/issues/49). ### Internal Changes - Update `fluent` dependency to version `0.14`. ## v0.9.4 ### New Features - Functionality to disable bidirectional isolation in Fluent with `FluentLanguageLoader` with a new `set_use_isolating` method [#45](https://github.com/kellpossible/cargo-i18n/issues/45). ### Internal Changes - Remove the now redundant CRLF fix [#36](https://github.com/kellpossible/cargo-i18n/issues/36). ## v0.9.3 ### Fixes - Updated documentation for `select()` function. ## v0.9.2 ### Fixes - Remove compiler warning. ## v0.9.1 ### Fixes - Renamed argument in `select()` method for clarity. - Changed logs in `select()` method to use `debug` level instead of `info` level. ## v0.9.0 - Bumped version to reflect potential breaking changes present in the new version of `fluent`, `0.13` which is exposed in this crate's public API. And yanked previous versions of `i18n-embed`: `0.8.6` and `0.8.5`. ## v0.8.6 - Update documentation and example to more accurately reflect the current state of `LangaugeRequester::poll()` on various systems. ## v0.8.5 ### New Features - Add new `get_args_fluent()` method to `FluentLanguageLoader` to allow arguments to be specified using `fluent`'s new `FluentArgs` type. ### Internal Changes - Update `fluent` to version `0.13`. - Fixes to address breaking changes in `fluent-syntax` version `0.10`. ## v0.8.4 ### Bug Fixes - A workaround for the [fluent issue #191](https://github.com/projectfluent/fluent-rs/issues/191), where CRLF formatted localization files are not always successfully parsed by fluent. ## v0.8.3 ### New Features - Added a new `with_mesage_iter()` method to `FluentLanguageLoader`, to allow iterating over the messages available for a particular language. - Added `Default` implementation for `WebLanguageRequester`. ## v0.8.2 - Fixed some mistakes in the docs. ## v0.8.1 - Update version reference to `i18n-embed` in README, and docs. ## v0.8.0 Changes to support the new `i18n-embed-fl` crate's `fl!()` macro, and some major cleanup/refactoring/simplification. ### New Features - A new `I18nAssets` trait, to support situations where assets are not embedded. - Automatic implementation of the `I18nAssets` trait for types that implement `RustEmbed`. - A new `FileSystemAssets` type (which is enabled using the crate feature `filesystem-assets`), which implements `I18nAssets` for loading assets at runtime from the file system. - Implemented `Debug` trait on more types. - Added new `has()` and `with_fluent_message()` methods to `FluentLanguageLoader`. - Made `LanguageRequesterImpl` available on default crate features. No longer requires `gettext-system` or `fluent-system` to be enabled. ### Breaking Changes - Removed `I18nEmbed` trait, and derive macro, it was replaced with the new `I18nAssets` trait. - Clarified the `domain` and `module` arguments/variable inputs to `FluentLanguageLoader` and `GettextLanguageLoader`, and in the `LanguageLoader` trait with some renaming. - Removed a bunch of unecessary lifetimes, and `'static` bounds on types, methods and arguments. - `LanguageRequester::current_languages()`'s return type now uses `String` as the `HashMap` key instead of `&'static str`. - `available_languages()` implementation moved from `I18nEmbed` to `LanguageLoader`. ### Bug Fixes - Improved resolution of `i18n.toml` location in both the `gettext_language_loader!()` and `fluent_language_loader!()` macros using [find-crate](https://github.com/taiki-e/find-crate). ## v0.7.2 - Fix broken documentation links when compiling with no features. ## v0.7.1 - Fix broken documentation links. ## v0.7.0 Changes for the support of the `fluent` localization system. ### New Features - Added two new optional crate feature flags `gettext-system` and `fluent-system` to enable the new `GettextLanguageLoader` and `FluentLanguageLoader` types. See the [README](./README.md) and docs for more details. ### Breaking Changes - Update to `i18n-config` version `0.3.0`, contains breaking changes to `i18n.toml` configuration file format. See the [i18n changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-config/CHANGELOG.md#v030) for more details. - Rename `language_loader!()` macro to `gettext_language_loader!()`, and change how it works a little to make it simpler. Most of the functionality has been moved into the new `GettextLanguageLoader` type. See the docs. - `gettext-system` is no longer included in the default crate features. ## v0.6.1 ### Bug Fixes - Only re-export optional dependencies when they're actually enabled in the crate features ([#26](https://github.com/kellpossible/cargo-i18n/pull/26) thanks to @jplatte.) ## v0.6.0 - Changed the argument for `LanguageRequester::add_listener()` to use a `std::rc::Weak` instead of `std::rc::Rc` to make it more obvious that it is the caller's responsibility to hold on to the `Rc` in order to maintain the reference. - Fixed typo in `LanguageRequester::set_language_override()`. ## v0.5.0 - Refactored `I18nEmbedError::Multiple(Box>)` to `I18nEmbedError::Multiple(Vec)`, removing the useless box (and complaining Clippy lint). - Refactored `select()` method to use slice argument instead of `&Vec`. - Changed `LanguageRequester::add_listener(&mut self, localizer: &Rc>>)` to `add_listener(&mut self, localizer: &Rc>)` removing the unnecessary `Box`. - Added `Default` implementation for `DesktopLanguageRequester`. ## v0.4.2 - Update `fluent-langneg` dependency to version `0.13`. - Update `unic-langid` dependency to version `0.9`. - Fix incorrect comment in code example [#18](https://github.com/kellpossible/cargo-i18n/issues/18). ## v0.4.0 Mostly a refactor of `LanguageLoader` and `I18nAssets` to solve [issue #15](https://github.com/kellpossible/cargo-i18n/issues/15). - Replaced the derive macro for `LanguageLoader` with a new `language_loader!(StructName)` which creates a new struct with the specified `StructName` and implements `LanguageLoader` for it. This was done because `LanguageLoader` now needs to store state for the currently selected language, and deriving this automatically would be complicated. - Refactored `I18nAssets` to move the `load_language_file` responsibility into `LanguageLoader` and add a new `load_language` method to `LanguageLoader`. - Refactored `I18nAssetsDyn` to also expose the `RustEmbed#get()` method, required for the new `LanguageLoader` changes. - Using `LanguageLoader` as a static now requires [lazy_static](https://crates.io/crates/lazy_static) or something similar because the `StructName#new()` constructor which is created for it in `language_loader!(StructName)` is not `const`. ## v0.3.4 - Made `WebLanguageRequester::requested_languages()` public. ## v0.3.3 - Updated link to this changelog in the crate README. ## v0.3.2 - Bump `i18n-config` dependency in `i18n-embed-impl` version to `0.2`. i18n-embed-0.14.1/Cargo.toml0000644000000054350000000000100107550ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "i18n-embed" version = "0.14.1" authors = ["Luke Frisken "] exclude = [ "i18n/", "i18n.toml", ] description = "Traits and macros to conveniently embed localization assets into your application binary or library in order to localize it at runtime." readme = "README.md" keywords = [ "embed", "macro", "i18n", "gettext", "fluent", ] categories = [ "localization", "internationalization", "development-tools::build-utils", "wasm", ] license = "MIT" repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-embed" [package.metadata.docs.rs] all-features = true [dependencies.arc-swap] version = "1" optional = true [dependencies.fluent] version = "0.16" optional = true [dependencies.fluent-langneg] version = "0.13" [dependencies.fluent-syntax] version = "0.11" optional = true [dependencies.gettext] version = "0.4" optional = true [dependencies.i18n-embed-impl] version = "0.8.1" optional = true [dependencies.intl-memoizer] version = "0.5" [dependencies.lazy_static] version = "1.4.0" [dependencies.locale_config] version = "0.3" optional = true [dependencies.log] version = "0.4" [dependencies.parking_lot] version = "0.12" optional = true [dependencies.rust-embed] version = "8.0" optional = true [dependencies.thiserror] version = "1.0" [dependencies.tr] version = "0.1" optional = true default-features = false [dependencies.unic-langid] version = "0.9" [dependencies.walkdir] version = "2.4" optional = true [dependencies.web-sys] version = "0.3" features = [ "Window", "Navigator", ] optional = true [dev-dependencies.doc-comment] version = "0.3" [dev-dependencies.env_logger] version = "0.10" [dev-dependencies.maplit] version = "1.0" [dev-dependencies.pretty_assertions] version = "1.4" [dev-dependencies.serial_test] version = "2.0" [features] default = ["rust-embed"] desktop-requester = ["locale_config"] filesystem-assets = ["walkdir"] fluent-system = [ "fluent", "fluent-syntax", "parking_lot", "i18n-embed-impl", "i18n-embed-impl/fluent-system", "arc-swap", ] gettext-system = [ "tr", "tr/gettext", "dep:gettext", "parking_lot", "i18n-embed-impl", "i18n-embed-impl/gettext-system", ] web-sys-requester = ["web-sys"] [badges.maintenance] status = "actively-developed" i18n-embed-0.14.1/Cargo.toml.orig000064400000000000000000000040301046102023000144240ustar 00000000000000[package] authors = ["Luke Frisken "] categories = ["localization", "internationalization", "development-tools::build-utils", "wasm"] description = "Traits and macros to conveniently embed localization assets into your application binary or library in order to localize it at runtime." edition = "2018" exclude = ["i18n/", "i18n.toml"] keywords = ["embed", "macro", "i18n", "gettext", "fluent"] license = "MIT" name = "i18n-embed" readme = "README.md" repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-embed" version = "0.14.1" [package.metadata.docs.rs] all-features = true [badges] maintenance = { status = "actively-developed" } [dependencies] fluent = { workspace = true, optional = true } arc-swap = { version = "1", optional = true } fluent-langneg = { workspace = true } fluent-syntax = { workspace = true, optional = true } gettext = { workspace = true, optional = true } i18n-embed-impl = { version = "0.8.1", path = "./i18n-embed-impl", optional = true } intl-memoizer = "0.5" lazy_static = { workspace = true } locale_config = { version = "0.3", optional = true } log = { workspace = true } parking_lot = { version = "0.12", optional = true } rust-embed = { workspace = true, optional = true } thiserror = { workspace = true } tr = { version = "0.1", default-features = false, optional = true } unic-langid = { workspace = true } walkdir = { workspace = true, optional = true } web-sys = { version = "0.3", features = ["Window", "Navigator"], optional = true } [dev-dependencies] doc-comment = { workspace = true } env_logger = { workspace = true } maplit = "1.0" pretty_assertions = { workspace = true } serial_test = "2.0" [features] default = ["rust-embed"] gettext-system = ["tr", "tr/gettext", "dep:gettext", "parking_lot", "i18n-embed-impl", "i18n-embed-impl/gettext-system"] fluent-system = ["fluent", "fluent-syntax", "parking_lot", "i18n-embed-impl", "i18n-embed-impl/fluent-system", "arc-swap"] desktop-requester = ["locale_config"] web-sys-requester = ["web-sys"] filesystem-assets = ["walkdir"] i18n-embed-0.14.1/LICENSE.txt000064400000000000000000000020601046102023000133610ustar 00000000000000Copyright 2020 Luke Frisken Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. © 2020 Luke Friskeni18n-embed-0.14.1/README.md000064400000000000000000000124771046102023000130320ustar 00000000000000# i18n-embed [![crates.io badge](https://img.shields.io/crates/v/i18n-embed.svg)](https://crates.io/crates/i18n-embed) [![docs.rs badge](https://docs.rs/i18n-embed/badge.svg)](https://docs.rs/i18n-embed/) [![license badge](https://img.shields.io/github/license/kellpossible/cargo-i18n)](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-embed/LICENSE.txt) [![github actions badge](https://github.com/kellpossible/cargo-i18n/workflows/Rust/badge.svg)](https://github.com/kellpossible/cargo-i18n/actions?query=workflow%3ARust) Traits and macros to conveniently embed localization assets into your application binary or library in order to localize it at runtime. Works in unison with [cargo-i18n](https://crates.io/crates/cargo_i18n). Currently this library depends on [rust-embed](https://crates.io/crates/rust-embed) to perform the actual embedding of the language files. This may change in the future to make the library more convenient to use. **[Changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-embed/CHANGELOG.md)** ## Optional Features The `i18n-embed` crate has the following optional Cargo features: + `fluent-system` + Enable support for the [fluent](https://www.projectfluent.org/) localization system via the `FluentLanguageLoader`. + `gettext-system` + Enable support for the [gettext](https://www.gnu.org/software/gettext/) localization system using the [tr macro](https://docs.rs/tr/0.1.3/tr/) and the [gettext crate](https://docs.rs/gettext/0.4.0/gettext/) via the `GettextLanguageLoader`. + `desktop-requester` + Enables a convenience implementation of `LanguageRequester` trait called `DesktopLanguageRequester` for the desktop platform (windows, mac, linux),which makes use of the [locale_config](https://crates.io/crates/locale_config) crate for resolving the current system locale. + `web-sys-requester` + Enables a convenience implementation of `LanguageRequester` trait called `WebLanguageRequester` which makes use of the [web-sys](https://crates.io/crates/web-sys) crate for resolving the language being requested by the user's web browser in a WASM context. ## Example The following is a minimal example for how localize your binary using this library using the [fluent](https://www.projectfluent.org/) localization system. First you need to compile `i18n-embed` in your `Cargo.toml` with the `fluent-system` and `desktop-requester` features enabled: ```toml [dependencies] i18n-embed = { version = "VERSION", features = ["fluent-system", "desktop-requester"]} rust-embed = "6" unic-langid = "0.9" ``` Set up a minimal `i18n.toml` in your crate root to use with `cargo-i18n` (see [cargo i18n](../README.md#configuration) for more information on the configuration file format): ```toml # (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-GB" # 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" ``` Next, you want to create your localization resources, per language fluent files. `language` needs to conform to the [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) standard, and will be parsed via the [unic_langid crate](https://docs.rs/unic-langid/0.9.0/unic_langid/). The directory structure should look like so: ```txt my_crate/ Cargo.toml i18n.toml src/ i18n/ {language}/ {domain}.ftl ``` Then you can instantiate your language loader and language requester: ```rust use i18n_embed::{DesktopLanguageRequester, fluent::{ FluentLanguageLoader, fluent_language_loader }}; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "i18n"] // path to the compiled localization resources struct Localizations; fn main() { let language_loader: FluentLanguageLoader = fluent_language_loader!(); // Use the language requester for the desktop platform (linux, windows, mac). // There is also a requester available for the web-sys WASM platform called // WebLanguageRequester, or you can implement your own. let requested_languages = DesktopLanguageRequester::requested_languages(); let _result = i18n_embed::select( &language_loader, &Localizations, &requested_languages); // continue on with your application } ``` To access localizations, you can use `FluentLanguageLoader`'s methods directly, or, for added compile-time checks/safety, you can use the [fl!() macro](https://crates.io/crates/i18n-embed-fl). Having an `i18n.toml` configuration file enables you to do the following: + Use the [cargo i18n](https://crates.io/crates/cargo-i18n) tool to perform validity checks (not yet implemented). + Integrate with a code-base using the `gettext` localization system. + Use the `fluent::fluent_language_loader!()` macro to pull the configuration in at compile time to create the `fluent::FluentLanguageLoader`. + Use the [fl!() macro](https://crates.io/crates/i18n-embed-fl) to have added compile-time safety when accessing messages. Example projects can be found in [examples](./examples). For more explained examples, see the [documentation for i18n-embed](https://docs.rs/i18n-embed/). i18n-embed-0.14.1/src/assets.rs000064400000000000000000000070641046102023000142060ustar 00000000000000use std::borrow::Cow; /// 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>; } #[cfg(feature = "rust-embed")] impl I18nAssets for T where T: rust_embed::RustEmbed + 'static, { fn get_file(&self, file_path: &str) -> Option> { Self::get(file_path).map(|file| file.data) } fn filenames_iter(&self) -> Box> { Box::new(Self::iter().map(|filename| filename.to_string())) } } /// An [I18nAssets] implementation which pulls assets from the OS /// file system. #[cfg(feature = "filesystem-assets")] #[derive(Debug)] pub struct FileSystemAssets { base_dir: std::path::PathBuf, } #[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 { let base_dir = base_dir.into(); if !base_dir.exists() { panic!("specified `base_dir` ({:?}) does not exist", base_dir); } if !base_dir.is_dir() { panic!("specified `base_dir` ({:?}) is not a directory", base_dir); } Self { base_dir } } } #[cfg(feature = "filesystem-assets")] impl I18nAssets for FileSystemAssets { fn get_file(&self, file_path: &str) -> Option> { let full_path = self.base_dir.join(file_path); if !(full_path.is_file() && full_path.exists()) { return None; } match std::fs::read(full_path) { Ok(contents) => Some(Cow::from(contents)), Err(e) => { log::error!( target: "i18n_embed::assets", "Unexpected error while reading localization asset file: {}", e); None } } } fn filenames_iter(&self) -> Box> { Box::new( walkdir::WalkDir::new(&self.base_dir) .into_iter() .filter_map(|f| match f { Ok(f) => { if f.file_type().is_file() { match f.file_name().to_str() { Some(filename) => Some(filename.to_string()), None => { log::error!( target: "i18n_embed::assets", "Filename {:?} is not valid UTF-8.", f.file_name()); None } } } else { None } } Err(err) => { log::error!( target: "i18n_embed::assets", "Unexpected error while gathering localization asset filenames: {}", err); None } }), ) } } i18n-embed-0.14.1/src/fluent.rs000064400000000000000000000622671046102023000142070ustar 00000000000000//! This module contains the types and functions to interact with the //! `fluent` localization system. //! //! Most important is the [FluentLanguageLoader]. //! //! ⚠️ *This module requires the following crate features to be activated: `fluent-system`.* use crate::{I18nAssets, I18nEmbedError, LanguageLoader}; use arc_swap::ArcSwap; pub use fluent_langneg::NegotiationStrategy; pub use i18n_embed_impl::fluent_language_loader; use fluent::{ bundle::FluentBundle, FluentArgs, FluentAttribute, FluentMessage, FluentResource, FluentValue, }; use fluent_syntax::ast::{self, Pattern}; use intl_memoizer::concurrent::IntlLangMemoizer; use parking_lot::RwLock; use std::{borrow::Cow, collections::HashMap, fmt::Debug, iter::FromIterator, sync::Arc}; use unic_langid::LanguageIdentifier; struct LanguageBundle { language: LanguageIdentifier, bundle: FluentBundle, IntlLangMemoizer>, resource: Arc, } impl LanguageBundle { fn new(language: LanguageIdentifier, resource: FluentResource) -> Self { let mut bundle = FluentBundle::new_concurrent(vec![language.clone()]); let resource = Arc::new(resource); if let Err(errors) = bundle.add_resource(resource.clone()) { errors.iter().for_each(|error | { log::error!(target: "i18n_embed::fluent", "Error while adding resource to bundle: {0:?}.", error); }) } Self { language, bundle, resource, } } } impl Debug for LanguageBundle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "LanguageBundle(language: {})", self.language) } } #[derive(Debug)] struct LanguageConfig { language_bundles: Vec, /// This maps a `LanguageIdentifier` to the index inside the /// `language_bundles` vector. language_map: HashMap, } #[derive(Debug)] struct CurrentLanguages { /// Languages currently selected. languages: Vec, /// Indexes into the [`LanguageConfig::language_bundles`] associated the /// currently selected [`CurrentLanguages::languages`]. indices: Vec, } #[derive(Debug)] struct FluentLanguageLoaderInner { language_config: Arc>, current_languages: CurrentLanguages, } /// [LanguageLoader] implemenation for the `fluent` localization /// system. Also provides methods to access localizations which have /// been loaded. /// /// ⚠️ *This API requires the following crate features to be activated: `fluent-system`.* #[derive(Debug)] pub struct FluentLanguageLoader { inner: ArcSwap, domain: String, fallback_language: unic_langid::LanguageIdentifier, } impl FluentLanguageLoader { /// Create a new `FluentLanguageLoader`, which loads messages for /// the specified `domain`, and relies on the specified /// `fallback_language` for any messages that do not exist for the /// current language. pub fn new>( domain: S, fallback_language: unic_langid::LanguageIdentifier, ) -> Self { let config = LanguageConfig { language_bundles: Vec::new(), language_map: HashMap::new(), }; Self { inner: ArcSwap::new(Arc::new(FluentLanguageLoaderInner { language_config: Arc::new(RwLock::new(config)), current_languages: CurrentLanguages { languages: vec![fallback_language.clone()], indices: vec![], }, })), domain: domain.into(), fallback_language, } } fn current_language_impl( &self, inner: &FluentLanguageLoaderInner, ) -> unic_langid::LanguageIdentifier { inner .current_languages .languages .first() .map_or_else(|| self.fallback_language.clone(), Clone::clone) } /// The languages associated with each actual currently loaded language bundle. pub fn current_languages(&self) -> Vec { self.inner.load().current_languages.languages.clone() } /// Get a localized message referenced by the `message_id`. pub fn get(&self, message_id: &str) -> String { self.get_args_fluent(message_id, None) } /// A non-generic version of [FluentLanguageLoader::get_args()]. pub fn get_args_concrete<'args>( &self, message_id: &str, args: HashMap<&'args str, FluentValue<'args>>, ) -> String { self.get_args_fluent(message_id, hash_map_to_fluent_args(args).as_ref()) } /// A non-generic version of [FluentLanguageLoader::get_args()] /// accepting [FluentArgs] instead of a [HashMap]. pub fn get_args_fluent<'args>( &self, message_id: &str, args: Option<&'args FluentArgs<'args>>, ) -> String { let inner = self.inner.load(); let language_config = inner.language_config.read(); inner .current_languages .indices .iter() .map(|&idx| &language_config.language_bundles[idx]) .find_map(|language_bundle| language_bundle .bundle .get_message(message_id) .and_then(|m: FluentMessage<'_>| m.value()) .map(|pattern: &Pattern<&str>| { let mut errors = Vec::new(); let value = language_bundle.bundle.format_pattern(pattern, args, &mut errors); if !errors.is_empty() { log::error!( target:"i18n_embed::fluent", "Failed to format a message for language \"{}\" and id \"{}\".\nErrors\n{:?}.", inner.current_languages.languages.first().unwrap_or(&self.fallback_language), message_id, errors ) } value.into() }) ) .unwrap_or_else(|| { log::error!( target:"i18n_embed::fluent", "Unable to find localization for language \"{}\" and id \"{}\".", inner.current_languages.languages.first().unwrap_or(&self.fallback_language), message_id ); format!("No localization for id: \"{}\"", message_id) }) } /// Get a localized message referenced by the `message_id`, and /// formatted with the specified `args`. pub fn get_args<'a, S, V>(&self, id: &str, args: HashMap) -> String where S: Into> + Clone, V: Into> + Clone, { self.get_args_fluent(id, hash_map_to_fluent_args(args).as_ref()) } /// Get a localized attribute referenced by the `message_id` and `attribute_id`. pub fn get_attr(&self, message_id: &str, attribute_id: &str) -> String { self.get_attr_args_fluent(message_id, attribute_id, None) } /// A non-generic version of [FluentLanguageLoader::get_attr_args()]. pub fn get_attr_args_concrete<'args>( &self, message_id: &str, attribute_id: &str, args: HashMap<&'args str, FluentValue<'args>>, ) -> String { self.get_attr_args_fluent( message_id, attribute_id, hash_map_to_fluent_args(args).as_ref(), ) } /// A non-generic version of [FluentLanguageLoader::get_attr_args()] /// accepting [FluentArgs] instead of a [HashMap]. pub fn get_attr_args_fluent<'args>( &self, message_id: &str, attribute_id: &str, args: Option<&'args FluentArgs<'args>>, ) -> String { let inner = self.inner.load(); 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_bundle .bundle .get_message(message_id) .and_then(|m: FluentMessage<'_>| { m.get_attribute(attribute_id) .map(|a: FluentAttribute<'_>| { a.value() }) }) .map(|pattern: &Pattern<&str>| { let mut errors = Vec::new(); let value = language_bundle.bundle.format_pattern(pattern, args, &mut errors); if !errors.is_empty() { log::error!( target:"i18n_embed::fluent", "Failed to format a message for language \"{}\" and id \"{}\".\nErrors\n{:?}.", current_language, message_id, errors ) } value.into() }) }) .unwrap_or_else(|| { log::error!( target:"i18n_embed::fluent", "Unable to find localization for language \"{}\", message id \"{}\" and attribute id \"{}\".", current_language, message_id, attribute_id ); format!("No localization for message id: \"{message_id}\" and attribute id: \"{attribute_id}\"") }) } /// Get a localized attribute referenced by the `message_id` and `attribute_id`, and /// formatted with the specified `args`. pub fn get_attr_args<'a, S, V>( &self, message_id: &str, attribute_id: &str, args: HashMap, ) -> String where S: Into> + Clone, V: Into> + Clone, { self.get_attr_args_fluent( message_id, attribute_id, hash_map_to_fluent_args(args).as_ref(), ) } /// 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 } /// Determines if an attribute associated with the specified `message_id` /// is available in any of the currently loaded languages, including the fallback language. /// /// Returns true if at least one available instance was found, /// false otherwise. /// /// Note that this also returns false if the `message_id` could not be found; /// use [FluentLanguageLoader::has()] to determine if the `message_id` is available. pub fn has_attr(&self, message_id: &str, attribute_id: &str) -> bool { self.inner .load() .language_config .read() .language_bundles .iter() .find_map(|bundle| { bundle .bundle .get_message(message_id) .map(|message| message.get_attribute(attribute_id).is_some()) }) .unwrap_or(false) } /// Run the `closure` with the message that matches the specified /// `message_id` (if it is available in any of the languages /// currently loaded, including the fallback language). Returns /// `Some` of whatever whatever the closure returns, or `None` if /// no messages were found matching the `message_id`. pub fn with_fluent_message(&self, message_id: &str, closure: C) -> Option where C: Fn(fluent::FluentMessage<'_>) -> OUT, { self.inner .load() .language_config .read() .language_bundles .iter() .find_map(|language_bundle| language_bundle.bundle.get_message(message_id)) .map(closure) } /// Runs the provided `closure` with an iterator over the messages /// available for the specified `language`. There may be duplicate /// messages when they are duplicated in resources applicable to /// the language. Returns the result of the closure. pub fn with_message_iter(&self, language: &LanguageIdentifier, closure: C) -> OUT where C: Fn(&mut dyn Iterator>) -> OUT, { let inner = self.inner.load(); let config_lock = inner.language_config.read(); let mut iter = config_lock .language_bundles .iter() .filter(|language_bundle| &language_bundle.language == language) .flat_map(|language_bundle| { language_bundle .resource .entries() .filter_map(|entry| match entry { ast::Entry::Message(message) => Some(message), _ => None, }) }); (closure)(&mut iter) } /// Set whether the underlying Fluent logic should insert Unicode /// Directionality Isolation Marks around placeables. /// /// See [`fluent::bundle::FluentBundleBase::set_use_isolating`] for more /// information. /// /// **Note:** This function will have no effect if /// [`LanguageLoader::load_languages`] has not been called first. /// /// Default: `true`. pub fn set_use_isolating(&self, value: bool) { self.with_bundles_mut(|bundle| bundle.set_use_isolating(value)); } /// Apply some configuration to each budle in this loader. /// /// **Note:** This function will have no effect if /// [`LanguageLoader::load_languages`] has not been called first. pub fn with_bundles_mut(&self, f: F) where F: Fn(&mut FluentBundle, IntlLangMemoizer>), { for bundle in self .inner .load() .language_config .write() .language_bundles .as_mut_slice() { 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 /// 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. pub fn select_languages>( &self, languages: &[LI], ) -> FluentLanguageLoader { let inner = self.inner.load(); let config_lock = inner.language_config.read(); let fallback_language: Option<&unic_langid::LanguageIdentifier> = if languages .iter() .any(|language| language.as_ref() == &self.fallback_language) { None } else { Some(&self.fallback_language) }; let indices = languages .iter() .map(|lang| lang.as_ref()) .chain(fallback_language) .filter_map(|lang| config_lock.language_map.get(lang.as_ref())) .cloned() .collect(); FluentLanguageLoader { inner: ArcSwap::new(Arc::new(FluentLanguageLoaderInner { current_languages: CurrentLanguages { languages: languages.iter().map(|lang| lang.as_ref().clone()).collect(), indices, }, language_config: self.inner.load().language_config.clone(), })), domain: self.domain.clone(), fallback_language: self.fallback_language.clone(), } } /// Select the requested `languages` from the currently loaded languages using the supplied /// [`NegotiationStrategy`]. pub fn select_languages_negotiate>( &self, languages: &[LI], strategy: NegotiationStrategy, ) -> FluentLanguageLoader { let available_languages = &self.inner.load().current_languages.languages; let negotiated_languages = fluent_langneg::negotiate_languages( languages, available_languages, Some(self.fallback_language()), strategy, ); self.select_languages(&negotiated_languages) } } impl LanguageLoader for FluentLanguageLoader { /// The fallback language for the module this loader is responsible /// for. fn fallback_language(&self) -> &unic_langid::LanguageIdentifier { &self.fallback_language } /// The domain for the translation that this loader is associated with. fn domain(&self) -> &str { &self.domain } /// The language file name to use for this loader. fn language_file_name(&self) -> String { format!("{}.ftl", self.domain()) } /// Get the language which is currently selected for this loader. fn current_language(&self) -> unic_langid::LanguageIdentifier { self.current_language_impl(&self.inner.load()) } /// Load the languages `language_ids` using the resources packaged /// in the `i18n_assets` in order of fallback preference. This /// also sets the [LanguageLoader::current_language()] to the /// 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( &self, i18n_assets: &dyn I18nAssets, language_ids: &[&unic_langid::LanguageIdentifier], ) -> Result<(), I18nEmbedError> { if language_ids.is_empty() { return Err(I18nEmbedError::RequestedLanguagesEmpty); } // The languages to load let mut load_language_ids: Vec = language_ids.iter().map(|id| (**id).clone()).collect(); if !load_language_ids.contains(&self.fallback_language) { load_language_ids.push(self.fallback_language.clone()); } 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 { log::debug!(target:"i18n_embed::fluent", "Loaded language file: \"{0}\" for language: \"{1}\"", path, language); let file_string = String::from_utf8(file.to_vec()) .map_err(|err| I18nEmbedError::ErrorParsingFileUtf8(path.clone(), err))? // TODO: Workaround for https://github.com/kellpossible/cargo-i18n/issues/57 // remove when https://github.com/projectfluent/fluent-rs/issues/213 is resolved. .replace("\u{000D}\n", "\n"); let resource = match FluentResource::try_new(file_string) { Ok(resource) => resource, Err((resource, errors)) => { errors.iter().for_each(|err| { log::error!(target: "i18n_embed::fluent", "Error while parsing fluent language file \"{0}\": \"{1:?}\".", path, err); }); resource } }; 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())); } } } self.inner.swap(Arc::new(FluentLanguageLoaderInner { current_languages: CurrentLanguages { languages: language_ids.iter().map(|&lang| lang.to_owned()).collect(), 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)) .collect(), language_bundles, })), })); Ok(()) } } fn hash_map_to_fluent_args<'args, K, V>(map: HashMap) -> Option> where K: Into>, V: Into>, { if map.is_empty() { None } else { Some(FluentArgs::from_iter(map)) } } i18n-embed-0.14.1/src/gettext.rs000064400000000000000000000100461046102023000143620ustar 00000000000000//! This module contains the types and functions to interact with the //! `gettext` localization system. //! //! Most important is the [GettextLanguageLoader]. //! //! ⚠️ *This module requires the following crate features to be activated: `gettext-system`.* use crate::{domain_from_module, I18nAssets, I18nEmbedError, LanguageLoader}; pub use i18n_embed_impl::gettext_language_loader; use gettext as gettext_system; use parking_lot::RwLock; use unic_langid::LanguageIdentifier; /// [LanguageLoader] implementation for the `gettext` localization /// system. /// /// ⚠️ *This API requires the following crate features to be activated: `gettext-system`.* #[derive(Debug)] pub struct GettextLanguageLoader { current_language: RwLock, module: &'static str, fallback_language: LanguageIdentifier, } impl GettextLanguageLoader { /// Create a new `GettextLanguageLoader`. /// /// # Example /// /// ``` /// use i18n_embed::gettext::GettextLanguageLoader; /// /// GettextLanguageLoader::new(module_path!(), "en".parse().unwrap()); /// ``` pub fn new(module: &'static str, fallback_language: unic_langid::LanguageIdentifier) -> Self { Self { current_language: RwLock::new(fallback_language.clone()), module, fallback_language, } } fn load_src_language(&self) { let catalog = gettext_system::Catalog::empty(); tr::internal::set_translator(self.module, catalog); *(self.current_language.write()) = self.fallback_language().clone(); } } impl LanguageLoader for GettextLanguageLoader { /// The fallback language for the module this loader is responsible /// for. fn fallback_language(&self) -> &LanguageIdentifier { &self.fallback_language } /// The domain for the translation that this loader is associated with. fn domain(&self) -> &'static str { domain_from_module(self.module) } /// The language file name to use for this loader's domain. fn language_file_name(&self) -> String { format!("{}.mo", self.domain()) } /// Get the language which is currently loaded for this loader. fn current_language(&self) -> LanguageIdentifier { self.current_language.read().clone() } /// Load the languages `language_ids` using the resources packaged /// in the `i18n_assets` in order of fallback preference. This /// also sets the [LanguageLoader::current_language()] to the /// 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. /// /// **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. fn load_languages( &self, i18n_assets: &dyn I18nAssets, language_ids: &[&unic_langid::LanguageIdentifier], ) -> Result<(), I18nEmbedError> { let language_id = *language_ids .get(0) .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) => { log::error!( target:"i18n_embed::gettext", "{} Setting current_language to fallback locale: \"{}\".", I18nEmbedError::LanguageNotAvailable(path, language_id.clone()), self.fallback_language); self.load_src_language(); return Ok(()); } }; 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(()) } } i18n-embed-0.14.1/src/lib.rs000064400000000000000000000644331046102023000134550ustar 00000000000000#![allow(clippy::needless_doctest_main)] //! Traits and macros to conveniently embed localization assets into //! your application binary or library in order to localize it at //! runtime. Works in unison with //! [cargo-i18n](https://crates.io/crates/cargo_i18n). //! //! This library recommends tha you make use of //! [rust-embed](https://crates.io/crates/rust-embed) to perform the //! actual embedding of the language files, unfortunately using this //! currently requires you to manually add it as a dependency to your //! project and implement its trait on your struct in addition to //! [I18nAssets](I18nAssets). `RustEmbed` will not compile if the //! target `folder` path is invalid, so it is recommended to either //! run `cargo i18n` before building your project, or committing the //! localization assets into source control to ensure that the the //! folder exists and project can build without requiring `cargo //! i18n`. //! //! # Optional Features //! //! The `i18n-embed` crate has the following optional Cargo features: //! //! + `rust-embed` (Enabled by default) //! + Enable an automatic implementation of [I18nAssets] for any //! type that also implements `RustEmbed`. //! + `fluent-system` //! + Enable support for the //! [fluent](https://www.projectfluent.org/) localization system //! via the `fluent::FluentLanguageLoader` in this crate. //! + `gettext-system` //! + Enable support for the //! [gettext](https://www.gnu.org/software/gettext/) localization //! system using the [tr macro](https://docs.rs/tr/0.1.3/tr/) and //! the [gettext crate](https://docs.rs/gettext/0.4.0/gettext/) //! via the `gettext::GettextLanguageLoader` in this crate. //! + `desktop-requester` //! + Enables a convenience implementation of //! [LanguageRequester](LanguageRequester) trait called //! `DesktopLanguageRequester for the desktop platform (windows, //! mac, linux), which makes use of the //! [locale_config](https://crates.io/crates/locale_config) crate //! for resolving the current system locale. //! + `web-sys-requester` //! + Enables a convenience implementation of //! [LanguageRequester](LanguageRequester) trait called //! `WebLanguageRequester` which makes use of the //! [web-sys](https://crates.io/crates/web-sys) crate for //! resolving the language being requested by the user's web //! browser in a WASM context. //! //! # Examples //! //! ## Fluent Localization System //! //! The following is a simple example for how to localize your binary //! using this library when it first runs, using the `fluent` //! localization system, directly instantiating the //! `FluentLanguageLoader`. //! //! First you'll need the following features enabled in your //! `Cargo.toml`: //! //! ```toml //! [dependencies] //! i18n-embed = { version = "VERSION", features = ["fluent-system", "desktop-requester"]} //! rust-embed = "8" //! ``` //! //! Set up a minimal `i18n.toml` in your crate root to use with //! `cargo-i18n` (see [cargo //! i18n](https://github.com/kellpossible/cargo-i18n#configuration) //! for more information on the configuration file format): //! //! ```toml //! # (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-GB" //! //! # 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" //! ``` //! //! Next, you want to create your localization resources, per language //! fluent (`.ftl`) files. `language` needs to conform to the [Unicode //! Language //! Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) //! standard, and will be parsed via the [unic_langid //! crate](https://docs.rs/unic-langid/0.9.0/unic_langid/): //! //! ```txt //! my_crate/ //! Cargo.toml //! i18n.toml //! src/ //! i18n/ //! {language}/ //! {domain}.ftl //! ``` //! //! Then in your Rust code: //! //! ``` //! # #[cfg(all(feature = "desktop-requester", feature = "fluent-system"))] //! # { //! use i18n_embed::{DesktopLanguageRequester, fluent::{ //! FluentLanguageLoader, fluent_language_loader //! }}; //! use rust_embed::RustEmbed; //! //! #[derive(RustEmbed)] //! #[folder = "i18n"] // path to the compiled localization resources //! struct Localizations; //! //! # #[allow(dead_code)] //! fn main() { //! let language_loader: FluentLanguageLoader = fluent_language_loader!(); //! //! // Use the language requester for the desktop platform (linux, windows, mac). //! // There is also a requester available for the web-sys WASM platform called //! // WebLanguageRequester, or you can implement your own. //! let requested_languages = DesktopLanguageRequester::requested_languages(); //! //! let _result = i18n_embed::select( //! &language_loader, &Localizations, &requested_languages); //! //! // continue on with your application //! } //! # } //! ``` //! //! To access localizations, you can use `FluentLanguageLoader`'s //! methods directly, or, for added compile-time checks/safety, you //! can use the [fl!() macro](https://crates.io/crates/i18n-embed-fl). //! //! Having an `i18n.toml` configuration file enables you to do the //! following: //! //! + Use the [cargo i18n](https://crates.io/crates/cargo-i18n) tool //! to perform validity checks (not yet implemented). //! + Integrate with a code-base using the `gettext` localization //! system. //! + Use the `fluent::fluent_language_loader!()` macro to pull the //! configuration in at compile time to create the //! `fluent::FluentLanguageLoader`. //! + Use the [fl!() macro](https://crates.io/crates/i18n-embed-fl) to //! have added compile-time safety when accessing messages. //! //! ## Gettext Localization System //! //! The following is a simple example for how to localize your binary //! using this library when it first runs, using the `gettext` //! localization system. Please note that the `gettext` localization //! system is technically inferior to `fluent` [in a number of //! ways](https://github.com/projectfluent/fluent/wiki/Fluent-vs-gettext), //! however there are always legacy reasons, and the //! developer/translator ecosystem around `gettext` is mature. //! //! The `gettext::GettextLanguageLoader` in this example is //! instantiated using the `gettext::gettext_language_loader!()` //! macro, which automatically determines the correct module for the //! crate, and pulls settings in from the `i18n.toml` configuration //! file. //! //! First you'll need the following features enabled in your //! `Cargo.toml`: //! //! ```toml //! [dependencies] //! i18n-embed = { version = "VERSION", features = ["gettext-system", "desktop-requester"]} //! rust-embed = "8" //! ``` //! //! Set up a minimal `i18n.toml` in your crate root to use with //! `cargo-i18n` (see [cargo //! i18n](https://github.com/kellpossible/cargo-i18n#configuration) //! for more information on the configuration file format): //! //! ```toml //! # (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" //! //! # Use the gettext localization system. //! [gettext] //! # (Required) The languages that the software will be translated into. //! target_languages = ["es"] //! //! # (Required) Path to the output directory, relative to `i18n.toml` of //! # the crate being localized. //! output_dir = "i18n" //! ``` //! //! Install and run [cargo i18n](https://crates.io/crates/cargo-i18n) //! for your crate to generate the language specific `po` and `mo` //! files, ready to be translated. It is recommended to add the //! `i18n/pot` folder to your repository gitignore. //! //! Then in your Rust code: //! //! ``` //! # #[cfg(all(feature = "gettext-system", feature = "desktop-requester"))] //! # { //! use i18n_embed::{DesktopLanguageRequester, gettext::{ //! gettext_language_loader //! }}; //! use rust_embed::RustEmbed; //! //! #[derive(RustEmbed)] //! // path to the compiled localization resources, //! // as determined by i18n.toml settings //! #[folder = "i18n/mo"] //! struct Localizations; //! //! # #[allow(dead_code)] //! fn main() { //! // Create the GettextLanguageLoader, pulling in settings from `i18n.toml` //! // at compile time using the macro. //! let language_loader = gettext_language_loader!(); //! //! // Use the language requester for the desktop platform (linux, windows, mac). //! // There is also a requester available for the web-sys WASM platform called //! // WebLanguageRequester, or you can implement your own. //! let requested_languages = DesktopLanguageRequester::requested_languages(); //! //! let _result = i18n_embed::select( //! &language_loader, &Localizations, &requested_languages); //! //! // continue on with your application //! } //! # } //! ``` //! //! ## Automatic Updating Selection //! //! Depending on the platform, you can also make use of the //! [LanguageRequester](LanguageRequester)'s ability to monitor //! changes to the currently requested language, and automatically //! update the selected language using a [Localizer](Localizer): //! //! ``` //! # #[cfg(all(feature = "fluent-system", feature = "desktop-requester"))] //! # { //! use std::sync::Arc; //! use i18n_embed::{ //! DesktopLanguageRequester, LanguageRequester, //! DefaultLocalizer, Localizer, fluent::FluentLanguageLoader //! }; //! use rust_embed::RustEmbed; use lazy_static::lazy_static; //! use unic_langid::LanguageIdentifier; //! //! #[derive(RustEmbed)] //! #[folder = "i18n/ftl"] // path to localization resources //! struct Localizations; //! //! lazy_static! { //! static ref LANGUAGE_LOADER: FluentLanguageLoader = { //! // Usually you could use the fluent_language_loader!() macro //! // to pull values from i18n.toml configuration and current //! // module here at compile time, but instantiating the loader //! // manually here instead so the example compiles. //! let fallback: LanguageIdentifier = "en-US".parse().unwrap(); //! FluentLanguageLoader::new("test", fallback) //! }; //! } //! //! # #[allow(dead_code)] //! fn main() { //! let localizer = DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations); //! //! let localizer_arc: Arc = Arc::new(localizer); //! //! let mut language_requester = DesktopLanguageRequester::new(); //! 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 //! // currently varies, it may not change when the system requested //! // language changes during runtime without restarting your application. //! // In the future some platforms may also gain support for //! // automatic triggering when the requested display language changes. //! language_requester.poll().unwrap(); //! //! // continue on with your application //! } //! # } //! ``` //! //! The above example makes use of the //! [DefaultLocalizer](DefaultLocalizer), but you can also implement //! the [Localizer](Localizer) trait yourself for a custom solution. //! It also makes use of //! [lazy_static](https://crates.io/crates/lazy_static) to allow the //! [LanguageLoader](LanguageLoader) implementation to be stored //! statically, because its constructor is not `const`. //! //! ## Localizing Libraries //! //! If you wish to create a localizable library using `i18n-embed`, //! you can follow this code pattern in the library itself: //! //! ``` //! # #[cfg(feature = "fluent-system")] //! # { //! use std::sync::Arc; //! use i18n_embed::{ //! 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 Localizations; //! //! lazy_static! { //! 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() -> Arc { //! Arc::new(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) //! } //! # } //! ``` //! //! People using this library can call `localize()` to obtain a //! [Localizer](Localizer), and add this as a listener to their chosen //! [LanguageRequester](LanguageRequester). //! //! ## Localizing Sub-crates //! //! If you want to localize a sub-crate in your project, and want to //! extract strings from this sub-crate and store/embed them in one //! location in the parent crate, you can use the following pattern //! for the library: //! //! ``` //! # #[cfg(feature = "gettext-system")] //! # { //! use std::sync::Arc; //! use i18n_embed::{ //! DefaultLocalizer, Localizer, gettext::{ //! gettext_language_loader, GettextLanguageLoader //! }}; //! use i18n_embed::I18nAssets; //! use lazy_static::lazy_static; //! //! lazy_static! { //! static ref LANGUAGE_LOADER: GettextLanguageLoader = //! gettext_language_loader!(); //! } //! //! /// 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) -> Arc { //! Arc::new(DefaultLocalizer::new( //! &*LANGUAGE_LOADER, //! embed //! )) //! } //! # } //! ``` //! //! For the above example, you can enable the following options in the //! sub-crate's `i18n.toml` to ensure that the localization resources //! are extracted and merged with the parent crate's `pot` file: //! //! ```toml //! # ... //! //! [gettext] //! //! # ... //! //! # (Optional) If this crate is being localized as a subcrate, store the final //! # localization artifacts (the module pot and mo files) with the parent crate's //! # output. Currently crates which contain subcrates with duplicate names are not //! # supported. //! extract_to_parent = true //! //! # (Optional) If a subcrate has extract_to_parent set to true, then merge the //! # output pot file of that subcrate into this crate's pot file. //! collate_extracted_subcrates = true //! ``` #![doc(test( no_crate_inject, attr(deny(warnings, rust_2018_idioms, single_use_lifetimes)) ))] #![forbid(unsafe_code)] #![warn( missing_debug_implementations, missing_docs, rust_2018_idioms, single_use_lifetimes, unreachable_pub )] mod assets; mod requester; mod util; #[cfg(feature = "fluent-system")] pub mod fluent; #[cfg(feature = "gettext-system")] pub mod gettext; pub use assets::*; pub use requester::*; pub use util::*; #[cfg(doctest)] #[macro_use] extern crate doc_comment; #[cfg(all(doctest, feature = "desktop-requester", feature = "fluent-system"))] doctest!("../README.md"); #[cfg(any(feature = "gettext-system", feature = "fluent-system"))] #[allow(unused_imports)] #[macro_use] extern crate i18n_embed_impl; #[cfg(feature = "gettext-system")] extern crate gettext as gettext_system; use std::{ borrow::Cow, fmt::Debug, path::{Component, Path}, string::FromUtf8Error, }; use fluent_langneg::{negotiate_languages, NegotiationStrategy}; use log::{debug, error}; use thiserror::Error; pub use unic_langid; /// An error that occurs in this library. #[derive(Error, Debug)] #[allow(missing_docs)] pub enum I18nEmbedError { #[error("Error parsing a language identifier string \"{0}\"")] ErrorParsingLocale(String, #[source] unic_langid::LanguageIdentifierError), #[error("Error reading language file \"{0}\" as utf8.")] ErrorParsingFileUtf8(String, #[source] FromUtf8Error), #[error("The slice of requested languages cannot be empty.")] RequestedLanguagesEmpty, #[error("The language file \"{0}\" for the language \"{1}\" is not available.")] LanguageNotAvailable(String, unic_langid::LanguageIdentifier), #[error("There are multiple errors: {}", error_vec_to_string(.0))] Multiple(Vec), #[cfg(feature = "gettext-system")] #[error(transparent)] Gettext(#[from] gettext_system::Error), } fn error_vec_to_string(errors: &[I18nEmbedError]) -> String { let strings: Vec = errors.iter().map(|e| format!("{e}")).collect(); strings.join(", ") } /// This trait provides dynamic access to an /// [LanguageLoader](LanguageLoader) and an [I18nAssets](I18nAssets), /// which are used together to localize a library/crate on demand. pub trait Localizer { /// The [LanguageLoader] used by this localizer. fn language_loader(&self) -> &'_ dyn LanguageLoader; /// The source of localization assets used by this localizer fn i18n_assets(&self) -> &'_ dyn I18nAssets; /// The available languages that can be selected by this localizer. fn available_languages(&self) -> Result, I18nEmbedError> { self.language_loader() .available_languages(self.i18n_assets()) } /// Automatically the language currently requested by the system /// by the the [LanguageRequester](LanguageRequester)), and load /// it using the provided [LanguageLoader](LanguageLoader). fn select( &self, requested_languages: &[unic_langid::LanguageIdentifier], ) -> Result, I18nEmbedError> { select( self.language_loader(), self.i18n_assets(), requested_languages, ) } } /// 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, } impl Debug for DefaultLocalizer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "DefaultLocalizer(language_loader: {:p}, i18n_assets: {:p})", self.language_loader, self.i18n_assets, ) } } #[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 } } 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, i18n_assets, } } } /// 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]. /// Returns the available languages that were negotiated as being the /// most suitable to be selected, and were loaded by /// [LanguageLoader::load_languages()]. If there were no available /// languages, then no languages will be loaded and the returned /// `Vec` will be empty. pub fn select( language_loader: &dyn LanguageLoader, i18n_assets: &dyn I18nAssets, requested_languages: &[unic_langid::LanguageIdentifier], ) -> Result, I18nEmbedError> { debug!( "Selecting translations for domain \"{0}\"", language_loader.domain() ); let available_languages: Vec = language_loader.available_languages(i18n_assets)?; let default_language: &unic_langid::LanguageIdentifier = language_loader.fallback_language(); let supported_languages = negotiate_languages( requested_languages, &available_languages, Some(default_language), NegotiationStrategy::Filtering, ); debug!("Requested Languages: {:?}", requested_languages); debug!("Available Languages: {:?}", available_languages); debug!("Supported Languages: {:?}", supported_languages); if !supported_languages.is_empty() { language_loader.load_languages(i18n_assets, supported_languages.as_slice())?; } Ok(supported_languages.into_iter().cloned().collect()) } /// A language resource file, and its associated `language`. #[derive(Debug)] pub struct LanguageResource<'a> { /// The language which this resource is associated with. pub language: unic_langid::LanguageIdentifier, /// The data for the file containing the localizations. pub file: Cow<'a, [u8]>, } /// A trait used by [I18nAssets](I18nAssets) to load a language file for /// a specific rust module using a specific localization system. The /// trait is designed such that the loader could be swapped during /// runtime, or contain state if required. pub trait LanguageLoader { /// The fallback language for the module this loader is responsible /// for. fn fallback_language(&self) -> &unic_langid::LanguageIdentifier; /// The domain for the translation that this loader is associated with. 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>( &self, language_id: &unic_langid::LanguageIdentifier, i18n_assets: &'a dyn I18nAssets, ) -> (String, Option>) { 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) } /// Calculate the languages which are available to be loaded. fn available_languages( &self, i18n_assets: &dyn I18nAssets, ) -> Result, I18nEmbedError> { let mut language_strings: Vec = i18n_assets .filenames_iter() .filter_map(|filename| { let path: &Path = Path::new(&filename); let components: Vec> = path.components().collect(); let locale: Option = match components.get(0) { Some(Component::Normal(s)) => { Some(s.to_str().expect("path should be valid utf-8").to_string()) } _ => None, }; let language_file_name: Option = components.get(1).and_then(|component| match component { Component::Normal(s) => { Some(s.to_str().expect("path should be valid utf-8").to_string()) } _ => None, }); match language_file_name { Some(language_file_name) => { debug!( "Searching for available languages, found language file: \"{0}\"", &filename ); if language_file_name == self.language_file_name() { locale } else { None } } None => None, } }) .collect(); let fallback_locale = self.fallback_language().to_string(); // For systems such as gettext which have a locale in the // source code, this language will not be found in the // localization assets, and should be the fallback_locale, so // it needs to be added manually here. if !language_strings .iter() .any(|language| language == &fallback_locale) { language_strings.insert(0, fallback_locale); } language_strings .into_iter() .map(|language: String| { language .parse() .map_err(|err| I18nEmbedError::ErrorParsingLocale(language, err)) }) .collect() } /// 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::>()) } /// 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 /// the `language_ids` slice. You can use [select()] to determine /// which fallbacks are actually available for an arbitrary slice /// of preferences. fn load_languages( &self, 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. #[cfg(all(test, feature = "gettext-system"))] mod gettext_test_string { fn _test_strings() { tr::tr!("only en"); tr::tr!("only ru"); tr::tr!("only es"); tr::tr!("only fr"); } } i18n-embed-0.14.1/src/requester.rs000064400000000000000000000330601046102023000147160ustar 00000000000000use crate::{I18nEmbedError, Localizer}; use std::{collections::HashMap, sync::Weak}; /// A trait used by [I18nAssets](crate::I18nAssets) to ascertain which /// languages are being requested. 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. [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, listener: Weak); /// 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. /// /// **NOTE:** Support for this across systems currently /// varies, it may not change when the system requested language /// changes during runtime without restarting your application. In /// the future some platforms may also gain support for automatic /// triggering when the requested display language changes. fn poll(&mut self) -> Result<(), I18nEmbedError>; /// Override the languages fed to the [Localizer](Localizer) listeners during /// a [#poll()](#poll()). Set this as `None` to disable the override. fn set_language_override( &mut self, language_override: Option, ) -> Result<(), I18nEmbedError>; /// The currently requested languages. fn requested_languages(&self) -> Vec; /// The languages reported to be available in the /// listener [Localizer](Localizer)s. fn available_languages(&self) -> Result, I18nEmbedError>; /// The languages currently loaded, keyed by the /// [LanguageLoader::domain()](crate::LanguageLoader::domain()). fn current_languages(&self) -> HashMap; } /// Provide the functionality for overrides and listeners for a /// [LanguageRequester](LanguageRequester) implementation. pub struct LanguageRequesterImpl<'a> { arc_listeners: Vec>, ref_listeners: Vec<&'a dyn Localizer>, language_override: Option, } impl<'a> LanguageRequesterImpl<'a> { /// Create a new [LanguageRequesterImpl](LanguageRequesterImpl). pub fn new() -> LanguageRequesterImpl<'a> { LanguageRequesterImpl { arc_listeners: Vec::new(), ref_listeners: Vec::new(), language_override: None, } } /// Set an override for the requested language which is used when the /// [LanguageRequesterImpl#poll()](LanguageRequester#poll()) method /// is called. If `None`, then no override is used. pub fn set_language_override( &mut self, language_override: Option, ) -> Result<(), I18nEmbedError> { self.language_override = language_override; Ok(()) } /// Add a weak reference to a [Localizer], which listens to /// changes to the current language. pub fn add_listener(&mut self, listener: Weak) { 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 /// [Localizer#select()](Localizer#select()) on each of the /// listeners. pub fn poll_without_override( &mut self, requested_languages: Vec, ) -> Result<(), I18nEmbedError> { let mut errors: Vec = Vec::new(); 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, }); for boxed_listener in &self.ref_listeners { if let Err(error) = boxed_listener.select(&requested_languages) { errors.push(error); } } if errors.is_empty() { Ok(()) } else if errors.len() == 1 { Err(errors.into_iter().next().unwrap()) } else { Err(I18nEmbedError::Multiple(errors)) } } /// With the provided `requested_languages` call /// [Localizer#select()](Localizer#select()) on each of the /// listeners. The `requested_languages` may be ignored if /// [#set_language_override()](#set_language_override()) has been /// set. pub fn poll( &mut self, requested_languages: Vec, ) -> Result<(), I18nEmbedError> { let languages = match &self.language_override { Some(language) => { log::debug!("Using language override: {}", language); vec![language.clone()] } None => requested_languages, }; self.poll_without_override(languages) } /// The languages reported to be available in the /// listener [Localizer](Localizer)s. pub fn available_languages( &self, ) -> Result, I18nEmbedError> { let mut available_languages = std::collections::HashSet::new(); 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| { available_languages.insert(language.clone()); }) } } 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()) } /// Gets a `HashMap` with what each language is currently set /// (value) per domain (key). pub fn current_languages(&self) -> HashMap { let mut current_languages = HashMap::new(); 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()); } } current_languages } } impl Default for LanguageRequesterImpl<'_> { fn default() -> Self { Self::new() } } impl std::fmt::Debug for LanguageRequesterImpl<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let listeners_debug: String = self .arc_listeners .iter() .map(|l| match l.upgrade() { Some(l) => format!("{l:p}"), None => "None".to_string(), }) .collect::>() .join(", "); write!( f, "LanguageRequesterImpl(listeners: {}, language_override: {:?})", listeners_debug, self.language_override, ) } } /// A [LanguageRequester](LanguageRequester) for the desktop platform, /// supporting windows, linux and mac. It uses /// [locale_config](locale_config) to select the language based on the /// system selected language. /// /// ⚠️ *This API requires the following crate features to be activated: `desktop-requester`.* #[cfg(feature = "desktop-requester")] #[derive(Debug)] pub struct DesktopLanguageRequester<'a> { implementation: LanguageRequesterImpl<'a>, } #[cfg(feature = "desktop-requester")] impl<'a> LanguageRequester<'a> for DesktopLanguageRequester<'a> { fn requested_languages(&self) -> Vec { Self::requested_languages() } fn add_listener(&mut self, listener: Weak) { 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( &mut self, language_override: Option, ) -> Result<(), I18nEmbedError> { self.implementation.set_language_override(language_override) } fn poll(&mut self) -> Result<(), I18nEmbedError> { self.implementation.poll(self.requested_languages()) } fn available_languages(&self) -> Result, I18nEmbedError> { self.implementation.available_languages() } fn current_languages(&self) -> HashMap { self.implementation.current_languages() } } #[cfg(feature = "desktop-requester")] impl Default for DesktopLanguageRequester<'_> { fn default() -> Self { DesktopLanguageRequester::new() } } #[cfg(feature = "desktop-requester")] impl DesktopLanguageRequester<'_> { /// Create a new `DesktopLanguageRequester`. pub fn new() -> Self { DesktopLanguageRequester { implementation: LanguageRequesterImpl::new(), } } /// The languages being requested by the operating /// system/environment according to the [locale_config] crate's /// implementation. pub fn requested_languages() -> Vec { use locale_config::{LanguageRange, Locale}; let current_locale = Locale::current(); let ids: Vec = current_locale .tags_for("messages") .filter_map(|tag: LanguageRange<'_>| match tag.to_string().parse() { Ok(tag) => Some(tag), Err(err) => { log::error!("Unable to parse your locale: {:?}", err); None } }) .collect(); log::info!("Current Locale: {:?}", ids); ids } } /// A [LanguageRequester](LanguageRequester) for the `web-sys` web platform. /// /// ⚠️ *This API requires the following crate features to be activated: `web-sys-requester`.* #[cfg(feature = "web-sys-requester")] #[derive(Debug)] pub struct WebLanguageRequester<'a> { implementation: LanguageRequesterImpl<'a>, } #[cfg(feature = "web-sys-requester")] impl WebLanguageRequester<'_> { /// Create a new `WebLanguageRequester`. pub fn new() -> Self { WebLanguageRequester { implementation: LanguageRequesterImpl::new(), } } /// The languages currently being requested by the browser context. pub fn requested_languages() -> Vec { use fluent_langneg::convert_vec_str_to_langids_lossy; let window = web_sys::window().expect("no global `window` exists"); let navigator = window.navigator(); let languages = navigator.languages(); let requested_languages = convert_vec_str_to_langids_lossy(languages.iter().map(|js_value| { js_value .as_string() .expect("language value should be a string.") })); requested_languages } } #[cfg(feature = "web-sys-requester")] impl Default for WebLanguageRequester<'_> { fn default() -> Self { WebLanguageRequester::new() } } #[cfg(feature = "web-sys-requester")] impl<'a> LanguageRequester<'a> for WebLanguageRequester<'a> { fn requested_languages(&self) -> Vec { Self::requested_languages() } fn add_listener(&mut self, listener: Weak) { 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> { self.implementation.poll(self.requested_languages()) } fn set_language_override( &mut self, language_override: Option, ) -> Result<(), I18nEmbedError> { self.implementation.set_language_override(language_override) } fn available_languages(&self) -> Result, I18nEmbedError> { self.implementation.available_languages() } fn current_languages(&self) -> HashMap { self.implementation.current_languages() } } i18n-embed-0.14.1/src/util.rs000064400000000000000000000003161046102023000136520ustar 00000000000000/// Get the translation domain from the module path (first module in /// the module path). pub fn domain_from_module(module_path: &str) -> &str { module_path.split("::").next().unwrap_or(module_path) } i18n-embed-0.14.1/tests/loader.rs000064400000000000000000000315311046102023000145210ustar 00000000000000#[cfg(any(feature = "fluent-system", feature = "gettext-system"))] fn setup() { let _ = env_logger::try_init(); } #[cfg(feature = "fluent-system")] mod fluent { use super::setup; use fluent_langneg::NegotiationStrategy; use i18n_embed::{fluent::FluentLanguageLoader, LanguageLoader}; use rust_embed::RustEmbed; use unic_langid::LanguageIdentifier; #[derive(RustEmbed)] #[folder = "i18n/ftl"] struct Localizations; #[test] fn hello_world_en_us() { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); loader.load_languages(&Localizations, &[&en_us]).unwrap(); pretty_assertions::assert_eq!("Hello World Localization!", loader.get("hello-world")); } #[test] fn fallback_en_gb_to_en_us() { setup(); 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(); pretty_assertions::assert_eq!("Hello World Localisation!", loader.get("hello-world")); pretty_assertions::assert_eq!("only US", loader.get("only-us")); } #[test] fn fallbacks_ru_to_en_gb_to_en_us() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); 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, &[&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")); pretty_assertions::assert_eq!("только русский", loader.get("only-ru")); } #[test] fn args_fallback_ru_to_en_us() { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); loader.load_languages(&Localizations, &[&ru]).unwrap(); let args = maplit::hashmap! { "userName" => "Tanya" }; pretty_assertions::assert_eq!( "Привет \u{2068}Tanya\u{2069}!", loader.get_args("only-ru-args", args) ); } #[test] fn attr() { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); loader.load_languages(&Localizations, &[&en_us]).unwrap(); pretty_assertions::assert_eq!("World (US version)!", loader.get_attr("with-attr", "attr")); } #[test] fn attr_with_args() { setup(); 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(); let args = maplit::hashmap! { "name" => "Joe Doe" }; pretty_assertions::assert_eq!( "\u{2068}Joe Doe\u{2069}!", loader.get_attr_args("with-attr-and-args", "who", args) ); } #[test] fn has() { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); loader.load_languages(&Localizations, &[&ru]).unwrap(); assert!(loader.has("only-ru-args")); assert!(loader.has("only-us")); assert!(!loader.has("non-existent-message")) } #[test] fn bidirectional_isolation_off() { 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.set_use_isolating(false); let args = maplit::hashmap! { "thing" => "thing" }; let msg = loader.get_args("isolation-chars", args); assert_eq!("inject a thing here", msg); } #[test] fn bidirectional_isolation_on() { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); loader.load_languages(&Localizations, &[&en_us]).unwrap(); let args = maplit::hashmap! { "thing" => "thing" }; let msg = loader.get_args("isolation-chars", args); assert_eq!("inject a \u{2068}thing\u{2069} here", msg); } #[test] fn multiline_lf() { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); loader.load_languages(&Localizations, &[&en_us]).unwrap(); let msg = loader.get("multi-line"); assert_eq!( "This is a multi-line message.\n\n\ This is a multi-line message.\n\n\ Finished!", msg ); } #[test] fn multiline_crlf() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", ru.clone()); loader.load_languages(&Localizations, &[&ru]).unwrap(); let msg = loader.get("multi-line"); assert_eq!( "Это многострочное сообщение.\n\n\ Это многострочное сообщение.\n\n\ Законченный!", msg ); } #[test] fn multiline_arguments_lf() { setup(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us.clone()); loader.load_languages(&Localizations, &[&en_us]).unwrap(); let args = maplit::hashmap! { "argOne" => "1", "argTwo" => "2", }; let msg = loader.get_args("multi-line-args", args); assert_eq!( "This is a multiline message with arguments.\n\n\ \u{2068}1\u{2069}\n\n\ This is a multiline message with arguments.\n\n\ \u{2068}2\u{2069}\n\n\ Finished!", msg ); } #[test] fn multiline_arguments_crlf() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let loader = FluentLanguageLoader::new("test", ru.clone()); loader.load_languages(&Localizations, &[&ru]).unwrap(); let args = maplit::hashmap! { "argOne" => "1", "argTwo" => "2", }; let msg = loader.get_args("multi-line-args", args); assert_eq!( "Это многострочное сообщение с параметрами.\n\n\ \u{2068}1\u{2069}\n\n\ Это многострочное сообщение с параметрами.\n\n\ \u{2068}2\u{2069}\n\n\ Законченный!", msg ); } #[test] fn select_languages_get_default_fallback() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us); loader .load_languages(&Localizations, &[&ru, &en_gb]) .unwrap(); let msg = loader.select_languages(&[&ru]).get("only-ru"); assert_eq!("только русский", msg); let msg = loader.select_languages(&[&ru]).get("only-gb"); assert_eq!("only GB (US Version)", msg); } #[test] fn select_languages_get_args_default_fallback() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us); loader .load_languages(&Localizations, &[&ru, &en_gb]) .unwrap(); let args = maplit::hashmap! { "argOne" => "1", "argTwo" => "2", }; let msg = loader .select_languages(&[&ru]) .get_args("multi-line-args", args); assert_eq!( "Это многострочное сообщение с параметрами.\n\n\ \u{2068}1\u{2069}\n\n\ Это многострочное сообщение с параметрами.\n\n\ \u{2068}2\u{2069}\n\n\ Законченный!", msg ); } #[test] fn select_languages_get_custom_fallback() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us); loader .load_languages(&Localizations, &[&ru, &en_gb]) .unwrap(); let msg = loader.select_languages(&[&ru, &en_gb]).get("only-gb"); assert_eq!("only GB", msg); let msg = loader.select_languages(&[&ru, &en_gb]).get("only-us"); assert_eq!("only US", msg); } #[test] fn select_languages_get_args_custom_fallback() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us); loader .load_languages(&Localizations, &[&ru, &en_gb]) .unwrap(); let args = maplit::hashmap! { "userName" => "username", }; let msg = loader .select_languages(&[&ru]) .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]) .get_args("only-gb-args", args.clone()); assert_eq!("Hello \u{2068}username\u{2069}!", msg); } #[test] fn select_languages_negotiate() { setup(); let ru: LanguageIdentifier = "ru".parse().unwrap(); let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); let en_us: LanguageIdentifier = "en-US".parse().unwrap(); let loader = FluentLanguageLoader::new("test", en_us); loader.load_available_languages(&Localizations).unwrap(); let msg = loader .select_languages_negotiate(&[&ru, &en_gb], NegotiationStrategy::Filtering) .get("only-gb-us"); assert_eq!("only GB US (GB)", msg); } } #[cfg(feature = "gettext-system")] mod gettext { use super::setup; use i18n_embed::{gettext::GettextLanguageLoader, LanguageLoader}; use rust_embed::RustEmbed; use serial_test::serial; use tr::internal::with_translator; use unic_langid::LanguageIdentifier; /// Custom version of the tr! macro function, without the runtime /// formatting, with the module set to `i18n_embed` where the /// strings were originally extracted from. fn tr(msgid: &str) -> String { with_translator("i18n_embed", |t| t.translate(msgid, None).to_string()) } #[derive(RustEmbed)] #[folder = "i18n/mo"] struct Localizations; #[test] #[serial] fn only_en() { setup(); let loader = GettextLanguageLoader::new("i18n_embed", "en".parse().unwrap()); let ru: LanguageIdentifier = "ru".parse().unwrap(); let en: LanguageIdentifier = "en".parse().unwrap(); loader.load_languages(&Localizations, &[&ru]).unwrap(); // It should replace the ru with en loader.load_languages(&Localizations, &[&en]).unwrap(); pretty_assertions::assert_eq!("only en", tr("only en")); pretty_assertions::assert_eq!("only ru", tr("only ru")); } #[test] #[serial] fn fallback_ru_to_en() { setup(); let loader = GettextLanguageLoader::new("i18n_embed", "en".parse().unwrap()); let ru: LanguageIdentifier = "ru".parse().unwrap(); assert!(Localizations::get("ru/i18n_embed.mo").is_some()); loader.load_languages(&Localizations, &[&ru]).unwrap(); pretty_assertions::assert_eq!("только ру", tr("only ru")); pretty_assertions::assert_eq!("only en", tr("only en")); } }