├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── i18n-build ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── i18n.toml ├── i18n │ ├── TRANSLATORS │ ├── mo │ │ ├── fr │ │ │ └── i18n_build.mo │ │ └── ru │ │ │ └── i18n_build.mo │ └── po │ │ ├── fr │ │ └── i18n_build.po │ │ └── ru │ │ └── i18n_build.po └── src │ ├── error.rs │ ├── gettext_impl │ └── mod.rs │ ├── lib.rs │ ├── util.rs │ └── watch.rs ├── i18n-config ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── README.md └── src │ ├── fluent.rs │ ├── gettext.rs │ └── lib.rs ├── i18n-embed-fl ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── examples │ └── web-server │ │ ├── Cargo.toml │ │ ├── i18n.toml │ │ ├── i18n │ │ ├── de-AT │ │ │ └── web_server.ftl │ │ ├── de-DE │ │ │ └── web_server.ftl │ │ ├── en-GB │ │ │ └── web_server.ftl │ │ ├── en-US │ │ │ └── web_server.ftl │ │ └── ka-GE │ │ │ └── web_server.ftl │ │ └── src │ │ └── main.rs ├── i18n.toml ├── i18n │ └── en-US │ │ └── i18n_embed_fl.ftl ├── src │ └── lib.rs └── tests │ └── fl_macro.rs ├── i18n-embed ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── examples │ ├── desktop-bin │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ └── main.rs │ └── library-fluent │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── i18n.toml │ │ ├── i18n │ │ ├── en │ │ │ └── library_fluent.ftl │ │ ├── eo │ │ │ └── library_fluent.ftl │ │ └── fr │ │ │ └── library_fluent.ftl │ │ ├── src │ │ └── lib.rs │ │ └── tests │ │ ├── no_localizer.rs │ │ └── with_localizer.rs ├── i18n-embed-impl │ ├── .gitignore │ ├── Cargo.toml │ ├── LICENSE.txt │ └── src │ │ └── lib.rs ├── i18n.toml ├── i18n │ ├── ftl │ │ ├── en-GB │ │ │ └── test.ftl │ │ ├── en-US │ │ │ ├── .gitattributes │ │ │ └── test.ftl │ │ └── ru │ │ │ ├── .gitattributes │ │ │ └── test.ftl │ ├── mo │ │ ├── README.md │ │ ├── es │ │ │ └── i18n_embed.mo │ │ ├── fr │ │ │ └── i18n_embed.mo │ │ └── ru │ │ │ └── i18n_embed.mo │ └── po │ │ ├── es │ │ └── i18n_embed.po │ │ ├── fr │ │ └── i18n_embed.po │ │ └── ru │ │ └── i18n_embed.po ├── src │ ├── assets.rs │ ├── fluent.rs │ ├── gettext.rs │ ├── lib.rs │ ├── requester.rs │ └── util.rs └── tests │ └── loader.rs ├── i18n.toml ├── i18n ├── TRANSLATORS ├── mo │ ├── de │ │ └── cargo_i18n.mo │ ├── fr │ │ └── cargo_i18n.mo │ └── ru │ │ └── cargo_i18n.mo └── po │ ├── de │ └── cargo_i18n.po │ ├── fr │ └── cargo_i18n.po │ └── ru │ └── cargo_i18n.po └── src ├── lib.rs └── main.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | test: 10 | name: Test Suite 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: nightly 18 | override: true 19 | - uses: actions-rs/cargo@v1 20 | with: 21 | command: test 22 | args: --all --all-features -- --test-threads 1 23 | 24 | fmt: 25 | name: Rustfmt 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: stable 33 | override: true 34 | - run: rustup component add rustfmt 35 | - uses: actions-rs/cargo@v1 36 | with: 37 | command: fmt 38 | args: --all -- --check 39 | 40 | clippy: 41 | name: Clippy 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v1 45 | - uses: actions-rs/toolchain@v1 46 | with: 47 | toolchain: nightly 48 | components: clippy 49 | override: true 50 | # Note that there is no release tag available yet 51 | # and the following code will use master branch HEAD 52 | # all the time. 53 | - uses: actions-rs/clippy@master 54 | with: 55 | args: --all-features 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode 4 | /i18n/pot 5 | .neoconf.json 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Luke Frisken "] 3 | categories = ["development-tools::cargo-plugins", "localization", "internationalization", "development-tools::build-utils"] 4 | description = "Cargo sub-command to extract and build localization resources to embed in your application/library" 5 | edition = "2018" 6 | keywords = ["cargo", "build", "i18n", "gettext", "locale"] 7 | license = "MIT" 8 | name = "cargo-i18n" 9 | readme = "README.md" 10 | repository = "https://github.com/kellpossible/cargo-i18n" 11 | version = "0.2.13" 12 | 13 | [badges] 14 | maintenance = { status = "actively-developed" } 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | i18n-embed = { workspace = true, features = ["desktop-requester", "gettext-system", "fluent-system"] } 20 | i18n-build = { workspace = true, features = ["localize"] } 21 | i18n-config = { workspace = true } 22 | anyhow = { workspace = true } 23 | gettext = { workspace = true } 24 | tr = { workspace = true, features = ["gettext"] } 25 | clap = { version = "4.4.5", features = ["cargo"] } 26 | rust-embed = { workspace = true } 27 | unic-langid = { workspace = true } 28 | env_logger = { workspace = true } 29 | log = { workspace = true } 30 | 31 | [dev-dependencies] 32 | doc-comment = { workspace = true } 33 | 34 | [workspace] 35 | 36 | members = [ 37 | "i18n-build", 38 | "i18n-config", 39 | "i18n-embed", 40 | "i18n-embed/i18n-embed-impl", 41 | "i18n-embed-fl", 42 | 43 | # Examples 44 | "i18n-embed/examples/library-fluent", 45 | "i18n-embed/examples/desktop-bin", 46 | "i18n-embed-fl/examples/web-server", 47 | ] 48 | 49 | [workspace.dependencies] 50 | rust-embed = "8.0" 51 | i18n-build = { version = "0.10.0", path = "./i18n-build" } 52 | i18n-embed = { version = "0.15.4", path = "./i18n-embed" } 53 | i18n-embed-impl = { version = "0.8.4", path = "./i18n-embed/i18n-embed-impl" } 54 | i18n-config = { version = "0.4.7", path = "./i18n-config" } 55 | i18n-embed-fl = { version = "0.9.1", path = "./i18n-embed-fl" } 56 | thiserror = "1.0" 57 | log = "0.4" 58 | unic-langid = "0.9" 59 | anyhow = "1.0" 60 | gettext = "0.4" 61 | tr = "0.1" 62 | doc-comment = "0.3" 63 | env_logger = "0.11" 64 | fluent = "0.16" 65 | fluent-syntax = "0.11" 66 | fluent-langneg = "0.13" 67 | proc-macro2 = "1.0" 68 | quote = "1.0" 69 | find-crate = "0.6" 70 | syn = "2.0" 71 | pretty_assertions = "1.4" 72 | walkdir = "2.4" 73 | serde = "1.0" 74 | serde_derive = "1.0" 75 | once_cell = "1.18" 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Luke Frisken 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-i18n [![crates.io badge](https://img.shields.io/crates/v/cargo-i18n.svg)](https://crates.io/crates/cargo-i18n) [![license badge](https://img.shields.io/github/license/kellpossible/cargo-i18n)](https://github.com/kellpossible/cargo-i18n/blob/master/LICENSE) [![github actions badge](https://github.com/kellpossible/cargo-i18n/workflows/Rust/badge.svg)](https://github.com/kellpossible/cargo-i18n/actions?query=workflow%3ARust) [![dependency status](https://deps.rs/repo/github/kellpossible/cargo-i18n/status.svg)](https://deps.rs/repo/github/kellpossible/cargo-i18n) 2 | 3 | This crate is a Cargo sub-command `cargo i18n` which can be used to extract and 4 | build, and verify localization resources at compile time for your crate. The 5 | [i18n-embed](https://crates.io/crates/i18n-embed) library has been created to 6 | allow you to conveniently embed these localizations into your application or 7 | library, and have them selected at runtime. Different systems can be used simultaneously. 8 | 9 | `i18n-embed` supports both the following localization systems: 10 | 11 | + [fluent](https://www.projectfluent.org/) 12 | + [gettext](https://www.gnu.org/software/gettext/) 13 | 14 | You can install this tool using the command: `cargo install cargo-i18n`. 15 | 16 | The `cargo i18n` command reads the configuration file (by default called `i18n.toml`) in the root directory of your crate, and then proceeds to extract localization resources from your source files, and build them. 17 | 18 | The [i18n-build](https://crates.io/crates/i18n-build) library contains most of the implementation for this tool. It has been published separately to allow its direct use within project build scripts if required. 19 | 20 | **[Changelog](https://github.com/kellpossible/cargo-i18n/releases)** 21 | 22 | ## Projects Using `cargo-i18n` or `i18n-embed` 23 | 24 | + The [source code](https://github.com/kellpossible/cargo-i18n/) for this project, which localizes itself. 25 | + [`age` file encryption](https://github.com/str4d/rage/tree/main/age) 26 | + [`avalanche-report`](https://github.com/kellpossible/avalanche-report) 27 | + [`coster` (work in progress)](https://github.com/kellpossible/coster) self-hosted web application. 28 | 29 | ## Usage with Fluent 30 | 31 | Fluent support is now available in [i18n-embed](https://crates.io/crates/i18n-embed). See the examples for that crate, and the [documentation for i18n-embed](https://docs.rs/i18n-embed/) for example of how to use it. 32 | 33 | See also the [fl!() macro](https://crates.io/crates/i18n-embed-fl) for a convenient compile-time checked way to access fluent messages when using `i18n-embed`. 34 | 35 | Currently there are no validations performed by the `cargo-i18n` tool when using the `fluent` localization system, however there are some planned (see tracking issue [#31](https://github.com/kellpossible/cargo-i18n/issues/31)). If you have any more ideas for this, please feel free to contribute to the issue discussion. 36 | 37 | ## Usage with Gettext 38 | 39 | This is an example for how to use the `cargo-i18n` tool, and `i18n-embed` the `gettext` localization tool 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 more mature. 40 | 41 | Firstly, ensure you have the required utilities installed on your system. See [Gettext System Requirements](#Gettext-Requirements) and install the necessary utilities and commands for the localization system you will be using. 42 | 43 | ### Defining Localized Strings 44 | 45 | You will need to ensure that your strings in your source code that you want localized are using the `tr!()` macro from the [tr](https://crates.io/crates/tr) crate. 46 | 47 | You can add comments to your strings which will be available to translators to add context, and ensure that they understand what the string is for. 48 | 49 | For example: 50 | 51 | ```rust 52 | use tr::tr; 53 | 54 | fn example(file: String) { 55 | let my_string = tr!( 56 | // {0} is a file path 57 | // Example message: Printing this file: "file.doc" 58 | "Printing this file: \"{0}\"", 59 | file 60 | ); 61 | } 62 | ``` 63 | 64 | ### Minimal Configuration 65 | 66 | You will need to create an `i18n.toml` configuration in the root directory of your crate. A minimal configuration for a binary crate to be localized to Spanish and Japanese using the `gettext` system would be: 67 | 68 | ```toml 69 | # (Required) The language identifier of the language used in the 70 | # source code for gettext system, and the primary fallback language 71 | # (for which all strings must be present) when using the fluent 72 | # system. 73 | fallback_language = "en" 74 | 75 | [gettext] 76 | # (Required) The languages that the software will be translated into. 77 | target_languages = ["es", "ja"] 78 | 79 | # (Required) Path to the output directory, relative to `i18n.toml` of the crate 80 | # being localized. 81 | output_dir = "i18n" 82 | ``` 83 | 84 | See [Configuration](#Configuration) for a description of all the available configuration options. 85 | 86 | ### Running `cargo i18n` 87 | 88 | Open your command line/terminal and navigate to your crate directory, and run `cargo i18n`. You may be prompted to enter some email addresses to use for contact points for each of the language's `po` files. At the end there should be a new directory in your crate called `i18n`, and inside will be `pot`, `po` and `mo` directories. 89 | 90 | The `pot` directory contains `pot` files which were extracted from your source code using the `xtr` tool, and there should be a single `pot` file with the name of your crate in here too, which is the result of merging all the other `pot` files. 91 | 92 | The `po` directory contains the language specific message files. 93 | 94 | The `mo` directory contains the compiled messages, which will later be embedded into your application. 95 | 96 | At this point it could be a good idea to add the following to your crate's `.gitignore` (if you are using git): 97 | 98 | ```gitignore 99 | /i18n/pot 100 | /i18n/mo 101 | ``` 102 | 103 | If you want your crate to be able to build without requiring this tool to be present on the system, then you can leave the `/i18n/mo` directory out of the `.gitignore`, and commit the files inside. 104 | 105 | ### Embedding Translations 106 | 107 | Now that you have compiled your translations, you can embed them within your application. For this purpose the [i18n-embed](https://crates.io/crates/i18n-embed) crate was created. 108 | 109 | Add the following to your `Cargo.toml` dependencies: 110 | 111 | ```toml 112 | [dependencies] 113 | i18n-embed = "VERSION" 114 | ``` 115 | 116 | A minimal example for how to embed the compiled translations into your application could be: 117 | 118 | ```rust 119 | use i18n_embed::{DesktopLanguageRequester, 120 | gettext::gettext_language_loader}; 121 | use rust_embed::RustEmbed; 122 | 123 | #[derive(RustEmbed)] 124 | #[folder = "i18n/mo"] // path to the compiled localization resources 125 | struct Translations; 126 | 127 | fn main() { 128 | let translations = Translations {}; 129 | let language_loader = gettext_language_loader!(); 130 | 131 | // Use the language requester for the desktop platform (linux, windows, mac). 132 | // There is also a requester available for the web-sys WASM platform called 133 | // WebLanguageRequester, or you can implement your own. 134 | let requested_languages = DesktopLanguageRequester::requested_languages(); 135 | 136 | i18n_embed::select(&language_loader, &translations, &requested_languages); 137 | 138 | // continue with your application 139 | } 140 | ``` 141 | 142 | You can see the [i18n-embed documentation](https://docs.rs/i18n-embed/) for more detailed examples of how this library can be used. 143 | 144 | ### Distributing to Translators 145 | 146 | Now you need to send of the `po` files to your translators, or provide them access to edit them. Some desktop tools which can be used for the translation include: 147 | 148 | + [poedit](https://poedit.net/) 149 | + [Qt Linguist](https://doc.qt.io/qt-5/linguist-translators.html) ([Windows build](https://github.com/thurask/Qt-Linguist)) 150 | 151 | Or you could also consider setting up a translation management website for your project to allow translators to edit translations without requiring them to interact with source control or mess around with sending files and installing applications. Some examples: 152 | 153 | **Self Hosted:** 154 | 155 | + [pootle](https://pootle.translatehouse.org/) 156 | + [weblate](https://weblate.org/) - also a cloud offering. 157 | 158 | **Cloud:** 159 | 160 | + [poeditor](https://poeditor.com/projects/) - free for open source projects, currently being used for this project. 161 | + [crowdin](https://crowdin.com/) - free for popular open source projects. 162 | 163 | ### Updating Translations 164 | 165 | Once you have some updated `po` files back from translators, or you want to update the `po` files with new or edited strings, all you need to do is run `cargo i18n` to update the `po` files, and recompile updated `mo` files, then rebuild your application with `cargo build`. 166 | 167 | For some projects using build scripts, with complex pipelines, and with continuous integration, you may want to look into using the [i18n-build](https://crates.io/crates/i18n-build) for automation as an alternative to the `cargo i18n` command line tool. 168 | 169 | ## Configuration 170 | 171 | Available configuration options for `i18n.toml`: 172 | 173 | ```toml 174 | # (Required) The language identifier of the language used in the 175 | # source code for gettext system, and the primary fallback language 176 | # (for which all strings must be present) when using the fluent 177 | # system. 178 | fallback_language = "en-US" 179 | 180 | # (Optional) Specify which subcrates to perform localization within. If the 181 | # subcrate has its own `i18n.toml` then, it will have its localization 182 | # performed independently (rather than being incorporated into the parent 183 | # project's localization). 184 | subcrates = ["subcrate1", "subcrate2"] 185 | 186 | # (Optional) Use the gettext localization system. 187 | [gettext] 188 | # (Required) The languages that the software will be translated into. 189 | target_languages = ["es", "ru", "cz"] 190 | 191 | # (Required) Path to the output directory, relative to `i18n.toml` of the crate 192 | # being localized. 193 | output_dir = "i18n" 194 | 195 | # (Optional) The reporting address for msgid bugs. This is the email address or 196 | # URL to which the translators shall report bugs in the untranslated 197 | # strings. 198 | msg_bugs_address = "example@example.com" 199 | 200 | # (Optional) Set the copyright holder for the generated files. 201 | copyright_holder = "You?" 202 | 203 | # (Optional) If this crate is being localized as a subcrate, store the final 204 | # localization artifacts (the module pot and mo files) with the parent crate's 205 | # output. Currently crates which contain subcrates with duplicate names are not 206 | # supported. By default this is false. 207 | extract_to_parent = false 208 | 209 | # (Optional) If a subcrate has extract_to_parent set to true, then merge the 210 | # output pot file of that subcrate into this crate's pot file. By default this 211 | # is false. 212 | collate_extracted_subcrates = false 213 | 214 | # (Optional) How much message location information to include in the output. 215 | # If the type is ‘full’ (the default), it generates the lines with both file 216 | # name and line number: ‘#: filename:line’. If it is ‘file’, the line number 217 | # part is omitted: ‘#: filename’. If it is ‘never’, nothing is generated. 218 | # [possible values: full, file, never]. 219 | add_location = "full" 220 | 221 | # (Optional) Whether or not to perform string extraction using the `xtr` tool. 222 | xtr = true 223 | 224 | # (Optional )Path to where the pot files will be written to by `xtr` command, 225 | # and were they will be read from by the `msginit` and `msgmerge` commands. By 226 | # default this is `output_dir/pot`. 227 | pot_dir = "i18n/pot" 228 | 229 | # (Optional) Path to where the po files will be stored/edited with the 230 | # `msgmerge` and `msginit` commands, and where they will be read from with the 231 | # `msgfmt` command. By default this is `output_dir/po`. 232 | po_dir = "i18n/po" 233 | 234 | # (Optional) Path to where the mo files will be written to by the `msgfmt` 235 | # command. By default this is `output_dir/mo`. 236 | mo_dir = "i18n/mo" 237 | 238 | # (Optional) Enable the `--use-fuzzy` option for the `msgfmt` command. By 239 | # default this is false. If your .po file are copied from another project, you 240 | # may need to enable it. 241 | use_fuzzy = false 242 | 243 | # (Optional) Use the fluent localization system. 244 | [fluent] 245 | # (Required) The path to the assets directory. 246 | # The paths inside the assets directory should be structured like so: 247 | # `assets_dir/{language}/{domain}.ftl` 248 | assets_dir = "i18n" 249 | ``` 250 | 251 | ## System Requirements 252 | 253 | ### Gettext Requirements 254 | 255 | Using the `gettext` localization system with this tool requires you to have [gettext](https://www.gnu.org/software/gettext/) installed on your system. 256 | 257 | The [`msginit`](https://www.gnu.org/software/gettext/manual/html_node/msginit-Invocation.html), [`msgfmt`](https://www.gnu.org/software/gettext/manual/html_node/msgfmt-Invocation.html), [`msgmerge`](https://www.gnu.org/software/gettext/manual/html_node/msgmerge-Invocation.html) and [`msgcat`](https://www.gnu.org/software/gettext/manual/html_node/msgcat-Invocation.html) commands all need to be installed and present in your path. 258 | 259 | You also need to ensure that you have the [xtr](https://crates.io/crates/xtr) 260 | string extraction command installed, which can be achieved using `cargo install 261 | xtr`. 262 | 263 | ## Contributing 264 | 265 | Pull-requests are welcome, but for design changes it is preferred that you create a [GitHub issue](https://github.com/kellpossible/cargo-i18n/issues) first to discuss it before implementation. You can also contribute to the localization of this tool via: 266 | 267 | + [POEditor - cargo-i18n](https://poeditor.com/join/project/J7NiRCGpXa) 268 | + [POEditor - i18n-build](https://poeditor.com/join/project/BCW39cVoco) 269 | 270 | Or you can also use your favourite `po` editor directly to help with localizing the files located in [i18n/po](./i18n/po) and [i18n-build/i18n/po](./i18n-build/i18n/po). 271 | 272 | To add a new language, you can make a request via a GitHub issue, or submit a pull request adding the new locale to [i18n.toml](https://github.com/kellpossible/cargo-i18n/blob/master/i18n.toml) and generating the associated new `po` files using `cargo i18n`. 273 | 274 | Translations of this [README.md](./README.md) file are also welcome, and can be submitted via pull request. Just name it `README.md.lang`, where `lang` is the locale code (see [List of ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)). 275 | 276 | ## Authors 277 | 278 | + [Contributors](https://github.com/kellpossible/cargo-i18n/graphs/contributors) 279 | + [Translators](https://github.com/kellpossible/cargo-i18n/blob/master/i18n/TRANSLATORS) 280 | -------------------------------------------------------------------------------- /i18n-build/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode 4 | /i18n/pot 5 | Cargo.lock -------------------------------------------------------------------------------- /i18n-build/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `i18n-build` 2 | 3 | ## v0.10.1 4 | 5 | ### Internal 6 | 7 | + Removed dependency on `lazy_static` #135 thanks to [@mrtryhard](https://github.com/mrtryhard). 8 | 9 | ## v0.10.0 10 | 11 | ### Internal 12 | 13 | + Fix clippy warnings. 14 | + Bump dependencies and use workspace dependencies. 15 | 16 | ### Breaking 17 | 18 | + Update i18n-embed to version `0.15.0`. 19 | 20 | ## v0.8.2 21 | 22 | + Update `rust-embed` to `6.3` to address [RUSTSEC-2021-0126](https://rustsec.org/advisories/RUSTSEC-2021-0126.html). 23 | 24 | ## v0.8.1 25 | 26 | + French translations provided by Christophe Chauvet. 27 | 28 | ## v0.8.0 29 | 30 | + Update `i18n-embed` to version `0.13`. 31 | + Update `rust-embed` to version `6`. 32 | 33 | ## v0.7.0 34 | 35 | + Update `i18n-embed` to version `0.12`. 36 | 37 | ## v0.6.1 38 | 39 | ### New Features 40 | 41 | + Add the `use_fuzzy` option for the `gettext` system. [#68](https://github.com/kellpossible/cargo-i18n/pull/68) thanks to [@vkill](https://github.com/vkill). 42 | + Update to `i18n-embed` version `0.12`. 43 | 44 | ## v0.6.0 45 | 46 | + Update to `i18n-embed` version `0.11`. 47 | 48 | ### Internal Changes 49 | 50 | + Fix clippy warnings. 51 | 52 | ## v0.5.4 53 | 54 | + Update `i18n-embed` to version `0.10`. 55 | 56 | ## v0.5.3 57 | 58 | + Update `i18n-embed` to version `0.9.0`. 59 | 60 | ## v0.5.2 61 | 62 | + Update to `i18n-config` version `0.4.0`. 63 | + Update to `i18n-embed` version `0.8.0`. 64 | 65 | ## v0.5.1 66 | 67 | ## Bug Fixes 68 | 69 | + Fix broken build by enabling `gettext-system` for `i18n-embed` dependency. 70 | 71 | ## v0.5.0 72 | 73 | Changes for the support of the `fluent` localization system. 74 | 75 | ### Breaking Changes 76 | 77 | + Update to `i18n-embed` version `0.7.0`, contains breaking changes to API. 78 | + 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. 79 | 80 | ## v0.4.1 81 | 82 | + Update to `i18n-embed` version `0.6.0`. 83 | 84 | ## v0.4.0 85 | 86 | + Update to `i18n-embed` version `0.5.0`. 87 | + Change `localizer()` method to return `DefaultLocalizer` instead of the boxed trait `Box>`. 88 | 89 | ## v0.3.1 90 | 91 | + Update to `i18n-embed` version `0.4.0`. 92 | 93 | ## v0.3.0 94 | 95 | + Add support for `xtr` `add-location` option. 96 | + Requires `xtr` version `0.1.5`. 97 | + Suppress progress output for `msgmerge` using `--silent`. 98 | 99 | ## v0.2.1 100 | 101 | + Updated link to this changelog in the crate README. 102 | 103 | ## v0.2.0 104 | 105 | + Bump `i18n-config` version to `0.2`. 106 | + Handle the situation correctly where the `run()` is called on a crate which is not the root crate, and which makes use of the `gettext` `extract_to_parent` option. This solves [issue 13](https://github.com/kellpossible/cargo-i18n/issues/13). 107 | + Altered the signature of the `run()` method to take the `Crate` by value. 108 | -------------------------------------------------------------------------------- /i18n-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Luke Frisken "] 3 | categories = ["localization", "internationalization", "development-tools::build-utils"] 4 | description = "Designed for use within the cargo-i18n tool for localizing crates. It has been published to allow its direct use within project build scripts if required." 5 | edition = "2018" 6 | keywords = ["script", "build", "i18n", "gettext", "locale"] 7 | license = "MIT" 8 | name = "i18n-build" 9 | readme = "README.md" 10 | repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-build" 11 | version = "0.10.1" 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [badges] 19 | maintenance = { status = "actively-developed" } 20 | 21 | [dependencies] 22 | subprocess = "0.2" 23 | anyhow = { workspace = true } 24 | thiserror = { workspace = true } 25 | tr = { workspace = true, default-features = false, features = ["gettext"] } 26 | walkdir = { workspace = true } 27 | i18n-embed = { workspace = true, features = ["gettext-system", "desktop-requester"], optional = true } 28 | i18n-config = { workspace = true } 29 | gettext = { workspace = true, optional = true } 30 | log = { workspace = true } 31 | rust-embed = { workspace = true } 32 | 33 | [features] 34 | default = [] 35 | 36 | # A feature to localize this library 37 | localize = ["i18n-embed", "gettext"] 38 | -------------------------------------------------------------------------------- /i18n-build/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Luke Frisken 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | © 2020 Luke Frisken -------------------------------------------------------------------------------- /i18n-build/README.md: -------------------------------------------------------------------------------- 1 | # i18n-build [![crates.io badge](https://img.shields.io/crates/v/i18n-build.svg)](https://crates.io/crates/i18n-build) [![docs.rs badge](https://docs.rs/i18n-build/badge.svg)](https://docs.rs/i18n-build/) [![license badge](https://img.shields.io/github/license/kellpossible/cargo-i18n)](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-build/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) 2 | 3 | A library for use within the [cargo-i18n](https://crates.io/crates/cargo_i18n) tool for localizing crates. It has been published to allow its direct use within project build scripts if required. 4 | 5 | **[Changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-build/CHANGELOG.md)** 6 | 7 | ## Optional Features 8 | 9 | The `i18n-build` crate has the following optional Cargo features: 10 | 11 | + `localize` 12 | + Enables the runtime localization of this library using `localize()` function via the [i18n-embed](https://crates.io/crates/i18n-embed) crate. 13 | 14 | ## Contributing 15 | 16 | Pull-requests are welcome, but for design changes it is preferred that you create a GitHub issue first to discuss it before implementation. You can also contribute to the localization of this library via [POEditor - i18n-build](https://poeditor.com/join/project/BCW39cVoco) or use your favourite `po` editor. 17 | 18 | To add a new language, you can make a request via a GitHub issue, or submit a pull request adding the new locale to [i18n.toml](https://github.com/kellpossible/cargo-i18n/blob/master/i18n.toml) and generating the associated new po files using `cargo i18n`. 19 | 20 | ## Authors 21 | 22 | + [Contributors](https://github.com/kellpossible/cargo-i18n/graphs/contributors) 23 | + [Translators](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-build/i18n/TRANSLATORS) -------------------------------------------------------------------------------- /i18n-build/i18n.toml: -------------------------------------------------------------------------------- 1 | # (Required) The language identifier of the language used in the 2 | # source code for gettext system, and the primary fallback language 3 | # (for which all strings must be present) when using the fluent 4 | # system. 5 | fallback_language = "en-US" 6 | 7 | # (Optional) Use the gettext localization system. 8 | [gettext] 9 | # (Required) The languages that the software will be translated into. 10 | target_languages = ["ru", "fr"] 11 | 12 | # (Required) Path to the output directory, relative to `i18n.toml` of 13 | # the crate being localized. 14 | output_dir = "i18n" 15 | 16 | # (Optional) The reporting address for msgid bugs. This is the email 17 | # address or URL to which the translators shall report bugs in the 18 | # untranslated strings. 19 | msg_bugs_address = "l.frisken@gmail.com" 20 | 21 | # (Optional) Set the copyright holder for the generated files. 22 | copyright_holder = "Luke Frisken" -------------------------------------------------------------------------------- /i18n-build/i18n/TRANSLATORS: -------------------------------------------------------------------------------- 1 | # This file lists all PUBLIC individuals having contributed content to the translation. 2 | # Entries are in alphabetical order. 3 | 4 | Anna Abramova 5 | Christophe Chauvet -------------------------------------------------------------------------------- /i18n-build/i18n/mo/fr/i18n_build.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n-build/i18n/mo/fr/i18n_build.mo -------------------------------------------------------------------------------- /i18n-build/i18n/mo/ru/i18n_build.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n-build/i18n/mo/ru/i18n_build.mo -------------------------------------------------------------------------------- /i18n-build/i18n/po/fr/i18n_build.po: -------------------------------------------------------------------------------- 1 | # French translations for i18n-build package. 2 | # Copyright (C) 2021 Luke Frisken 3 | # This file is distributed under the same license as the i18n-build package. 4 | # , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: i18n-build 0.8.0\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-29 17:22+0000\n" 11 | "PO-Revision-Date: 2021-11-29 20:22+0300\n" 12 | "Last-Translator: \n" 13 | "Language-Team: French \n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #. {0} is the file path, {1} is the item which it is for, {2} is the type of item (file, directory, etc) 21 | #: i18n-build/src/error.rs:140 22 | msgid "The path (\"{0}\") for {1} {2} does not have valid a utf-8 encoding." 23 | msgstr "Le chemin (\"{0}\") pour {1} {2} n'a pas un encodage utf-8 valide." 24 | 25 | #. {0} can be either "file", or "directory", or "symlink" 26 | #. {1} is a file path 27 | #. {2} is more detailed information about the error 28 | #. Example: Cannot create the file "i18n/ru/something.pot" because "some error occurred" 29 | #: i18n-build/src/error.rs:155 30 | msgid "Cannot create the {0} \"{1}\" because: \"{2}\"." 31 | msgstr "Impossible de créer le {0} \"{1}\" car : \"{2}\"." 32 | 33 | #. {0} can be either "file", or "directory", or "symlink" 34 | #. {1} is a file path 35 | #. {2} is more detailed information about the error 36 | #. Example: Cannot delete the file "i18n/ru/something.pot" because "some error occurred" 37 | #: i18n-build/src/error.rs:165 38 | msgid "Cannot delete the {0} \"{1}\" because: \"{2}\"." 39 | msgstr "Impossible de supprimer le {0} \"{1}\" car : \"{2}\"." 40 | 41 | #. {0} can be either "file", or "directory", or "symlink" 42 | #. {1} is the name of the file to be renamed 43 | #. {2} is the new file name 44 | #. {3} is more detailed information about the error 45 | #. Example: Cannot rename the file "old.pot" to "new.pot" because "some error occurred" 46 | #: i18n-build/src/error.rs:176 47 | msgid "Cannot rename the {0} \"{1}\" to \"{2}\" because {3}." 48 | msgstr "Impossible de renommer le {0} \"{1}\" en \"{2}\" car {3}." 49 | 50 | #: i18n-build/src/gettext_impl/mod.rs:208 51 | msgid "There was a problem executing the \"{0}\" command" 52 | msgstr "Il y a eu un problème lors de l'exécution de la commande \"{0}\"" 53 | 54 | #: i18n-build/src/gettext_impl/mod.rs:432 55 | msgid "There was a problem parsing one of the subcrates: \"{0}\"." 56 | msgstr "Un problème est survenu lors de l'analyse de l'une des sous-caisse : \"{0}\"." 57 | 58 | #: i18n-build/src/util.rs:17 59 | msgid "The \"{0}\" command was unable to start." 60 | msgstr "La commande \"{0}\" n'a pas pu démarrer." 61 | 62 | #: i18n-build/src/util.rs:21 63 | msgid "The \"{0}\" command had a problem waiting for output." 64 | msgstr "La commande \"{0}\" a rencontré un problème d'attente en sortie." 65 | 66 | #: i18n-build/src/util.rs:29 67 | msgid "The \"{0}\" command reported that it was unsuccessful." 68 | msgstr "La commande \"{0}\" a signalé un échec." 69 | 70 | #. {0} is a directory path 71 | #. {1} is the name of the parent directory 72 | #. {2} is the expected parent of the directory in {0} 73 | #. Example: The path "i18n/src/po" is not inside the "src" directory: "i18n/src"." 74 | #: i18n-build/src/error.rs:187 75 | msgid "The path \"{0}\" is not inside the \"{1}\" directory: \"{2}\"." 76 | msgstr "Le chemin \"{0}\" n'est pas dans le répertoire \"{1}\": \"{2}\"." 77 | 78 | #: i18n-build/src/error.rs:147 79 | msgid "The path \"{0}\" does not exist on the filesystem." 80 | msgstr "Le chemin \"{0}\" n'existe pas sur le système de fichiers." 81 | 82 | #: i18n-build/src/error.rs:20 83 | msgid "file" 84 | msgstr "fichier" 85 | 86 | #: i18n-build/src/error.rs:21 87 | msgid "directory" 88 | msgstr "annuaire" 89 | 90 | #: i18n-build/src/error.rs:22 91 | msgid "symbolic link" 92 | msgstr "lien symbolique" 93 | 94 | -------------------------------------------------------------------------------- /i18n-build/i18n/po/ru/i18n_build.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: i18n-build\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2021-11-29 17:21+0000\n" 6 | "Language: ru\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "X-Generator: POEditor.com\n" 11 | 12 | #: i18n-build/src/error.rs:20 13 | msgid "file" 14 | msgstr "файл" 15 | 16 | #: i18n-build/src/error.rs:21 17 | msgid "directory" 18 | msgstr "каталог" 19 | 20 | #: i18n-build/src/error.rs:22 21 | msgid "symbolic link" 22 | msgstr "символическая ссылка" 23 | 24 | #. {0} is the file path, {1} is the item which it is for, {2} is the type of item (file, directory, etc) 25 | #: i18n-build/src/error.rs:140 26 | msgid "The path (\"{0}\") for {1} {2} does not have valid a utf-8 encoding." 27 | msgstr "Путь (\"{0}\") для {1} {2} не имеeт допустимой кодировки utf-8." 28 | 29 | #: i18n-build/src/error.rs:147 30 | msgid "The path \"{0}\" does not exist on the filesystem." 31 | msgstr "Путь \"{0}\" не существует в файловой системе." 32 | 33 | #. {0} can be either "file", or "directory", or "symlink" 34 | #. {1} is a file path 35 | #. {2} is more detailed information about the error 36 | #. Example: Cannot create the file "i18n/ru/something.pot" because "some error occurred" 37 | #: i18n-build/src/error.rs:155 38 | msgid "Cannot create the {0} \"{1}\" because: \"{2}\"." 39 | msgstr "Не удалось создать {0} \"{1}\" так как: \"{2}\"." 40 | 41 | #. {0} can be either "file", or "directory", or "symlink" 42 | #. {1} is a file path 43 | #. {2} is more detailed information about the error 44 | #. Example: Cannot delete the file "i18n/ru/something.pot" because "some error occurred" 45 | #: i18n-build/src/error.rs:165 46 | msgid "Cannot delete the {0} \"{1}\" because: \"{2}\"." 47 | msgstr "Не удалось удалить {0} \"{1}\" так как: \"{2}\"." 48 | 49 | #. {0} can be either "file", or "directory", or "symlink" 50 | #. {1} is the name of the file to be renamed 51 | #. {2} is the new file name 52 | #. {3} is more detailed information about the error 53 | #. Example: Cannot rename the file "old.pot" to "new.pot" because "some error occurred" 54 | #: i18n-build/src/error.rs:176 55 | msgid "Cannot rename the {0} \"{1}\" to \"{2}\" because {3}." 56 | msgstr "Не удалось переименовать {0} \"{1}\" в \"{2}\" так как {3}." 57 | 58 | #. {0} is a directory path 59 | #. {1} is the name of the parent directory 60 | #. {2} is the expected parent of the directory in {0} 61 | #. Example: The path "i18n/src/po" is not inside the "src" directory: "i18n/src"." 62 | #: i18n-build/src/error.rs:187 63 | msgid "The path \"{0}\" is not inside the \"{1}\" directory: \"{2}\"." 64 | msgstr "Путь \"{0}\" не находится \"{1}\" в каталоге: \"{2}\"." 65 | 66 | #: i18n-build/src/gettext_impl/mod.rs:208 67 | msgid "There was a problem executing the \"{0}\" command" 68 | msgstr "Произошла ошибка при выполнении команды \"{0}\"." 69 | 70 | #: i18n-build/src/gettext_impl/mod.rs:432 71 | msgid "There was a problem parsing one of the subcrates: \"{0}\"." 72 | msgstr "При анализе одного из подкрэйтора произошла ошибка: \"{0}\"." 73 | 74 | #: i18n-build/src/util.rs:17 75 | msgid "The \"{0}\" command was unable to start." 76 | msgstr "Команду \"{0}\" не удалось запустить." 77 | 78 | #: i18n-build/src/util.rs:21 79 | msgid "The \"{0}\" command had a problem waiting for output." 80 | msgstr "У команды \"{0}\" возникла проблема при ожидании вывода." 81 | 82 | #: i18n-build/src/util.rs:29 83 | msgid "The \"{0}\" command reported that it was unsuccessful." 84 | msgstr "Команда \"{0}\" сообщила что она не удалась." 85 | -------------------------------------------------------------------------------- /i18n-build/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for use with the `i18n_build` library. 2 | 3 | use std::fmt::Display; 4 | use std::io; 5 | use std::path::PathBuf; 6 | use thiserror::Error; 7 | use tr::tr; 8 | 9 | /// Type of path being represented in an error message. 10 | #[derive(Debug)] 11 | pub enum PathType { 12 | File, 13 | Directory, 14 | Symlink, 15 | } 16 | 17 | impl Display for PathType { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | match self { 20 | PathType::File => write!(f, "{}", tr!("file")), 21 | PathType::Directory => write!(f, "{}", tr!("directory")), 22 | PathType::Symlink => write!(f, "{}", tr!("symbolic link")), 23 | } 24 | } 25 | } 26 | 27 | /// The kinds of errors which can be expressed in a [PathError](PathError) 28 | #[derive(Debug)] 29 | pub enum PathErrorKind { 30 | NotValidUTF8 { 31 | for_item: String, 32 | path_type: PathType, 33 | }, 34 | DoesNotExist, 35 | CannotCreate(PathType, io::Error), 36 | CannotDelete(PathType, io::Error), 37 | CannotRename(PathType, PathBuf, io::Error), 38 | NotInsideDirectory(String, PathBuf), 39 | } 40 | 41 | /// This error type collates all the various generic file/path related 42 | /// errors in this application into one place, so that they can be 43 | /// translated easily. 44 | #[derive(Error, Debug)] 45 | pub struct PathError { 46 | /// The path associated with this error 47 | pub path: PathBuf, 48 | /// What specific kind of error this is 49 | pub kind: PathErrorKind, 50 | } 51 | 52 | impl PathError { 53 | /// An error for when a directory cannot be created. 54 | pub fn cannot_create_dir>(path: P, source: io::Error) -> PathError { 55 | PathError { 56 | path: path.into(), 57 | kind: PathErrorKind::CannotCreate(PathType::Directory, source), 58 | } 59 | } 60 | 61 | /// An error for when a file cannot be created. 62 | pub fn cannot_create_file>(path: P, source: io::Error) -> PathError { 63 | PathError { 64 | path: path.into(), 65 | kind: PathErrorKind::CannotCreate(PathType::File, source), 66 | } 67 | } 68 | 69 | /// An error for when a directory cannot be deleted. 70 | pub fn cannot_delete_dir>(path: P, source: io::Error) -> PathError { 71 | PathError { 72 | path: path.into(), 73 | kind: PathErrorKind::CannotDelete(PathType::Directory, source), 74 | } 75 | } 76 | 77 | /// An error for when a file cannot be deleted. 78 | pub fn cannot_delete_file>(path: P, source: io::Error) -> PathError { 79 | PathError { 80 | path: path.into(), 81 | kind: PathErrorKind::CannotCreate(PathType::Directory, source), 82 | } 83 | } 84 | 85 | /// An error for when a file cannot be renamed. 86 | pub fn cannot_rename_file>(from: P, to: P, source: io::Error) -> PathError { 87 | PathError { 88 | path: from.into(), 89 | kind: PathErrorKind::CannotRename(PathType::File, to.into(), source), 90 | } 91 | } 92 | 93 | /// An error for when the given path does not exist (when it was expected to exist). 94 | pub fn does_not_exist>(path: P) -> PathError { 95 | PathError { 96 | path: path.into(), 97 | kind: PathErrorKind::DoesNotExist, 98 | } 99 | } 100 | 101 | /// An error for when the given path contains some characters 102 | /// which do not conform to the UTF-8 standard/encoding. 103 | pub fn not_valid_utf8, P: Into>( 104 | path: P, 105 | for_item: F, 106 | path_type: PathType, 107 | ) -> PathError { 108 | PathError { 109 | path: path.into(), 110 | kind: PathErrorKind::NotValidUTF8 { 111 | for_item: for_item.into(), 112 | path_type, 113 | }, 114 | } 115 | } 116 | 117 | /// An error for when the given path is not inside another given 118 | /// path which is a directory. 119 | pub fn not_inside_dir, P: Into>( 120 | path: P, 121 | parent_name: S, 122 | parent_path: P, 123 | ) -> PathError { 124 | PathError { 125 | path: path.into(), 126 | kind: PathErrorKind::NotInsideDirectory(parent_name.into(), parent_path.into()), 127 | } 128 | } 129 | } 130 | 131 | impl Display for PathError { 132 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 133 | let message = match &self.kind { 134 | PathErrorKind::NotValidUTF8 { 135 | for_item, 136 | path_type, 137 | } => { 138 | tr!( 139 | // {0} is the file path, {1} is the item which it is for, {2} is the type of item (file, directory, etc) 140 | "The path (\"{0}\") for {1} {2} does not have valid a utf-8 encoding.", 141 | self.path.to_string_lossy(), 142 | for_item, 143 | path_type 144 | ) 145 | } 146 | PathErrorKind::DoesNotExist => tr!( 147 | "The path \"{0}\" does not exist on the filesystem.", 148 | self.path.to_string_lossy() 149 | ), 150 | PathErrorKind::CannotCreate(path_type, source) => tr!( 151 | // {0} can be either "file", or "directory", or "symlink" 152 | // {1} is a file path 153 | // {2} is more detailed information about the error 154 | // Example: Cannot create the file "i18n/ru/something.pot" because "some error occurred" 155 | "Cannot create the {0} \"{1}\" because: \"{2}\".", 156 | path_type, 157 | self.path.to_string_lossy(), 158 | source 159 | ), 160 | PathErrorKind::CannotDelete(path_type, source) => tr!( 161 | // {0} can be either "file", or "directory", or "symlink" 162 | // {1} is a file path 163 | // {2} is more detailed information about the error 164 | // Example: Cannot delete the file "i18n/ru/something.pot" because "some error occurred" 165 | "Cannot delete the {0} \"{1}\" because: \"{2}\".", 166 | path_type, 167 | self.path.to_string_lossy(), 168 | source 169 | ), 170 | PathErrorKind::CannotRename(path_type, to, source) => tr!( 171 | // {0} can be either "file", or "directory", or "symlink" 172 | // {1} is the name of the file to be renamed 173 | // {2} is the new file name 174 | // {3} is more detailed information about the error 175 | // Example: Cannot rename the file "old.pot" to "new.pot" because "some error occurred" 176 | "Cannot rename the {0} \"{1}\" to \"{2}\" because {3}.", 177 | path_type, 178 | self.path.to_string_lossy(), 179 | to.to_string_lossy(), 180 | source 181 | ), 182 | PathErrorKind::NotInsideDirectory(parent_name, parent_dir) => tr!( 183 | // {0} is a directory path 184 | // {1} is the name of the parent directory 185 | // {2} is the expected parent of the directory in {0} 186 | // Example: The path "i18n/src/po" is not inside the "src" directory: "i18n/src"." 187 | "The path \"{0}\" is not inside the \"{1}\" directory: \"{2}\".", 188 | self.path.to_string_lossy(), 189 | parent_name, 190 | parent_dir.to_string_lossy(), 191 | ), 192 | }; 193 | 194 | write!(f, "{message}") 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /i18n-build/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This library is designed for use within the 2 | //! [cargo-i18n](https://crates.io/crates/cargo_i18n) tool for 3 | //! localizing crates. It has been exposed and published as a library 4 | //! to allow its direct use within project build scripts if required. 5 | //! 6 | //! `xtr` (installed with `cargo install xtr`), and GNU Gettext CLI 7 | //! tools `msginit`, `msgfmt`, `msgmerge` and `msgcat` to be present 8 | //! in your system path. 9 | //! 10 | //! # Optional Features 11 | //! 12 | //! The `i18n-build` crate has the following optional Cargo features: 13 | //! 14 | //! + `localize` 15 | //! + Enables the runtime localization of this library using 16 | //! [localize()](#localize()) function via the 17 | //! [i18n-embed](https://crates.io/crates/i18n-embed) crate 18 | 19 | pub mod error; 20 | pub mod gettext_impl; 21 | pub mod util; 22 | pub mod watch; 23 | 24 | use anyhow::Result; 25 | use i18n_config::Crate; 26 | 27 | /// Run the i18n build process for the provided crate, which must 28 | /// contain an i18n config. 29 | pub fn run(crt: Crate) -> Result<()> { 30 | let mut crates: Vec = Vec::new(); 31 | 32 | let mut parent = crt.find_parent(); 33 | 34 | crates.push(crt); 35 | 36 | while parent.is_some() { 37 | crates.push(parent.unwrap()); 38 | parent = crates.last().unwrap().find_parent(); 39 | } 40 | 41 | crates.reverse(); 42 | 43 | let mut crates_iter = crates.iter_mut(); 44 | 45 | let mut parent = crates_iter 46 | .next() 47 | .expect("expected there to be at least one crate"); 48 | 49 | for child in crates_iter { 50 | child.parent = Some(parent); 51 | parent = child; 52 | } 53 | 54 | let last_child_crt = parent; 55 | 56 | let i18n_config = last_child_crt.config_or_err()?; 57 | if i18n_config.gettext.is_some() { 58 | gettext_impl::run(last_child_crt)?; 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | #[cfg(feature = "localize")] 65 | mod localize_feature { 66 | use i18n_embed::{ 67 | gettext::{gettext_language_loader, GettextLanguageLoader}, 68 | DefaultLocalizer, 69 | }; 70 | use std::sync::OnceLock; 71 | 72 | use rust_embed::RustEmbed; 73 | 74 | #[derive(RustEmbed)] 75 | #[folder = "i18n/mo"] 76 | pub struct Translations; 77 | 78 | static TRANSLATIONS: Translations = Translations {}; 79 | 80 | fn language_loader() -> &'static GettextLanguageLoader { 81 | static LANGUAGE_LOADER: OnceLock = OnceLock::new(); 82 | 83 | LANGUAGE_LOADER.get_or_init(|| gettext_language_loader!()) 84 | } 85 | 86 | /// Obtain a [Localizer](i18n_embed::Localizer) for localizing this library. 87 | /// 88 | /// ⚠️ *This API requires the following crate features to be activated: `localize`.* 89 | pub fn localizer() -> DefaultLocalizer<'static> { 90 | DefaultLocalizer::new(&*language_loader(), &TRANSLATIONS) 91 | } 92 | } 93 | 94 | #[cfg(feature = "localize")] 95 | pub use localize_feature::localizer; 96 | -------------------------------------------------------------------------------- /i18n-build/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for use with the `i18n_build` library. 2 | 3 | use log::debug; 4 | use std::fs::{create_dir_all, remove_file, rename}; 5 | use std::path::Path; 6 | use std::process::Command; 7 | 8 | use crate::error::PathError; 9 | use anyhow::{ensure, Context, Result}; 10 | use tr::tr; 11 | 12 | /// Run the specified command, check that it's output was reported as successful. 13 | pub fn run_command_and_check_success(command_name: &str, mut command: Command) -> Result<()> { 14 | debug!("Running command: {0:?}", &command); 15 | let output = command 16 | .spawn() 17 | .with_context(|| tr!("The \"{0}\" command was unable to start.", command_name))? 18 | .wait_with_output() 19 | .with_context(|| { 20 | tr!( 21 | "The \"{0}\" command had a problem waiting for output.", 22 | command_name 23 | ) 24 | })?; 25 | 26 | ensure!( 27 | output.status.success(), 28 | tr!( 29 | "The \"{0}\" command reported that it was unsuccessful.", 30 | command_name 31 | ) 32 | ); 33 | Ok(()) 34 | } 35 | 36 | /// Check that the given path exists, if it doesn't then throw a 37 | /// [PathError](PathError). 38 | pub fn check_path_exists>(path: P) -> Result<(), PathError> { 39 | if !path.as_ref().exists() { 40 | Err(PathError::does_not_exist(path.as_ref())) 41 | } else { 42 | Ok(()) 43 | } 44 | } 45 | 46 | /// Create any of the directories in the specified path if they don't 47 | /// already exist. 48 | pub fn create_dir_all_if_not_exists>(path: P) -> Result<(), PathError> { 49 | if !path.as_ref().exists() { 50 | create_dir_all(path.as_ref()) 51 | .map_err(|e| PathError::cannot_create_dir(path.as_ref(), e))?; 52 | } 53 | Ok(()) 54 | } 55 | 56 | /// Remove a file if it exists, otherwise return a [PathError#CannotDelete](PathError#CannotDelete). 57 | pub fn remove_file_if_exists>(file_path: P) -> Result<(), PathError> { 58 | if file_path.as_ref().exists() { 59 | remove_file(file_path.as_ref()) 60 | .map_err(|e| PathError::cannot_delete_file(file_path.as_ref(), e))?; 61 | } 62 | 63 | Ok(()) 64 | } 65 | 66 | /// Remove a file, or return a [PathError#CannotDelete](PathError#CannotDelete) if unable to. 67 | pub fn remove_file_or_error>(file_path: P) -> Result<(), PathError> { 68 | remove_file(file_path.as_ref()) 69 | .map_err(|e| PathError::cannot_delete_file(file_path.as_ref(), e)) 70 | } 71 | 72 | /// Rename a file, or return a [PathError#CannotRename](PathError#CannotRename) if unable to. 73 | pub fn rename_file, P2: AsRef>(from: P1, to: P2) -> Result<(), PathError> { 74 | let from_ref = from.as_ref(); 75 | let to_ref = to.as_ref(); 76 | rename(from_ref, to_ref) 77 | .map_err(|e| PathError::cannot_rename_file(from_ref.to_path_buf(), to_ref.to_path_buf(), e)) 78 | } 79 | -------------------------------------------------------------------------------- /i18n-build/src/watch.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions to use within a `build.rs` build script using 2 | //! this library. 3 | 4 | use crate::error::{PathError, PathType}; 5 | use std::path::Path; 6 | 7 | use anyhow::{anyhow, Result}; 8 | 9 | use walkdir::WalkDir; 10 | 11 | /// Tell `Cargo` to rerun the build script that calls this function 12 | /// (upon rebuild) if the specified file/directory changes. 13 | pub fn cargo_rerun_if_changed(path: &Path) -> Result<(), PathError> { 14 | println!( 15 | "cargo:rerun-if-changed={}", 16 | path.to_str().ok_or_else(|| PathError::not_valid_utf8( 17 | path, 18 | "rerun build script if file changed", 19 | PathType::Directory, 20 | ))? 21 | ); 22 | Ok(()) 23 | } 24 | 25 | /// Tell `Cargo` to rerun the build script that calls this function 26 | /// (upon rebuild) if any of the files/directories within the 27 | /// specified directory changes. 28 | pub fn cargo_rerun_if_dir_changed(path: &Path) -> Result<()> { 29 | cargo_rerun_if_changed(path)?; 30 | 31 | for result in WalkDir::new(path) { 32 | match result { 33 | Ok(entry) => { 34 | cargo_rerun_if_changed(entry.path())?; 35 | } 36 | Err(err) => return Err(anyhow!("error walking directory gui/: {}", err)), 37 | } 38 | } 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /i18n-config/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode 4 | Cargo.lock -------------------------------------------------------------------------------- /i18n-config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `i18n-config` Changelog 2 | 3 | ## v0.4.7 4 | 5 | ### Internal 6 | 7 | + Use `basic-toml` instead of `toml`. 8 | 9 | ## v0.4.6 10 | 11 | ### Internal 12 | 13 | + Bump `toml` to version `0.8`. 14 | 15 | ## v0.4.5 16 | 17 | ### Internal 18 | 19 | + Bump dependencies and refactor to use workspace dependencies. 20 | 21 | ## v0.4.4 22 | 23 | ### New Features 24 | 25 | + Add option to override the default domain name for fluent assets. 26 | 27 | ### Internal 28 | 29 | + Fix clippy warnings. 30 | + Bump `toml` to version `0.7`. 31 | 32 | ## v0.4.3 33 | 34 | + Prevent a panic when the crate root is a workspace. [#93](https://github.com/kellpossible/cargo-i18n/pull/93) thanks to [@ctrlcctrlv](https://github.com/ctrlcctrlv). 35 | 36 | ## v0.4.2 37 | 38 | ### New Features 39 | 40 | + Add the `use_fuzzy` option for the `gettext` system. [#68](https://github.com/kellpossible/cargo-i18n/pull/68) thanks to [@vkill](https://github.com/vkill). 41 | 42 | ## v0.4.1 43 | 44 | ### New Features 45 | 46 | + New `locate_crate_paths()` function for use in procedural macros. 47 | 48 | ## v0.4.0 49 | 50 | ### New Features 51 | 52 | + Introduced new `assets_dir` member of `[fluent]` subsection. 53 | 54 | ### Breaking Changes 55 | 56 | + Changed type of `fallback_language` from `String` to `unic_langid::LanguageIdentifier`. 57 | 58 | ### Internal Changes 59 | 60 | + Improved error messages. 61 | 62 | ## v0.3.0 63 | 64 | Changes for the support of the `fluent` localization system. 65 | 66 | ### New Features 67 | 68 | + New `FluentConfig` (along with associated `[fluent]` subsection in the configuration file format) for using the `fluent` localization system. 69 | 70 | ### Breaking Changes 71 | 72 | + Renamed `src_locale` to `fallback_language`. 73 | + Moved `target_locales` to within the `[gettext]` subsection, and renamed it to `target_languages`. 74 | 75 | ### Internal Changes 76 | 77 | + Now using `parking_lot::RwLock` for the language loaders, instead of the `RwLock` in the standard library. 78 | 79 | ## v0.2.2 80 | 81 | + Add support for `xtr` `add-location` option. 82 | 83 | ## v0.2.1 84 | 85 | + Updated link to this changelog in the crate README. 86 | 87 | ## v0.2.0 88 | 89 | + A bunch of changes to help with solving [issue 13](https://github.com/kellpossible/cargo-i18n/issues/13). 90 | + Add some debug logging using the [log crate](https://crates.io/crates/log). 91 | + Migrate away from `anyhow` and provide a new `I18nConfigError` type. 92 | + Change `I18nConfig#subcrates` type from `Option>` to `Vec` and use `serde` default of empty vector. 93 | + Add a `find_parent` method which searches. 94 | -------------------------------------------------------------------------------- /i18n-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Luke Frisken "] 3 | categories = ["localization", "internationalization"] 4 | description = "This library contains the configuration stucts (along with their parsing functions) for the cargo-i18n tool/system." 5 | edition = "2018" 6 | keywords = ["cargo", "build", "i18n", "gettext", "locale"] 7 | license = "MIT" 8 | name = "i18n-config" 9 | readme = "README.md" 10 | repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-config" 11 | version = "0.4.7" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [badges] 16 | maintenance = { status = "actively-developed" } 17 | 18 | [dependencies] 19 | log = { workspace = true } 20 | basic-toml = "0.1" 21 | serde = { workspace = true, features = ["derive"] } 22 | serde_derive = { workspace = true } 23 | thiserror = { workspace = true } 24 | unic-langid = { workspace = true, features = ["serde"] } 25 | -------------------------------------------------------------------------------- /i18n-config/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Luke Frisken 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | © 2020 Luke Frisken -------------------------------------------------------------------------------- /i18n-config/README.md: -------------------------------------------------------------------------------- 1 | # i18n-config [![crates.io badge](https://img.shields.io/crates/v/i18n-config.svg)](https://crates.io/crates/i18n-config) [![docs.rs badge](https://docs.rs/i18n-config/badge.svg)](https://docs.rs/i18n-config/) [![license badge](https://img.shields.io/github/license/kellpossible/cargo-i18n)](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-config/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) 2 | 3 | This library contains the configuration structs (along with their parsing functions) for the [cargo-i18n](https://crates.io/crates/cargo_i18n) tool/system. 4 | 5 | **[Changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-config/CHANGELOG.md)** 6 | -------------------------------------------------------------------------------- /i18n-config/src/fluent.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::path::PathBuf; 3 | 4 | /// The data structure representing what is stored (and possible to 5 | /// store) within the `fluent` subsection of a `i18n.toml` file. 6 | #[derive(Deserialize, Debug, Clone)] 7 | pub struct FluentConfig { 8 | /// (Required) The path to the assets directory. 9 | /// 10 | /// The paths inside the assets directory should be structured 11 | /// like so: `assets_dir/{language}/{domain}.ftl` 12 | pub assets_dir: PathBuf, 13 | 14 | /// (Optional) Domain name to override default value (i.e. package name) 15 | /// The paths inside the assets directory should be structured 16 | /// like so: `assets_dir/{language}/{domain}.ftl` 17 | pub domain: Option, 18 | } 19 | -------------------------------------------------------------------------------- /i18n-config/src/gettext.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::path::PathBuf; 3 | 4 | /// The data structure representing what is stored (and possible to 5 | /// store) within the `gettext` subsection of a `i18n.toml` file. 6 | #[derive(Deserialize, Debug, Clone)] 7 | pub struct GettextConfig { 8 | /// The languages that the software will be translated into. 9 | pub target_languages: Vec, 10 | /// Path to the output directory, relative to `i18n.toml` of the 11 | /// crate being localized. 12 | pub output_dir: PathBuf, 13 | // If this crate is being localized as a subcrate, store the 14 | // localization artifacts with the parent crate's output. 15 | // Currently crates which contain subcrates with duplicate names 16 | // are not supported. 17 | // 18 | // By default this is **false**. 19 | #[serde(default)] 20 | pub extract_to_parent: bool, 21 | // If a subcrate has extract_to_parent set to true, 22 | // then merge the output pot file of that subcrate into this 23 | // crate's pot file. 24 | // 25 | // By default this is **false**. 26 | #[serde(default)] 27 | pub collate_extracted_subcrates: bool, 28 | /// Set the copyright holder for the generated files. 29 | pub copyright_holder: Option, 30 | /// The reporting address for msgid bugs. This is the email 31 | /// address or URL to which the translators shall report bugs in 32 | /// the untranslated strings. 33 | pub msgid_bugs_address: Option, 34 | /// Whether or not to perform string extraction using the `xtr` command. 35 | pub xtr: Option, 36 | /// Generate ‘#: filename:line’ lines (default) in the pot files when 37 | /// running the `xtr` command. If the type is ‘full’ (the default), 38 | /// it generates the lines with both file name and line number. 39 | /// If it is ‘file’, the line number part is omitted. If it is ‘never’, 40 | /// nothing is generated. [possible values: full, file, never]. 41 | #[serde(default)] 42 | pub add_location: GettextAddLocation, 43 | /// Path to where the pot files will be written to by the `xtr` 44 | /// command, and were they will be read from by `msginit` and 45 | /// `msgmerge`. 46 | pot_dir: Option, 47 | /// Path to where the po files will be stored/edited with the 48 | /// `msgmerge` and `msginit` commands, and where they will be read 49 | /// from with the `msgfmt` command. 50 | po_dir: Option, 51 | /// Path to where the mo files will be written to by the 52 | /// `msgfmt` command. 53 | mo_dir: Option, 54 | /// Enable the `--use-fuzzy` option for the `msgfmt` command. 55 | /// 56 | /// By default this is **false**. 57 | #[serde(default)] 58 | pub use_fuzzy: bool, 59 | } 60 | 61 | impl GettextConfig { 62 | /// Path to where the pot files will be written to by the `xtr` 63 | /// command, and were they will be read from by `msginit` and 64 | /// `msgmerge`. 65 | /// 66 | /// By default this is 67 | /// **[output_dir](GettextConfig::output_dir)/pot**. 68 | pub fn pot_dir(&self) -> PathBuf { 69 | // match self.pot_dir { 70 | // Some(pot_dir) => pot_dir, 71 | // None => { 72 | // panic!("panic"); 73 | // }, 74 | // } 75 | self.pot_dir 76 | .clone() 77 | .unwrap_or_else(|| self.output_dir.join("pot")) 78 | } 79 | 80 | /// Path to where the po files will be stored/edited with the 81 | /// `msgmerge` and `msginit` commands, and where they will 82 | /// be read from with the `msgfmt` command. 83 | /// 84 | /// By default this is **[output_dir](GettextConfig::output_dir)/po**. 85 | pub fn po_dir(&self) -> PathBuf { 86 | self.po_dir 87 | .clone() 88 | .unwrap_or_else(|| self.output_dir.join("po")) 89 | } 90 | 91 | /// Path to where the mo files will be written to by the `msgfmt` command. 92 | /// 93 | /// By default this is 94 | /// **[output_dir](GettextConfig::output_dir)/mo**. 95 | pub fn mo_dir(&self) -> PathBuf { 96 | self.mo_dir 97 | .clone() 98 | .unwrap_or_else(|| self.output_dir.join("mo")) 99 | } 100 | } 101 | 102 | #[derive(Deserialize, Debug, Clone, Default)] 103 | #[serde(rename_all = "lowercase")] 104 | pub enum GettextAddLocation { 105 | #[default] 106 | Full, 107 | File, 108 | Never, 109 | } 110 | 111 | impl GettextAddLocation { 112 | pub fn to_str(&self) -> &str { 113 | match self { 114 | GettextAddLocation::Full => "full", 115 | GettextAddLocation::File => "file", 116 | GettextAddLocation::Never => "never", 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /i18n-config/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This library contains the configuration stucts (along with their 2 | //! parsing functions) for the 3 | //! [cargo-i18n](https://crates.io/crates/cargo_i18n) tool/system. 4 | 5 | mod fluent; 6 | mod gettext; 7 | 8 | pub use fluent::FluentConfig; 9 | pub use gettext::GettextConfig; 10 | 11 | use std::fs::read_to_string; 12 | use std::io; 13 | use std::{ 14 | fmt::Display, 15 | path::{Path, PathBuf}, 16 | }; 17 | 18 | use log::{debug, error}; 19 | use serde_derive::Deserialize; 20 | use thiserror::Error; 21 | use unic_langid::LanguageIdentifier; 22 | 23 | /// An error type explaining why a crate failed to verify. 24 | #[derive(Debug, Error)] 25 | pub enum WhyNotCrate { 26 | #[error("there is no Cargo.toml present")] 27 | NoCargoToml, 28 | #[error("it is a workspace")] 29 | Workspace, 30 | } 31 | 32 | /// An error type for use with the `i18n-config` crate. 33 | #[derive(Debug, Error)] 34 | pub enum I18nConfigError { 35 | #[error("The specified path is not a crate because {1}.")] 36 | NotACrate(PathBuf, WhyNotCrate), 37 | #[error("Cannot read file {0:?} in the current working directory {1:?} because {2}.")] 38 | CannotReadFile(PathBuf, io::Result, #[source] io::Error), 39 | #[error("Cannot parse Cargo configuration file {0:?} because {1}.")] 40 | CannotParseCargoToml(PathBuf, String), 41 | #[error("Cannot deserialize toml file {0:?} because {1}.")] 42 | CannotDeserializeToml(PathBuf, basic_toml::Error), 43 | #[error("Cannot parse i18n configuration file {0:?} because {1}.")] 44 | CannotPaseI18nToml(PathBuf, String), 45 | #[error("There is no i18n configuration file present for the crate {0}.")] 46 | NoI18nConfig(String), 47 | #[error("The \"{0}\" is required to be present in the i18n configuration file \"{1}\"")] 48 | OptionMissingInI18nConfig(String, PathBuf), 49 | #[error("There is no parent crate for {0}. Required because {1}.")] 50 | NoParentCrate(String, String), 51 | #[error( 52 | "There is no i18n config file present for the parent crate of {0}. Required because {1}." 53 | )] 54 | NoParentI18nConfig(String, String), 55 | #[error("Cannot read `CARGO_MANIFEST_DIR` environment variable.")] 56 | CannotReadCargoManifestDir, 57 | } 58 | 59 | #[derive(Deserialize)] 60 | struct RawCrate { 61 | #[serde(alias = "workspace")] 62 | package: RawPackage, 63 | } 64 | 65 | #[derive(Deserialize)] 66 | struct RawPackage { 67 | name: String, 68 | version: String, 69 | } 70 | 71 | /// Represents a rust crate. 72 | #[derive(Debug, Clone)] 73 | pub struct Crate<'a> { 74 | /// The name of the crate. 75 | pub name: String, 76 | /// The version of the crate. 77 | pub version: String, 78 | /// The path to the crate. 79 | pub path: PathBuf, 80 | /// Path to the parent crate which is triggering the localization 81 | /// for this crate. 82 | pub parent: Option<&'a Crate<'a>>, 83 | /// The file path expected to be used for `i18n_config` relative to this crate's root. 84 | pub config_file_path: PathBuf, 85 | /// The localization config for this crate (if it exists). 86 | pub i18n_config: Option, 87 | } 88 | 89 | impl<'a> Crate<'a> { 90 | /// Read crate from `Cargo.toml` i18n config using the 91 | /// `config_file_path` (if there is one). 92 | pub fn from, P2: Into>( 93 | path: P1, 94 | parent: Option<&'a Crate>, 95 | config_file_path: P2, 96 | ) -> Result, I18nConfigError> { 97 | let path_into = path.into(); 98 | 99 | let config_file_path_into = config_file_path.into(); 100 | 101 | let cargo_path = path_into.join("Cargo.toml"); 102 | 103 | if !cargo_path.exists() { 104 | return Err(I18nConfigError::NotACrate( 105 | path_into, 106 | WhyNotCrate::NoCargoToml, 107 | )); 108 | } 109 | 110 | let toml_str = read_to_string(cargo_path.clone()).map_err(|err| { 111 | I18nConfigError::CannotReadFile(cargo_path.clone(), std::env::current_dir(), err) 112 | })?; 113 | 114 | let cargo_toml: RawCrate = basic_toml::from_str(&toml_str) 115 | .map_err(|err| I18nConfigError::CannotDeserializeToml(cargo_path.clone(), err))?; 116 | 117 | let full_config_file_path = path_into.join(&config_file_path_into); 118 | let i18n_config = if full_config_file_path.exists() { 119 | Some(I18nConfig::from_file(&full_config_file_path)?) 120 | } else { 121 | None 122 | }; 123 | 124 | Ok(Crate { 125 | name: cargo_toml.package.name, 126 | version: cargo_toml.package.version, 127 | path: path_into, 128 | parent, 129 | config_file_path: config_file_path_into, 130 | i18n_config, 131 | }) 132 | } 133 | 134 | /// The name of the module/library used for this crate. Replaces 135 | /// `-` characters with `_` in the crate name. 136 | pub fn module_name(&self) -> String { 137 | self.name.replace('-', "_") 138 | } 139 | 140 | /// If there is a parent, get it's 141 | /// [I18nConfig#active_config()](I18nConfig#active_config()), 142 | /// otherwise return None. 143 | pub fn parent_active_config( 144 | &'a self, 145 | ) -> Result, I18nConfigError> { 146 | match self.parent { 147 | Some(parent) => parent.active_config(), 148 | None => Ok(None), 149 | } 150 | } 151 | 152 | /// Identify the config which should be used for this crate, and 153 | /// the crate (either this crate or one of it's parents) 154 | /// associated with that config. 155 | pub fn active_config(&'a self) -> Result, I18nConfigError> { 156 | debug!("Resolving active config for {0}", self); 157 | match &self.i18n_config { 158 | Some(config) => { 159 | if let Some(gettext_config) = &config.gettext { 160 | if gettext_config.extract_to_parent { 161 | debug!("Resolving active config for {0}, extract_to_parent is true, so attempting to obtain parent config.", self); 162 | 163 | if self.parent.is_none() { 164 | return Err(I18nConfigError::NoParentCrate( 165 | self.to_string(), 166 | "the gettext extract_to_parent option is active".to_string(), 167 | )); 168 | } 169 | 170 | return Ok(Some(self.parent_active_config()?.ok_or_else(|| { 171 | I18nConfigError::NoParentI18nConfig( 172 | self.to_string(), 173 | "the gettext extract_to_parent option is active".to_string(), 174 | ) 175 | })?)); 176 | } 177 | } 178 | 179 | Ok(Some((self, config))) 180 | } 181 | None => { 182 | debug!( 183 | "{0} has no i18n config, attempting to obtain parent config instead.", 184 | self 185 | ); 186 | self.parent_active_config() 187 | } 188 | } 189 | } 190 | 191 | /// Get the [I18nConfig](I18nConfig) in this crate, or return an 192 | /// error if there is none present. 193 | pub fn config_or_err(&self) -> Result<&I18nConfig, I18nConfigError> { 194 | match &self.i18n_config { 195 | Some(config) => Ok(config), 196 | None => Err(I18nConfigError::NoI18nConfig(self.to_string())), 197 | } 198 | } 199 | 200 | /// Get the [GettextConfig](GettextConfig) in this crate, or 201 | /// return an error if there is none present. 202 | pub fn gettext_config_or_err(&self) -> Result<&GettextConfig, I18nConfigError> { 203 | match &self.config_or_err()?.gettext { 204 | Some(gettext_config) => Ok(gettext_config), 205 | None => Err(I18nConfigError::OptionMissingInI18nConfig( 206 | "gettext section".to_string(), 207 | self.config_file_path.clone(), 208 | )), 209 | } 210 | } 211 | 212 | /// If this crate has a parent, check whether the parent wants to 213 | /// collate subcrates string extraction, as per the parent's 214 | /// [GettextConfig#collate_extracted_subcrates](GettextConfig#collate_extracted_subcrates). 215 | /// This also requires that the current crate's [GettextConfig#extract_to_parent](GettextConfig#extract_to_parent) 216 | /// is **true**. 217 | /// 218 | /// Returns **false** if there is no parent or the parent has no gettext config. 219 | pub fn collated_subcrate(&self) -> bool { 220 | let parent_extract_to_subcrate = self 221 | .parent 222 | .map(|parent_crate| { 223 | parent_crate 224 | .gettext_config_or_err() 225 | .map(|parent_gettext_config| parent_gettext_config.collate_extracted_subcrates) 226 | .unwrap_or(false) 227 | }) 228 | .unwrap_or(false); 229 | 230 | let extract_to_parent = self 231 | .gettext_config_or_err() 232 | .map(|gettext_config| gettext_config.extract_to_parent) 233 | .unwrap_or(false); 234 | 235 | parent_extract_to_subcrate && extract_to_parent 236 | } 237 | 238 | /// Attempt to resolve the parents of this crate which have this 239 | /// crate listed as a subcrate in their i18n config. 240 | pub fn find_parent(&self) -> Option> { 241 | let parent_crt = match self 242 | .path 243 | .canonicalize() 244 | .map(|op| op.parent().map(|p| p.to_path_buf())) 245 | .ok() 246 | .unwrap_or(None) 247 | { 248 | Some(parent_path) => match Crate::from(parent_path, None, "i18n.toml") { 249 | Ok(parent_crate) => { 250 | debug!("Found parent ({0}) of {1}.", parent_crate, self); 251 | Some(parent_crate) 252 | } 253 | Err(err) => { 254 | match err { 255 | I18nConfigError::NotACrate(path, WhyNotCrate::Workspace) => { 256 | debug!("The parent of {0} at path {1:?} is a workspace", self, path); 257 | } 258 | I18nConfigError::NotACrate(path, WhyNotCrate::NoCargoToml) => { 259 | debug!("The parent of {0} at path {1:?} is not a valid crate with a Cargo.toml", self, path); 260 | } 261 | _ => { 262 | error!( 263 | "Error occurred while attempting to resolve parent of {0}: {1}", 264 | self, err 265 | ); 266 | } 267 | } 268 | 269 | None 270 | } 271 | }, 272 | None => None, 273 | }; 274 | 275 | match parent_crt { 276 | Some(crt) => match &crt.i18n_config { 277 | Some(config) => { 278 | let this_is_subcrate = config 279 | .subcrates 280 | .iter() 281 | .any(|subcrate_path| { 282 | let subcrate_path_canon = match crt.path.join(subcrate_path).canonicalize() { 283 | Ok(canon) => canon, 284 | Err(err) => { 285 | error!("Error: unable to canonicalize the subcrate path: {0:?} because {1}", subcrate_path, err); 286 | return false; 287 | } 288 | }; 289 | 290 | let self_path_canon = match self.path.canonicalize() { 291 | Ok(canon) => canon, 292 | Err(err) => { 293 | error!("Error: unable to canonicalize the crate path: {0:?} because {1}", self.path, err); 294 | return false; 295 | } 296 | }; 297 | 298 | subcrate_path_canon == self_path_canon 299 | }); 300 | 301 | if this_is_subcrate { 302 | Some(crt) 303 | } else { 304 | debug!("Parent {0} does not have {1} correctly listed as one of its subcrates (curently: {2:?}) in its i18n config.", crt, self, config.subcrates); 305 | None 306 | } 307 | } 308 | None => { 309 | debug!("Parent {0} of {1} does not have an i18n config", crt, self); 310 | None 311 | } 312 | }, 313 | None => { 314 | debug!("Could not find a valid parent of {0}.", self); 315 | None 316 | } 317 | } 318 | } 319 | } 320 | 321 | impl<'a> Display for Crate<'a> { 322 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 323 | write!( 324 | f, 325 | "Crate \"{0}\" at \"{1}\"", 326 | self.name, 327 | self.path.to_string_lossy() 328 | ) 329 | } 330 | } 331 | 332 | /// The data structure representing what is stored (and possible to 333 | /// store) within a `i18n.toml` file. 334 | #[derive(Deserialize, Debug, Clone)] 335 | pub struct I18nConfig { 336 | /// The locale identifier of the language used in the source code 337 | /// for `gettext` system, and the primary fallback language (for 338 | /// which all strings must be present) when using the `fluent` 339 | /// system. 340 | pub fallback_language: LanguageIdentifier, 341 | /// Specify which subcrates to perform localization within. The 342 | /// subcrate needs to have its own `i18n.toml`. 343 | #[serde(default)] 344 | pub subcrates: Vec, 345 | /// The subcomponent of this config relating to gettext, only 346 | /// present if the gettext localization system will be used. 347 | pub gettext: Option, 348 | /// The subcomponent of this config relating to gettext, only 349 | /// present if the fluent localization system will be used. 350 | pub fluent: Option, 351 | } 352 | 353 | impl I18nConfig { 354 | /// Load the config from the specified toml file path. 355 | pub fn from_file>(toml_path: P) -> Result { 356 | let toml_path_final: &Path = toml_path.as_ref(); 357 | let toml_str = read_to_string(toml_path_final).map_err(|err| { 358 | I18nConfigError::CannotReadFile( 359 | toml_path_final.to_path_buf(), 360 | std::env::current_dir(), 361 | err, 362 | ) 363 | })?; 364 | let config: I18nConfig = basic_toml::from_str(toml_str.as_ref()).map_err(|err| { 365 | I18nConfigError::CannotDeserializeToml(toml_path_final.to_path_buf(), err) 366 | })?; 367 | 368 | Ok(config) 369 | } 370 | } 371 | 372 | /// Important i18n-config paths related to the current crate. 373 | pub struct CratePaths { 374 | /// The current crate directory path (where the `Cargo.toml` is 375 | /// located). 376 | pub crate_dir: PathBuf, 377 | /// The current i18n config file path 378 | pub i18n_config_file: PathBuf, 379 | } 380 | 381 | /// Locate the current crate's directory and `i18n.toml` config file. 382 | /// This is intended to be called by a procedural macro during crate 383 | /// compilation. 384 | pub fn locate_crate_paths() -> Result { 385 | let crate_dir = Path::new( 386 | &std::env::var_os("CARGO_MANIFEST_DIR") 387 | .ok_or(I18nConfigError::CannotReadCargoManifestDir)?, 388 | ) 389 | .to_path_buf(); 390 | let i18n_config_file = crate_dir.join("i18n.toml"); 391 | 392 | Ok(CratePaths { 393 | crate_dir, 394 | i18n_config_file, 395 | }) 396 | } 397 | -------------------------------------------------------------------------------- /i18n-embed-fl/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `i18n-embed-fl` 2 | 3 | ## v0.9.4 4 | 5 | ### New Features 6 | 7 | + Dashmap is now an optional dependency. Part of ongoing fix for [#131](https://github.com/kellpossible/cargo-i18n/issues/144) to reduce the number of dependencies. Thanks to [@mrtryhard](https://github.com/mrtryhard) for the contribution and [@bikeshedder](https://github.com/bikeshedder) for the review! 8 | 9 | ### Fixes 10 | 11 | + Support recursive message argument resolution. Fix for [#144](https://github.com/kellpossible/cargo-i18n/issues/144). Many thanks to [@cbs228](https://github.com/cbs228) for this contribution. 12 | 13 | ## v0.9.3 14 | 15 | ### Internal 16 | 17 | + Removed dependency on `lazy_static` #135 thanks to [@mrtryhard](https://github.com/mrtryhard). 18 | 19 | ## v0.9.2 20 | 21 | ### Fixes 22 | 23 | + Switch to proc-macro-error2 [#133](https://github.com/kellpossible/cargo-i18n/pull/133), fixes [#113](https://github.com/kellpossible/cargo-i18n/issues/113), thanks to [@mrtryhard](https://github.com/mrtryhard). 24 | + Fix compile warning for default features of `syn` crate. 25 | 26 | ## v0.9.1 27 | 28 | ### Fixes 29 | 30 | + Fix broken test/build due to version bump needed in root Cargo.toml. 31 | + Use domain override thanks to @Matt3o12 [#124](https://github.com/kellpossible/cargo-i18n/pull/124). 32 | 33 | ## v0.9.0 34 | 35 | ### Internal 36 | 37 | + Bump `strsim` to `0.11`. 38 | + Bump `dashmap` to `6.0`. 39 | 40 | ### Breaking 41 | 42 | + Bump `i18n-embed` to `0.15.0`. 43 | 44 | ### Fixes 45 | 46 | + Fallback to `std::env::var("CARGO_PKG_NAME")` Fixes [#97](https://github.com/kellpossible/cargo-i18n/issues/97) 47 | 48 | ## v0.8.0 49 | 50 | ### Breaking 51 | 52 | + The loader argument is now wrapped in brackets when expanded. This means that the return type for the macro will always be `String`, previously if the loader was a reference (e.g. `fl!(&loader, "message")`) the returned value would be `&String`, which was misleading (see [112](https://github.com/kellpossible/cargo-i18n/issues/112)) and not useful. 53 | 54 | ## v0.7.0 55 | 56 | ### Internal 57 | 58 | + Bump dependencies and use workspace dependencies. 59 | 60 | ## v0.6.7 61 | 62 | + Update to syn version `2.0`. 63 | 64 | ## v0.6.6 65 | 66 | + Fix for [#104](https://github.com/kellpossible/cargo-i18n/issues/104), include files necessary for running tests in crate. 67 | 68 | ## v0.6.5 69 | 70 | ### New Features 71 | 72 | + Support fluent attributes [#98](https://github.com/kellpossible/cargo-i18n/pull/98) thanks to [@Almost-Senseless-Coder](https://github.com/Almost-Senseless-Coder)! 73 | + Tweaked the `fl!()` macro definition such that it optionally accepts an attribute ID in addition to a message ID and arguments. 74 | + Implemented compile-time verification of attributes. 75 | 76 | ### Internal 77 | 78 | + Bump `i18n-embed` dependency to version `0.13.5`. 79 | + Bump `env_logger` dev dependency to version `0.10`. 80 | + Fix clippy warnings. 81 | 82 | ## v0.6.4 83 | 84 | + Update `dashmap` to version `5.1`. 85 | + Update `rust-embed` to `6.3` to address [RUSTSEC-2021-0126](https://rustsec.org/advisories/RUSTSEC-2021-0126.html). 86 | 87 | ## v0.6.3 88 | 89 | + Revert `dashmap` back to `4.0` due to [security warning](https://rustsec.org/advisories/RUSTSEC-2022-0002.html) 90 | 91 | ## v0.6.2 92 | 93 | + Update `dashmap` to version `5.1`. 94 | 95 | ## v0.6.1 96 | 97 | + Fix for #76, add missing `syn` dependency with `full` feature flag specified. 98 | 99 | ## v0.6.0 100 | 101 | ### Documentation 102 | 103 | + Don't reference specific `i18n-embed` version number. 104 | 105 | ### Breaking Changes 106 | 107 | + Update `i18n-embed` to version `0.13`. 108 | + Update `rust-embed` to version `6`. 109 | + Update `fluent` to version `0.16`. 110 | 111 | ## v0.5.0 112 | 113 | ### Breaking Changes 114 | 115 | + Updated `fluent` to version `0.15`. 116 | 117 | ## v0.4.0 118 | 119 | ### Breaking Changes 120 | 121 | + Update `i18n-embed` to version `0.11`. 122 | 123 | ### Internal Changes 124 | 125 | + Refactoring during the fix for [#60](https://github.com/kellpossible/cargo-i18n/issues/60). 126 | 127 | ## v0.3.1 128 | 129 | ### Internal Changes 130 | 131 | + Safer use of DashMap's new `4.0` API thanks to [#56](https://github.com/kellpossible/cargo-i18n/pull/56). 132 | 133 | ## v0.3.0 134 | 135 | + Update `fluent` dependency to version `0.14`. 136 | + Update to `dashmap` version `4.0`, and fix breaking change. 137 | 138 | ## v0.2.0 139 | 140 | + 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 version of `i18n-embed-fl`: `0.1.6`. 141 | 142 | ## v0.1.6 143 | 144 | ### Internal Changes 145 | 146 | + Update to `fluent` version `0.13`. 147 | + Fixes to address breaking changes in `fluent-syntax` version `0.10`. 148 | 149 | ## v0.1.5 150 | 151 | ### New Features 152 | 153 | + Updated readme with example convenience wrapper macro. 154 | + Added suggestions for message ids (ranked by levenshtein distance) to the error message when the current one fails to match. 155 | 156 | ## v0.1.4 157 | 158 | + Enable the args hashmap option `fl!(loader, "message_id", args())` to be parsed as an expression, instead of just an ident. 159 | 160 | ## v0.1.3 161 | 162 | + Fix bug where message check wasn't occurring with no arguments or with hashmap arguments. 163 | 164 | ## v0.1.2 165 | 166 | + Change the `loader` argument to be an expression, instead of an ident, so it allows more use cases. 167 | 168 | ## v0.1.1 169 | 170 | + Remove `proc_macro_diagnostic` feature causing problems compiling on stable, and use `proc-macro-error` crate instead. 171 | 172 | ## v0.1.0 173 | 174 | + Initial version, introduces the `fl!()` macro. 175 | -------------------------------------------------------------------------------- /i18n-embed-fl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "i18n-embed-fl" 3 | description = "Macro to perform compile time checks when using the i18n-embed crate and the fluent localization system" 4 | categories = ["localization", "internationalization", "development-tools"] 5 | version = "0.9.4" 6 | authors = ["Luke Frisken "] 7 | edition = "2018" 8 | license = "MIT" 9 | repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-embed-fl" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | dashmap = { version = "6.0", optional = true } 16 | find-crate = { workspace = true } 17 | fluent = { workspace = true } 18 | fluent-syntax = { workspace = true } 19 | i18n-config = { workspace = true } 20 | i18n-embed = { workspace = true, features = ["fluent-system", "filesystem-assets"]} 21 | proc-macro2 = { workspace = true } 22 | proc-macro-error2 = "2.0.1" 23 | quote = { workspace = true } 24 | strsim = "0.11" 25 | unic-langid = { workspace = true } 26 | 27 | [dependencies.syn] 28 | workspace = true 29 | features = ["derive", "proc-macro", "parsing", "printing", "extra-traits", "full"] 30 | 31 | [dev-dependencies] 32 | doc-comment = { workspace = true } 33 | env_logger = { workspace = true } 34 | pretty_assertions = { workspace = true } 35 | rust-embed = { workspace = true } 36 | 37 | [features] 38 | # Uses dashmap implementation for `fl!()` macro lookups. 39 | dashmap = ["dep:dashmap"] -------------------------------------------------------------------------------- /i18n-embed-fl/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Luke Frisken 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | © 2020 Luke Frisken -------------------------------------------------------------------------------- /i18n-embed-fl/README.md: -------------------------------------------------------------------------------- 1 | # i18n-embed-fl [![crates.io badge](https://img.shields.io/crates/v/i18n-embed-fl.svg)](https://crates.io/crates/i18n-embed-fl) [![docs.rs badge](https://docs.rs/i18n-embed-fl/badge.svg)](https://docs.rs/i18n-embed-fl/) [![license badge](https://img.shields.io/github/license/kellpossible/cargo-i18n)](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-embed-fl/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) 2 | 3 | This crate provides a macro to perform compile time checks when using the [i18n-embed](https://crates.io/crates/i18n-embed) crate and the [fluent](https://www.projectfluent.org/) localization system. 4 | 5 | See [docs](https://docs.rs/i18n-embed-fl/), and [i18n-embed](https://crates.io/crates/i18n-embed) for more information. 6 | 7 | **[Changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-embed-fl/CHANGELOG.md)** 8 | 9 | ## Example 10 | 11 | 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): 12 | 13 | ```toml 14 | # (Required) The language identifier of the language used in the 15 | # source code for gettext system, and the primary fallback language 16 | # (for which all strings must be present) when using the fluent 17 | # system. 18 | fallback_language = "en-GB" 19 | 20 | # Use the fluent localization system. 21 | [fluent] 22 | # (Required) The path to the assets directory. 23 | # The paths inside the assets directory should be structured like so: 24 | # `assets_dir/{language}/{domain}.ftl` 25 | assets_dir = "i18n" 26 | ``` 27 | 28 | Create a fluent localization file for the `en-GB` language in `i18n/en-GB/{domain}.ftl`, where `domain` is the rust path of your crate (`_` instead of `-`): 29 | 30 | ```fluent 31 | hello-arg = Hello {$name}! 32 | ``` 33 | 34 | Simple set up of the `FluentLanguageLoader`, and obtaining a message formatted with an argument: 35 | 36 | ```rust 37 | use i18n_embed::{ 38 | fluent::{fluent_language_loader, FluentLanguageLoader}, 39 | LanguageLoader, 40 | }; 41 | use i18n_embed_fl::fl; 42 | use rust_embed::RustEmbed; 43 | 44 | #[derive(RustEmbed)] 45 | #[folder = "i18n/"] 46 | struct Localizations; 47 | 48 | let loader: FluentLanguageLoader = fluent_language_loader!(); 49 | loader 50 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 51 | .unwrap(); 52 | 53 | assert_eq!( 54 | "Hello \u{2068}Bob 23\u{2069}!", 55 | // Compile time check for message id, and the `name` argument, 56 | // to ensure it matches what is specified in the `fallback_language`'s 57 | // fluent resource file. 58 | fl!(loader, "hello-arg", name = format!("Bob {}", 23)) 59 | ) 60 | ``` 61 | 62 | ## Convenience Macro 63 | 64 | You will notice that this macro requires `loader` to be specified in every call. For you project you may have access to a statically defined loader, and you can create a convenience macro wrapper so this doesn't need to be imported and specified every time. 65 | 66 | ```rust 67 | macro_rules! fl { 68 | ($message_id:literal) => {{ 69 | i18n_embed_fl::fl!($crate::YOUR_STATIC_LOADER, $message_id) 70 | }}; 71 | 72 | ($message_id:literal, $($args:expr),*) => {{ 73 | i18n_embed_fl::fl!($crate::YOUR_STATIC_LOADER, $message_id, $($args), *) 74 | }}; 75 | } 76 | ``` 77 | 78 | This can now be invoked like so: `fl!("message-id")`, `fl!("message-id", args)` and `fl!("message-id", arg = "value")`. 79 | -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | i18n-embed = { workspace = true, features = ["fluent-system"] } 10 | i18n-embed-fl = { workspace = true } 11 | rust-embed = { workspace = true } 12 | once_cell = { workspace = true } 13 | unic-langid = { workspace = true } 14 | -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/i18n.toml: -------------------------------------------------------------------------------- 1 | # (Required) The language identifier of the language used in the 2 | # source code for gettext system, and the primary fallback language 3 | # (for which all strings must be present) when using the fluent 4 | # system. 5 | fallback_language = "en-US" 6 | 7 | # Use the fluent localization system. 8 | [fluent] 9 | # (Required) The path to the assets directory. 10 | # The paths inside the assets directory should be structured like so: 11 | # `assets_dir/{language}/{domain}.ftl` 12 | assets_dir = "i18n" 13 | -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/i18n/de-AT/web_server.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n-embed-fl/examples/web-server/i18n/de-AT/web_server.ftl -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/i18n/de-DE/web_server.ftl: -------------------------------------------------------------------------------- 1 | hello-world = "Hallo Welt!" 2 | -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/i18n/en-GB/web_server.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n-embed-fl/examples/web-server/i18n/en-GB/web_server.ftl -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/i18n/en-US/web_server.ftl: -------------------------------------------------------------------------------- 1 | hello-world = Hello World! 2 | -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/i18n/ka-GE/web_server.ftl: -------------------------------------------------------------------------------- 1 | hello-world = გამარჯობა მსოფლიო! 2 | -------------------------------------------------------------------------------- /i18n-embed-fl/examples/web-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::{ 2 | fluent::{fluent_language_loader, FluentLanguageLoader, NegotiationStrategy}, 3 | LanguageLoader, 4 | }; 5 | use i18n_embed_fl::fl; 6 | use rust_embed::RustEmbed; 7 | 8 | #[derive(RustEmbed)] 9 | #[folder = "i18n/"] 10 | struct Localizations; 11 | 12 | fn main() { 13 | let loader: FluentLanguageLoader = fluent_language_loader!(); 14 | loader.load_available_languages(&Localizations).unwrap(); 15 | 16 | println!( 17 | "available languages: {:?}", 18 | loader.available_languages(&Localizations).unwrap() 19 | ); 20 | 21 | println!( 22 | "requested [en-US], response: {}", 23 | hande_request(&loader, &[&"en-US".parse().unwrap()]) 24 | ); 25 | println!( 26 | "requested [ka-GE], response: {}", 27 | hande_request(&loader, &[&"ka-GE".parse().unwrap()]) 28 | ); 29 | println!( 30 | "requested [en-UK], response: {}", 31 | hande_request(&loader, &[&"en-UK".parse().unwrap()]) 32 | ); 33 | println!( 34 | "requested [de-AT], response: {}", 35 | hande_request(&loader, &[&"de-AT".parse().unwrap()]) 36 | ); 37 | println!( 38 | "requested [ru-RU], response: {}", 39 | hande_request( 40 | &loader, 41 | &[&"ru-RU".parse().unwrap(), &"de-DE".parse().unwrap()] 42 | ) 43 | ); 44 | } 45 | 46 | fn hande_request( 47 | loader: &FluentLanguageLoader, 48 | requested_languages: &[&unic_langid::LanguageIdentifier], 49 | ) -> String { 50 | let loader = 51 | loader.select_languages_negotiate(requested_languages, NegotiationStrategy::Filtering); 52 | let message: String = fl!(loader, "hello-world"); 53 | format!("

{message}

") 54 | } 55 | -------------------------------------------------------------------------------- /i18n-embed-fl/i18n.toml: -------------------------------------------------------------------------------- 1 | # (Required) The language identifier of the language used in the 2 | # source code for gettext system, and the primary fallback language 3 | # (for which all strings must be present) when using the fluent 4 | # system. 5 | fallback_language = "en-US" 6 | 7 | # Use the fluent localization system. 8 | [fluent] 9 | # (Required) The path to the assets directory. 10 | # The paths inside the assets directory should be structured like so: 11 | # `assets_dir/{language}/{domain}.ftl` 12 | assets_dir = "i18n" -------------------------------------------------------------------------------- /i18n-embed-fl/i18n/en-US/i18n_embed_fl.ftl: -------------------------------------------------------------------------------- 1 | hello-world = Hello World! 2 | hello-arg = Hello {$name}! 3 | .attr = Hello {$name}'s attribute! 4 | hello-arg-2 = Hello {$name1} and {$name2}! 5 | hello-attr = Uninspiring. 6 | .text = Hello, attribute! 7 | hello-recursive = Hello { hello-recursive-descent } 8 | .attr = Why hello { hello-recursive-descent } 9 | .again = Why hello { hello-recursive-descent.attr } 10 | hello-recursive-descent = to you, {$name}! 11 | .attr = again, {$name}! 12 | hello-select = { $attr -> 13 | *[no] { hello-recursive } 14 | [yes] { hello-recursive.attr } 15 | } 16 | -------------------------------------------------------------------------------- /i18n-embed-fl/tests/fl_macro.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::{ 2 | fluent::{fluent_language_loader, FluentLanguageLoader}, 3 | LanguageLoader, 4 | }; 5 | use i18n_embed_fl::fl; 6 | use rust_embed::RustEmbed; 7 | use std::collections::HashMap; 8 | 9 | #[derive(RustEmbed)] 10 | #[folder = "i18n/"] 11 | struct Localizations; 12 | 13 | #[test] 14 | fn with_args_hashmap() { 15 | let loader: FluentLanguageLoader = fluent_language_loader!(); 16 | loader 17 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 18 | .unwrap(); 19 | 20 | let mut args: HashMap<&str, &str> = HashMap::new(); 21 | args.insert("name", "Bob"); 22 | 23 | pretty_assertions::assert_eq!("Hello \u{2068}Bob\u{2069}!", fl!(loader, "hello-arg", args)); 24 | } 25 | 26 | #[test] 27 | fn with_args_hashmap_expr() { 28 | let loader: FluentLanguageLoader = fluent_language_loader!(); 29 | loader 30 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 31 | .unwrap(); 32 | 33 | let args_expr = || { 34 | let mut args: HashMap<&str, &str> = HashMap::new(); 35 | args.insert("name", "Bob"); 36 | args 37 | }; 38 | 39 | pretty_assertions::assert_eq!( 40 | "Hello \u{2068}Bob\u{2069}!", 41 | fl!(loader, "hello-arg", args_expr()) 42 | ); 43 | } 44 | 45 | #[test] 46 | fn with_loader_expr() { 47 | let loader = || { 48 | let loader: FluentLanguageLoader = fluent_language_loader!(); 49 | loader 50 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 51 | .unwrap(); 52 | loader 53 | }; 54 | 55 | pretty_assertions::assert_eq!("Hello World!", fl!(loader(), "hello-world")); 56 | } 57 | 58 | #[test] 59 | fn with_one_arg_lit() { 60 | let loader: FluentLanguageLoader = fluent_language_loader!(); 61 | loader 62 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 63 | .unwrap(); 64 | 65 | pretty_assertions::assert_eq!( 66 | "Hello \u{2068}Bob\u{2069}!", 67 | fl!(loader, "hello-arg", name = "Bob") 68 | ); 69 | } 70 | 71 | #[test] 72 | fn with_attr() { 73 | let loader: FluentLanguageLoader = fluent_language_loader!(); 74 | loader 75 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 76 | .unwrap(); 77 | 78 | pretty_assertions::assert_eq!("Hello, attribute!", fl!(loader, "hello-attr", "text")); 79 | } 80 | 81 | #[test] 82 | fn with_attr_and_args() { 83 | let loader: FluentLanguageLoader = fluent_language_loader!(); 84 | loader 85 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 86 | .unwrap(); 87 | 88 | pretty_assertions::assert_eq!( 89 | "Hello \u{2068}Bob\u{2069}'s attribute!", 90 | fl!(loader, "hello-arg", "attr", name = "Bob") 91 | ); 92 | } 93 | 94 | #[test] 95 | fn with_args_in_messagereference() { 96 | let loader: FluentLanguageLoader = fluent_language_loader!(); 97 | loader 98 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 99 | .unwrap(); 100 | 101 | pretty_assertions::assert_eq!( 102 | "Hello to you, \u{2068}Bob\u{2069}!", 103 | fl!(loader, "hello-recursive", name = "Bob") 104 | ); 105 | } 106 | 107 | #[test] 108 | fn with_args_in_messagereference_attr() { 109 | let loader: FluentLanguageLoader = fluent_language_loader!(); 110 | loader 111 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 112 | .unwrap(); 113 | 114 | pretty_assertions::assert_eq!( 115 | "Why hello to you, \u{2068}Bob\u{2069}!", 116 | fl!(loader, "hello-recursive", "attr", name = "Bob") 117 | ); 118 | } 119 | 120 | #[test] 121 | fn with_args_in_messagereference_attr_to_attr() { 122 | let loader: FluentLanguageLoader = fluent_language_loader!(); 123 | loader 124 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 125 | .unwrap(); 126 | 127 | pretty_assertions::assert_eq!( 128 | "Why hello again, \u{2068}Bob\u{2069}!", 129 | fl!(loader, "hello-recursive", "again", name = "Bob") 130 | ); 131 | } 132 | 133 | #[test] 134 | fn with_args_in_select_messagereference() { 135 | let loader: FluentLanguageLoader = fluent_language_loader!(); 136 | loader 137 | .load_languages(&Localizations, &[loader.fallback_language().clone()]) 138 | .unwrap(); 139 | 140 | pretty_assertions::assert_eq!( 141 | "Hello to you, \u{2068}Bob\u{2069}!", 142 | fl!(loader, "hello-select", attr = "", name = "Bob") 143 | ); 144 | 145 | pretty_assertions::assert_eq!( 146 | "Why hello to you, \u{2068}Bob\u{2069}!", 147 | fl!(loader, "hello-select", attr = "yes", name = "Bob") 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /i18n-embed/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode 4 | Cargo.lock 5 | i18n/pot -------------------------------------------------------------------------------- /i18n-embed/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `i18n-embed` 2 | 3 | ## v0.15.4 4 | 5 | ### New Features 6 | 7 | + Add `FluentLanguageLoader::with_fluent_message_and_bundle()`. This method functions like `with_fluent_message()`, but it also returns the `FluentBundle` which owns the message. As part of implementation of [#144](https://github.com/kellpossible/cargo-i18n/issues/144). Many thanks to [@cbs228](https://github.com/cbs228) for this contribution. 8 | 9 | ### Internal 10 | 11 | + Bump version of `notify` dependency to version `8.0.0`. 12 | 13 | ## v0.15.3 14 | 15 | ### Internal 16 | 17 | + Removed dependency on `lazy_static` #135 thanks to [@mrtryhard](https://github.com/mrtryhard). 18 | 19 | ## v0.15.2 20 | 21 | + Fix incorrect workspace package specification for `i18n-embed-impl`. 22 | 23 | ## v0.15.1 (Yanked) 24 | 25 | ### Fixes 26 | 27 | + Fix for [#97](https://github.com/kellpossible/cargo-i18n/issues/97) Error when using workspace defined package metadata. Contributed by [@Umio-Yasuno](https://github.com/Umio-Yasuno). 28 | 29 | ### Internal 30 | 31 | + Use workspace dependency for `i18n-embed-impl` crate. 32 | 33 | ## v0.15.0 34 | 35 | ### New Features 36 | 37 | + New `autoreload` crate feature. 38 | + `RustEmbedNotifyAssets` - A wrapper for `rust_embed::RustEmbed` that supports notifications when files have changed on the file system. 39 | + `FileSystemAssets::notify_changes_enabled()` - A new method to enable watching for changes. 40 | + `AssetsMultiplexor` - A way to multiplex implmentations of [`I18nAssets`] where assets are multiplexed by a priority. 41 | 42 | ### Breaking 43 | 44 | + Modified `I18nAssets` trait. 45 | + Support multiple files referencing the same asset (to allow a heirachy of overrides). 46 | + Support for subscribing to updates to assets. 47 | + Remove deprecated methods for `LanguageConfig`, Please use `lang(...).get_attr_args(...)` etc instead. 48 | + `LanguageConfig::get_lang()` 49 | + `LanguageConfig::get_lang_args_concrete()` 50 | + `LanguageConfig::get_lang_args_fluent()` 51 | + `LanguageConfig::get_lang_args()` 52 | + `LanguageConfig::get_lang_attr()` 53 | + `LanguageConfig::get_lang_attr_args_concrete()` 54 | + `LanguageConfig::get_lang_attr_args_fluent()` 55 | + `LanguageConfig::get_lang_attr_args()` 56 | + `LanguageConfig::lang()` - Please use `select_languages(...)` instead. 57 | + Extra bounds on the arguments for `DefaultLocalizer::new()` (`Send + Sync + 'static`) to allow it to be used with `autoreload` feature. 58 | + `LanguageLoader::load_languages()` now accepts `&[unic_langid::LanguageIdentifier]` instead of `&[&unic_langid::LanguageIdentifier]`. 59 | + `LanguageLoader:reload()` - Added a new trait method which is used to reload the currently loaded languages. 60 | 61 | ### Fixes 62 | 63 | + Fallback to `std::env::var("CARGO_PKG_NAME")` Fixes [#97](https://github.com/kellpossible/cargo-i18n/issues/97) 64 | 65 | ## v0.14.1 66 | 67 | ## Internal 68 | 69 | + Relax the version constraint on `arc-swap`. 70 | 71 | ## v0.14.0 72 | 73 | ### Internal 74 | 75 | + Bump dependencies and use workspace dependencies. 76 | 77 | ## v0.13.9 78 | 79 | ## New Features 80 | 81 | - Add option to override the default domain name for fluent assets. 82 | 83 | ## v0.13.8 84 | 85 | ### Internal 86 | 87 | - Made `arc-swap` an optional dependency, it's only required for `fluent-system` implementation. 88 | 89 | ## v0.13.7 90 | 91 | ### New Features 92 | 93 | - A new `LanguageLoader::load_available_languages()` method to load all available languages. 94 | - A new `FluentLanguageLoader::select_languages()` method (renamed `FluentLanguageLoader::lang()`). 95 | - A new `FluentLanguageLoader::select_languages_negotiate()` method to select languages based on a negotiation strategy using the available languges. 96 | 97 | ### Deprecated 98 | 99 | - `FluentLanguageLoader::lang()` is deprecated in favour of renamed `FluentLanguageLoader::select_languages()`. 100 | 101 | ## v0.13.6 102 | 103 | ### New Features 104 | 105 | - A single new method called `FluentLanguageLoader::lang()` thanks to [@bikeshedder](https://github.com/bikeshedder)! 106 | 107 | This methods allows creating a shallow copy of the 108 | FluentLanguageLoader which can than be used just like the original 109 | loader but with a different current language setting. That makes it 110 | possible to use the fl! macro without any changes and is a far more 111 | elegant implementation than adding multiple get_lang\* methods as 112 | done in #84. 113 | 114 | ### Deprecated 115 | 116 | - `FluentLanguageLoader::get_lang*` methods have been deprecated in favour of `FluentLanguageLoader::lang()`. 117 | 118 | ## v0.13.5 119 | 120 | ### New Features 121 | 122 | - Support fluent attributes [#98](https://github.com/kellpossible/cargo-i18n/pull/98) thanks to [@Almost-Senseless-Coder](https://github.com/Almost-Senseless-Coder)! 123 | - New methods on `FluentLanguageLoader`: 124 | - `get_attr` 125 | - `get_attr_args_concrete` 126 | - `get_attr_args_fluent` 127 | - `get_attr_args` 128 | - `get_lang_attr` 129 | - `get_lang_attr_args_concrete` 130 | - `get_lang_attr_args_fluent` 131 | - `get_lang_attr_args` 132 | 133 | ### Internal 134 | 135 | - Bump `env_logger` dev dependency to version `0.10`. 136 | - Fix clippy warnings. 137 | 138 | ## v0.13.4 139 | 140 | ### New Features 141 | 142 | - 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). 143 | 144 | ## v0.13.3 145 | 146 | - Update `rust-embed` to `6.3` to address [RUSTSEC-2021-0126](https://rustsec.org/advisories/RUSTSEC-2021-0126.html). 147 | 148 | ## v0.13.2 149 | 150 | ### Internal 151 | 152 | - Use conditional compilation correctly for doctests. 153 | - Update `parking_lot` to version `0.12`. 154 | 155 | ## v0.13.1 156 | 157 | ### New Features 158 | 159 | - New `FluentLanguageLoader::with_bundles_mut()` method to allow mutable access to bundles. 160 | 161 | ### Internal Changes 162 | 163 | - Bumped `pretty_assertions` version `1.0`. 164 | - Fixed clippy lints. 165 | 166 | ## v0.13.0 167 | 168 | ### Breaking Changes 169 | 170 | - Update `rust-embed` to version `6`. 171 | - Update `fluent` to version `0.16`. 172 | 173 | ## v0.12.1 174 | 175 | ### Documentation 176 | 177 | - Updated crate description. 178 | - Don't reference specific `i18n-embed` version number. 179 | 180 | ## v0.12.0 181 | 182 | ### Documentation 183 | 184 | - Added [`bin`](./examples/bin/) example which explains how to consume the [`lib-fluent`](./examples/lib-fluent) example library in a desktop CLI application. 185 | 186 | ### Breaking Changes 187 | 188 | - Updated `fluent` to version `0.15`. 189 | 190 | ### Internal Changes 191 | 192 | - 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. 193 | 194 | ## v0.11.0 195 | 196 | ### Documentation 197 | 198 | - 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. 199 | - Updated examples with new `LanguageRequester::add_listener()` signature now using `std::sync::Arc` and `std::sync::Weak`. 200 | 201 | ### Breaking Changes 202 | 203 | - 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. 204 | - 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`. 205 | 206 | ### New Features 207 | 208 | - New `LanguageRequester::add_listener_ref()` method to add permenant listeners of type `&dyn Localizer`. This also affects `DesktopLanguageRequester` and `WebLanguageRequester`. 209 | 210 | ### Internal Changes 211 | 212 | - Fix clippy warnings. 213 | - Update `i18-embed-impl` to version `0.7.0`. 214 | 215 | ## v0.10.2 216 | 217 | - Add workaround for [#57](https://github.com/kellpossible/cargo-i18n/issues/57) for until is solved. 218 | 219 | ## v0.10.1 220 | 221 | - Update references to `i18n-embed` version in readme and source code examples. 222 | 223 | ## v0.10.0 224 | 225 | ### Fixes 226 | 227 | - 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). 228 | 229 | ### Internal Changes 230 | 231 | - Update `fluent` dependency to version `0.14`. 232 | 233 | ## v0.9.4 234 | 235 | ### New Features 236 | 237 | - 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). 238 | 239 | ### Internal Changes 240 | 241 | - Remove the now redundant CRLF fix [#36](https://github.com/kellpossible/cargo-i18n/issues/36). 242 | 243 | ## v0.9.3 244 | 245 | ### Fixes 246 | 247 | - Updated documentation for `select()` function. 248 | 249 | ## v0.9.2 250 | 251 | ### Fixes 252 | 253 | - Remove compiler warning. 254 | 255 | ## v0.9.1 256 | 257 | ### Fixes 258 | 259 | - Renamed argument in `select()` method for clarity. 260 | - Changed logs in `select()` method to use `debug` level instead of `info` level. 261 | 262 | ## v0.9.0 263 | 264 | - 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`. 265 | 266 | ## v0.8.6 267 | 268 | - Update documentation and example to more accurately reflect the current state of `LangaugeRequester::poll()` on various systems. 269 | 270 | ## v0.8.5 271 | 272 | ### New Features 273 | 274 | - Add new `get_args_fluent()` method to `FluentLanguageLoader` to allow arguments to be specified using `fluent`'s new `FluentArgs` type. 275 | 276 | ### Internal Changes 277 | 278 | - Update `fluent` to version `0.13`. 279 | - Fixes to address breaking changes in `fluent-syntax` version `0.10`. 280 | 281 | ## v0.8.4 282 | 283 | ### Bug Fixes 284 | 285 | - 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. 286 | 287 | ## v0.8.3 288 | 289 | ### New Features 290 | 291 | - Added a new `with_mesage_iter()` method to `FluentLanguageLoader`, to allow iterating over the messages available for a particular language. 292 | - Added `Default` implementation for `WebLanguageRequester`. 293 | 294 | ## v0.8.2 295 | 296 | - Fixed some mistakes in the docs. 297 | 298 | ## v0.8.1 299 | 300 | - Update version reference to `i18n-embed` in README, and docs. 301 | 302 | ## v0.8.0 303 | 304 | Changes to support the new `i18n-embed-fl` crate's `fl!()` macro, and some major cleanup/refactoring/simplification. 305 | 306 | ### New Features 307 | 308 | - A new `I18nAssets` trait, to support situations where assets are not embedded. 309 | - Automatic implementation of the `I18nAssets` trait for types that implement `RustEmbed`. 310 | - 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. 311 | - Implemented `Debug` trait on more types. 312 | - Added new `has()` and `with_fluent_message()` methods to `FluentLanguageLoader`. 313 | - Made `LanguageRequesterImpl` available on default crate features. No longer requires `gettext-system` or `fluent-system` to be enabled. 314 | 315 | ### Breaking Changes 316 | 317 | - Removed `I18nEmbed` trait, and derive macro, it was replaced with the new `I18nAssets` trait. 318 | - Clarified the `domain` and `module` arguments/variable inputs to `FluentLanguageLoader` and `GettextLanguageLoader`, and in the `LanguageLoader` trait with some renaming. 319 | - Removed a bunch of unecessary lifetimes, and `'static` bounds on types, methods and arguments. 320 | - `LanguageRequester::current_languages()`'s return type now uses `String` as the `HashMap` key instead of `&'static str`. 321 | - `available_languages()` implementation moved from `I18nEmbed` to `LanguageLoader`. 322 | 323 | ### Bug Fixes 324 | 325 | - 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). 326 | 327 | ## v0.7.2 328 | 329 | - Fix broken documentation links when compiling with no features. 330 | 331 | ## v0.7.1 332 | 333 | - Fix broken documentation links. 334 | 335 | ## v0.7.0 336 | 337 | Changes for the support of the `fluent` localization system. 338 | 339 | ### New Features 340 | 341 | - 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. 342 | 343 | ### Breaking Changes 344 | 345 | - 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. 346 | - 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. 347 | - `gettext-system` is no longer included in the default crate features. 348 | 349 | ## v0.6.1 350 | 351 | ### Bug Fixes 352 | 353 | - 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.) 354 | 355 | ## v0.6.0 356 | 357 | - 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. 358 | - Fixed typo in `LanguageRequester::set_language_override()`. 359 | 360 | ## v0.5.0 361 | 362 | - Refactored `I18nEmbedError::Multiple(Box>)` to `I18nEmbedError::Multiple(Vec)`, removing the useless box (and complaining Clippy lint). 363 | - Refactored `select()` method to use slice argument instead of `&Vec`. 364 | - Changed `LanguageRequester::add_listener(&mut self, localizer: &Rc>>)` to `add_listener(&mut self, localizer: &Rc>)` removing the unnecessary `Box`. 365 | - Added `Default` implementation for `DesktopLanguageRequester`. 366 | 367 | ## v0.4.2 368 | 369 | - Update `fluent-langneg` dependency to version `0.13`. 370 | - Update `unic-langid` dependency to version `0.9`. 371 | - Fix incorrect comment in code example [#18](https://github.com/kellpossible/cargo-i18n/issues/18). 372 | 373 | ## v0.4.0 374 | 375 | Mostly a refactor of `LanguageLoader` and `I18nAssets` to solve [issue #15](https://github.com/kellpossible/cargo-i18n/issues/15). 376 | 377 | - 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. 378 | - Refactored `I18nAssets` to move the `load_language_file` responsibility into `LanguageLoader` and add a new `load_language` method to `LanguageLoader`. 379 | - Refactored `I18nAssetsDyn` to also expose the `RustEmbed#get()` method, required for the new `LanguageLoader` changes. 380 | - 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`. 381 | 382 | ## v0.3.4 383 | 384 | - Made `WebLanguageRequester::requested_languages()` public. 385 | 386 | ## v0.3.3 387 | 388 | - Updated link to this changelog in the crate README. 389 | 390 | ## v0.3.2 391 | 392 | - Bump `i18n-config` dependency in `i18n-embed-impl` version to `0.2`. 393 | -------------------------------------------------------------------------------- /i18n-embed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Luke Frisken "] 3 | categories = ["localization", "internationalization", "development-tools::build-utils", "wasm"] 4 | description = "Traits and macros to conveniently embed localization assets into your application binary or library in order to localize it at runtime." 5 | edition = "2018" 6 | exclude = ["i18n/", "i18n.toml"] 7 | keywords = ["embed", "macro", "i18n", "gettext", "fluent"] 8 | license = "MIT" 9 | name = "i18n-embed" 10 | readme = "README.md" 11 | repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-embed" 12 | version = "0.15.4" 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | 17 | [badges] 18 | maintenance = { status = "actively-developed" } 19 | 20 | [dependencies] 21 | fluent = { workspace = true, optional = true } 22 | arc-swap = { version = "1", optional = true } 23 | fluent-langneg = { workspace = true } 24 | fluent-syntax = { workspace = true, optional = true } 25 | gettext = { workspace = true, optional = true } 26 | i18n-embed-impl = { workspace = true, optional = true } 27 | intl-memoizer = "0.5" 28 | locale_config = { version = "0.3", optional = true } 29 | log = { workspace = true } 30 | notify = { version = "8.0.0", optional = true } 31 | parking_lot = { version = "0.12", optional = true } 32 | rust-embed = { workspace = true, optional = true } 33 | thiserror = { workspace = true } 34 | tr = { version = "0.1", default-features = false, optional = true } 35 | unic-langid = { workspace = true } 36 | walkdir = { workspace = true, optional = true } 37 | web-sys = { version = "0.3", features = ["Window", "Navigator"], optional = true } 38 | 39 | [dev-dependencies] 40 | doc-comment = { workspace = true } 41 | env_logger = { workspace = true } 42 | maplit = "1.0" 43 | pretty_assertions = { workspace = true } 44 | serial_test = "3.0" 45 | 46 | [features] 47 | default = ["rust-embed"] 48 | 49 | gettext-system = ["tr", "tr/gettext", "dep:gettext", "parking_lot", "i18n-embed-impl", "i18n-embed-impl/gettext-system"] 50 | fluent-system = ["fluent", "fluent-syntax", "parking_lot", "i18n-embed-impl", "i18n-embed-impl/fluent-system", "arc-swap"] 51 | 52 | desktop-requester = ["locale_config"] 53 | web-sys-requester = ["web-sys"] 54 | 55 | filesystem-assets = ["walkdir"] 56 | 57 | autoreload = ["notify"] 58 | -------------------------------------------------------------------------------- /i18n-embed/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Luke Frisken 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | © 2020 Luke Frisken -------------------------------------------------------------------------------- /i18n-embed/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | 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). 4 | 5 | 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. 6 | 7 | **[Changelog](https://github.com/kellpossible/cargo-i18n/blob/master/i18n-embed/CHANGELOG.md)** 8 | 9 | ## Optional Features 10 | 11 | The `i18n-embed` crate has the following optional Cargo features: 12 | 13 | + `fluent-system` 14 | + Enable support for the [fluent](https://www.projectfluent.org/) localization system via the `FluentLanguageLoader`. 15 | + `gettext-system` 16 | + 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`. 17 | + `desktop-requester` 18 | + 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. 19 | + `web-sys-requester` 20 | + 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. 21 | 22 | ## Example 23 | 24 | The following is a minimal example for how localize your binary using this 25 | library using the [fluent](https://www.projectfluent.org/) localization system. 26 | 27 | First you need to compile `i18n-embed` in your `Cargo.toml` with the `fluent-system` and `desktop-requester` features enabled: 28 | 29 | ```toml 30 | [dependencies] 31 | i18n-embed = { version = "VERSION", features = ["fluent-system", "desktop-requester"]} 32 | rust-embed = "6" 33 | unic-langid = "0.9" 34 | ``` 35 | 36 | 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): 37 | 38 | ```toml 39 | # (Required) The language identifier of the language used in the 40 | # source code for gettext system, and the primary fallback language 41 | # (for which all strings must be present) when using the fluent 42 | # system. 43 | fallback_language = "en-GB" 44 | 45 | # Use the fluent localization system. 46 | [fluent] 47 | # (Required) The path to the assets directory. 48 | # The paths inside the assets directory should be structured like so: 49 | # `assets_dir/{language}/{domain}.ftl` 50 | assets_dir = "i18n" 51 | ``` 52 | 53 | 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/). 54 | 55 | The directory structure should look like so: 56 | 57 | ```txt 58 | my_crate/ 59 | Cargo.toml 60 | i18n.toml 61 | src/ 62 | i18n/ 63 | {language}/ 64 | {domain}.ftl 65 | ``` 66 | 67 | Then you can instantiate your language loader and language requester: 68 | 69 | ```rust 70 | use i18n_embed::{DesktopLanguageRequester, fluent::{ 71 | FluentLanguageLoader, fluent_language_loader 72 | }}; 73 | use rust_embed::RustEmbed; 74 | 75 | #[derive(RustEmbed)] 76 | #[folder = "i18n"] // path to the compiled localization resources 77 | struct Localizations; 78 | 79 | fn main() { 80 | let language_loader: FluentLanguageLoader = fluent_language_loader!(); 81 | 82 | // Use the language requester for the desktop platform (linux, windows, mac). 83 | // There is also a requester available for the web-sys WASM platform called 84 | // WebLanguageRequester, or you can implement your own. 85 | let requested_languages = DesktopLanguageRequester::requested_languages(); 86 | let _result = i18n_embed::select( 87 | &language_loader, &Localizations, &requested_languages); 88 | 89 | // continue on with your application 90 | } 91 | ``` 92 | 93 | 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: 94 | 95 | + Use the [cargo i18n](https://crates.io/crates/cargo-i18n) tool to perform validity checks (not yet implemented). 96 | + Integrate with a code-base using the `gettext` localization system. 97 | + Use the `fluent::fluent_language_loader!()` macro to pull the configuration in at compile time to create the `fluent::FluentLanguageLoader`. 98 | + Use the [fl!() macro](https://crates.io/crates/i18n-embed-fl) to have added compile-time safety when accessing messages. 99 | 100 | Example projects can be found in [examples](./examples). 101 | 102 | For more explained examples, see the [documentation for i18n-embed](https://docs.rs/i18n-embed/). 103 | -------------------------------------------------------------------------------- /i18n-embed/examples/desktop-bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bin" 3 | version = "0.1.0" 4 | authors = ["Luke Frisken "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | library-fluent = { path = "../library-fluent/" } 11 | i18n-embed = { workspace = true, features = ["fluent-system", "desktop-requester"]} 12 | env_logger = "0.11.3" 13 | -------------------------------------------------------------------------------- /i18n-embed/examples/desktop-bin/README.md: -------------------------------------------------------------------------------- 1 | # `bin` `i18n-embed` Example 2 | 3 | This example demonstrates how to use a library localized with [i18n-embed](../../i18n-embed/) and the `DesktopLanguageRequester` in a desktop CLI application. 4 | 5 | On unix, you can override the detected language by setting the `LANG` environment variable before running. The two available languages in the `library-fluent` example are `fr`, `eo`, and `en` (the fallback). 6 | -------------------------------------------------------------------------------- /i18n-embed/examples/desktop-bin/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use i18n_embed::{DesktopLanguageRequester, Localizer}; 4 | use library_fluent::{hello_world, localizer}; 5 | 6 | fn main() { 7 | env_logger::init(); 8 | let library_localizer = localizer().with_autoreload().unwrap(); 9 | let requested_languages = DesktopLanguageRequester::requested_languages(); 10 | 11 | if let Err(error) = library_localizer.select(&requested_languages) { 12 | eprintln!("Error while loading languages for library_fluent {error}"); 13 | } 14 | 15 | loop { 16 | println!("{}", hello_world()); 17 | std::thread::sleep(Duration::from_secs(1)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "library-fluent" 3 | version = "0.2.0" 4 | authors = ["Luke Frisken "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | i18n-embed = { workspace = true, features = ["fluent-system", "autoreload"] } 11 | i18n-embed-fl = { workspace = true } 12 | once_cell = { workspace = true } 13 | rust-embed = { workspace = true } 14 | -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/README.md: -------------------------------------------------------------------------------- 1 | # `library-fluent` `i18n-embed` Example 2 | 3 | This example demonstrates how to use [i18n-embed](../../i18n-embed/) in 4 | conjunction with the `fluent` localization system to localize a crate library 5 | that can be re-used in other projects. 6 | -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/i18n.toml: -------------------------------------------------------------------------------- 1 | # (Required) The language identifier of the language used in the 2 | # source code for gettext system, and the primary fallback language 3 | # (for which all strings must be present) when using the fluent 4 | # system. 5 | fallback_language = "en" 6 | 7 | # (Optional) Use the fluent localization system. 8 | [fluent] 9 | # (Required) The path to the assets directory. 10 | # The paths inside the assets directory should be structured like so: 11 | # `assets_dir/{language}/{domain}.ftl` 12 | assets_dir = "i18n" -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/i18n/en/library_fluent.ftl: -------------------------------------------------------------------------------- 1 | hello-world = Hello World! 2 | -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/i18n/eo/library_fluent.ftl: -------------------------------------------------------------------------------- 1 | hello-world = Saluton mondo! -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/i18n/fr/library_fluent.ftl: -------------------------------------------------------------------------------- 1 | hello-world = Bonjour le monde! -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/src/lib.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::{ 2 | fluent::{fluent_language_loader, FluentLanguageLoader}, 3 | DefaultLocalizer, LanguageLoader, RustEmbedNotifyAssets, 4 | }; 5 | use i18n_embed_fl::fl; 6 | use once_cell::sync::Lazy; 7 | use rust_embed::RustEmbed; 8 | 9 | #[derive(RustEmbed)] 10 | #[folder = "i18n/"] 11 | pub struct LocalizationsEmbed; 12 | 13 | pub static LOCALIZATIONS: Lazy> = Lazy::new(|| { 14 | RustEmbedNotifyAssets::new(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("i18n/")) 15 | }); 16 | 17 | static LANGUAGE_LOADER: Lazy = Lazy::new(|| { 18 | let loader: FluentLanguageLoader = fluent_language_loader!(); 19 | 20 | // Load the fallback langauge by default so that users of the 21 | // library don't need to if they don't care about localization. 22 | loader 23 | .load_fallback_language(&*LOCALIZATIONS) 24 | .expect("Error while loading fallback language"); 25 | 26 | loader 27 | }); 28 | 29 | macro_rules! fl { 30 | ($message_id:literal) => {{ 31 | i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id) 32 | }}; 33 | 34 | ($message_id:literal, $($args:expr),*) => {{ 35 | i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id, $($args), *) 36 | }}; 37 | } 38 | 39 | /// Get the hello world statement in whatever the currently selected 40 | /// localization is. 41 | pub fn hello_world() -> String { 42 | fl!("hello-world") 43 | } 44 | 45 | // Get the `Localizer` to be used for localizing this library. 46 | pub fn localizer() -> DefaultLocalizer<'static> { 47 | DefaultLocalizer::new(&*LANGUAGE_LOADER, &*LOCALIZATIONS) 48 | } 49 | -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/tests/no_localizer.rs: -------------------------------------------------------------------------------- 1 | use library_fluent::hello_world; 2 | 3 | /// Test using the library without using the provided 4 | /// [`localizer()`](library_fluent::localizer()) method. 5 | #[test] 6 | fn test_no_localizer() { 7 | assert_eq!(&hello_world(), "Hello World!") 8 | } 9 | -------------------------------------------------------------------------------- /i18n-embed/examples/library-fluent/tests/with_localizer.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::Localizer; 2 | use library_fluent::{hello_world, localizer}; 3 | 4 | use std::collections::HashSet; 5 | use std::iter::FromIterator; 6 | 7 | /// Test that the expected languages and fallback language are 8 | /// available. 9 | #[test] 10 | fn test_available_languages() { 11 | let localizer = localizer(); 12 | assert_eq!( 13 | &localizer.language_loader.fallback_language().to_string(), 14 | "en" 15 | ); 16 | 17 | let available_ids: HashSet = HashSet::from_iter( 18 | localizer 19 | .available_languages() 20 | .unwrap() 21 | .into_iter() 22 | .map(|id| id.to_string()), 23 | ); 24 | 25 | let expected_available_ids: HashSet = 26 | HashSet::from_iter(vec!["en".to_string(), "fr".to_string(), "eo".to_string()]); 27 | 28 | assert_eq!(available_ids, expected_available_ids) 29 | } 30 | 31 | /// Test loading the `en` language. 32 | #[test] 33 | fn test_select_english() { 34 | localizer().select(&["en".parse().unwrap()]).unwrap(); 35 | assert_eq!("Hello World!", &hello_world()) 36 | } 37 | 38 | /// Test loading the `fr` language. 39 | #[test] 40 | fn test_select_french() { 41 | localizer().select(&["fr".parse().unwrap()]).unwrap(); 42 | assert_eq!("Bonjour le monde!", &hello_world()) 43 | } 44 | -------------------------------------------------------------------------------- /i18n-embed/i18n-embed-impl/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode 4 | Cargo.lock -------------------------------------------------------------------------------- /i18n-embed/i18n-embed-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Luke Frisken "] 3 | description = "Macro implementations for i18n-embed" 4 | edition = "2018" 5 | keywords = ["build", "i18n", "gettext", "locale", "fluent"] 6 | categories = ["localization", "internationalization"] 7 | license = "MIT" 8 | name = "i18n-embed-impl" 9 | readme = "../README.md" 10 | repository = "https://github.com/kellpossible/cargo-i18n/tree/master/i18n-embed" 11 | version = "0.8.4" 12 | 13 | [badges] 14 | maintenance = { status = "actively-developed" } 15 | 16 | [lib] 17 | proc-macro = true 18 | 19 | [dependencies] 20 | find-crate = { workspace = true, optional = true } 21 | i18n-config = { workspace = true, optional = true } 22 | proc-macro2 = { workspace = true } 23 | quote = { workspace = true, optional = true } 24 | 25 | [dev-dependencies] 26 | rust-embed = { workspace = true } 27 | 28 | [dependencies.syn] 29 | workspace = true 30 | default-features = false 31 | features = ["derive", "proc-macro", "parsing", "printing", "extra-traits",] 32 | 33 | [features] 34 | default = [] 35 | gettext-system = ["i18n-config", "find-crate", "quote"] 36 | fluent-system = ["i18n-config", "find-crate", "quote"] 37 | -------------------------------------------------------------------------------- /i18n-embed/i18n-embed-impl/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Luke Frisken 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | © 2020 Luke Frisken -------------------------------------------------------------------------------- /i18n-embed/i18n-embed-impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// A procedural macro to create a new `GettextLanguageLoader` using 2 | /// the current crate's `i18n.toml` configuration, and domain. 3 | /// 4 | /// ⚠️ *This API requires the following crate features to be 5 | /// activated: `gettext-system`.* 6 | /// 7 | /// ## Example 8 | /// 9 | /// ```ignore 10 | /// use i18n_embed::gettext::{gettext_language_loader, GettextLanguageLoader}; 11 | /// let my_language_loader: GettextLanguageLoader = gettext_language_loader!(); 12 | /// ``` 13 | #[proc_macro] 14 | #[cfg(feature = "gettext-system")] 15 | pub fn gettext_language_loader(_: proc_macro::TokenStream) -> proc_macro::TokenStream { 16 | let manifest = find_crate::Manifest::new().expect("Error reading Cargo.toml"); 17 | let current_crate_package_name = { 18 | manifest.crate_package().map(|pkg| pkg.name).unwrap_or( 19 | std::env::var("CARGO_PKG_NAME").expect("Error fetching `CARGO_PKG_NAME` env"), 20 | ) 21 | }; 22 | 23 | // Special case for when this macro is invoked in i18n-embed tests/docs 24 | let i18n_embed_crate_name = if current_crate_package_name == "i18n_embed" { 25 | "i18n_embed".to_string() 26 | } else { 27 | manifest 28 | .find(|s| s == "i18n-embed") 29 | .expect("i18n-embed should be an active dependency in your Cargo.toml") 30 | .name 31 | }; 32 | 33 | let i18n_embed_crate_ident = 34 | syn::Ident::new(&i18n_embed_crate_name, proc_macro2::Span::call_site()); 35 | 36 | let config_file_path = i18n_config::locate_crate_paths() 37 | .unwrap_or_else(|error| { 38 | panic!( 39 | "gettext_language_loader!() is unable to locate i18n config file: {}", 40 | error 41 | ) 42 | }) 43 | .i18n_config_file; 44 | 45 | let config = i18n_config::I18nConfig::from_file(&config_file_path).unwrap_or_else(|err| { 46 | panic!( 47 | "gettext_language_loader!() had a problem reading i18n config file {0:?}: {1}", 48 | std::fs::canonicalize(&config_file_path).unwrap_or_else(|_| config_file_path.clone()), 49 | err 50 | ) 51 | }); 52 | 53 | if config.gettext.is_none() { 54 | panic!( 55 | "gettext_language_loader!() had a problem parsing i18n config file {0:?}: there is no `[gettext]` section", 56 | std::fs::canonicalize(&config_file_path).unwrap_or(config_file_path) 57 | ) 58 | } 59 | 60 | let fallback_language = syn::LitStr::new( 61 | &config.fallback_language.to_string(), 62 | proc_macro2::Span::call_site(), 63 | ); 64 | 65 | let gen = quote::quote! { 66 | #i18n_embed_crate_ident::gettext::GettextLanguageLoader::new( 67 | module_path!(), 68 | #fallback_language.parse().unwrap(), 69 | ) 70 | }; 71 | 72 | gen.into() 73 | } 74 | 75 | /// A procedural macro to create a new `FluentLanguageLoader` using 76 | /// the current crate's `i18n.toml` configuration, and domain. 77 | /// 78 | /// ⚠️ *This API requires the following crate features to be 79 | /// activated: `fluent-system`.* 80 | /// 81 | /// ## Example 82 | /// 83 | /// ```ignore 84 | /// use i18n_embed::fluent::{fluent_language_loader, FluentLanguageLoader}; 85 | /// let my_language_loader: FluentLanguageLoader = fluent_language_loader!(); 86 | /// ``` 87 | #[proc_macro] 88 | #[cfg(feature = "fluent-system")] 89 | pub fn fluent_language_loader(_: proc_macro::TokenStream) -> proc_macro::TokenStream { 90 | let manifest = find_crate::Manifest::new().expect("Error reading Cargo.toml"); 91 | let current_crate_package_name = manifest 92 | .crate_package() 93 | .map(|pkg| pkg.name) 94 | .unwrap_or(std::env::var("CARGO_PKG_NAME").expect("Error fetching `CARGO_PKG_NAME` env")); 95 | 96 | // Special case for when this macro is invoked in i18n-embed tests/docs 97 | let i18n_embed_crate_name = if current_crate_package_name == "i18n_embed" { 98 | "i18n_embed".to_string() 99 | } else { 100 | manifest 101 | .find(|s| s == "i18n-embed") 102 | .expect("i18n-embed should be an active dependency in your Cargo.toml") 103 | .name 104 | }; 105 | 106 | let i18n_embed_crate_ident = 107 | syn::Ident::new(&i18n_embed_crate_name, proc_macro2::Span::call_site()); 108 | 109 | let config_file_path = i18n_config::locate_crate_paths() 110 | .unwrap_or_else(|error| { 111 | panic!( 112 | "fluent_language_loader!() is unable to locate i18n config file: {}", 113 | error 114 | ) 115 | }) 116 | .i18n_config_file; 117 | 118 | let config = i18n_config::I18nConfig::from_file(&config_file_path).unwrap_or_else(|err| { 119 | panic!( 120 | "fluent_language_loader!() had a problem reading i18n config file {0:?}: {1}", 121 | std::fs::canonicalize(&config_file_path).unwrap_or_else(|_| config_file_path.clone()), 122 | err 123 | ) 124 | }); 125 | 126 | if config.fluent.is_none() { 127 | panic!( 128 | "fluent_language_loader!() had a problem parsing i18n config file {0:?}: there is no `[fluent]` section", 129 | std::fs::canonicalize(&config_file_path).unwrap_or(config_file_path) 130 | ) 131 | } 132 | 133 | let fallback_language = syn::LitStr::new( 134 | &config.fallback_language.to_string(), 135 | proc_macro2::Span::call_site(), 136 | ); 137 | 138 | let domain_str = config 139 | .fluent 140 | .and_then(|f| f.domain) 141 | .unwrap_or(current_crate_package_name); 142 | let domain = syn::LitStr::new(&domain_str, proc_macro2::Span::call_site()); 143 | 144 | let gen = quote::quote! { 145 | #i18n_embed_crate_ident::fluent::FluentLanguageLoader::new( 146 | #domain, 147 | #fallback_language.parse().unwrap(), 148 | ) 149 | }; 150 | 151 | gen.into() 152 | } 153 | -------------------------------------------------------------------------------- /i18n-embed/i18n.toml: -------------------------------------------------------------------------------- 1 | # This is included only to allow the code examples in this crate to 2 | # compile. 3 | 4 | # (Required) The language identifier of the language used in the 5 | # source code for gettext system, and the primary fallback language 6 | # (for which all strings must be present) when using the fluent 7 | # system. 8 | fallback_language = "en" 9 | 10 | # (Optional) Use the gettext localization system. 11 | [gettext] 12 | # (Required) The languages that the software will be translated into. 13 | target_languages = ["es", "ru", "fr"] 14 | 15 | # (Required) Path to the output directory, relative to `i18n.toml` of 16 | # the crate being localized. 17 | output_dir = "i18n" 18 | 19 | # (Optional) The reporting address for msgid bugs. This is the email address or 20 | # URL to which the translators shall report bugs in the untranslated 21 | # strings. 22 | msgid_bugs_address = "l.frisken@gmail.com" 23 | 24 | # (Optional) Use the fluent localization system. 25 | [fluent] 26 | # (Required) The path to the assets directory. 27 | # The paths inside the assets directory should be structured like so: 28 | # `assets_dir/{language}/{domain}.ftl` 29 | assets_dir = "i18n" -------------------------------------------------------------------------------- /i18n-embed/i18n/ftl/en-GB/test.ftl: -------------------------------------------------------------------------------- 1 | hello-world = Hello World Localisation! 2 | only-gb = only GB 3 | only-gb-args = Hello {$userName}! 4 | only-gb-us = only GB US (GB) 5 | different-args = this message has {$different} {$args} in different languages 6 | with-attr = Hello 7 | .attr = World! 8 | with-attr-and-args = Hello 9 | .who = {$name}! 10 | -------------------------------------------------------------------------------- /i18n-embed/i18n/ftl/en-US/.gitattributes: -------------------------------------------------------------------------------- 1 | test.ftl text eol=LF -------------------------------------------------------------------------------- /i18n-embed/i18n/ftl/en-US/test.ftl: -------------------------------------------------------------------------------- 1 | hello-world = Hello World Localization! 2 | only-us = only US 3 | only-ru = only RU 4 | only-gb-us = only GB US (US) 5 | only-gb = only GB (US Version) 6 | only-gb-args = Hello {$userName}! (US Version) 7 | different-args = this message has different {$arg}s in different languages 8 | isolation-chars = inject a { $thing } here 9 | multi-line = 10 | This is a multi-line message. 11 | 12 | This is a multi-line message. 13 | 14 | Finished! 15 | multi-line-args = 16 | This is a multiline message with arguments. 17 | 18 | { $argOne } 19 | 20 | This is a multiline message with arguments. 21 | 22 | { $argTwo } 23 | 24 | Finished! 25 | with-attr = Hello 26 | .attr = World (US version)! 27 | with-attr-and-args = Hello 28 | .who = {$name}! 29 | -------------------------------------------------------------------------------- /i18n-embed/i18n/ftl/ru/.gitattributes: -------------------------------------------------------------------------------- 1 | test.ftl text eol=CRLF -------------------------------------------------------------------------------- /i18n-embed/i18n/ftl/ru/test.ftl: -------------------------------------------------------------------------------- 1 | ### This file has been saved with CRLF line breaks. 2 | 3 | hello-world = Привет Мир Локализация! 4 | only-ru = только русский 5 | only-ru-args = Привет {$userName}! 6 | different-args = this message has {$different} args in different languages 7 | multi-line = 8 | Это многострочное сообщение. 9 | 10 | Это многострочное сообщение. 11 | 12 | Законченный! 13 | multi-line-args = 14 | Это многострочное сообщение с параметрами. 15 | 16 | { $argOne } 17 | 18 | Это многострочное сообщение с параметрами. 19 | 20 | { $argTwo } 21 | 22 | Законченный! -------------------------------------------------------------------------------- /i18n-embed/i18n/mo/README.md: -------------------------------------------------------------------------------- 1 | This directory, and `README.md` file exist only to allow the code examples in this crate to compile. -------------------------------------------------------------------------------- /i18n-embed/i18n/mo/es/i18n_embed.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n-embed/i18n/mo/es/i18n_embed.mo -------------------------------------------------------------------------------- /i18n-embed/i18n/mo/fr/i18n_embed.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n-embed/i18n/mo/fr/i18n_embed.mo -------------------------------------------------------------------------------- /i18n-embed/i18n/mo/ru/i18n_embed.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n-embed/i18n/mo/ru/i18n_embed.mo -------------------------------------------------------------------------------- /i18n-embed/i18n/po/es/i18n_embed.po: -------------------------------------------------------------------------------- 1 | # Spanish translations for i18n-embed package. 2 | # Copyright (C) 2020 THE i18n-embed'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the i18n-embed package. 4 | # Luke Frisken , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: i18n-embed 0.7.0\n" 9 | "Report-Msgid-Bugs-To: l.frisken@gmail.com\n" 10 | "POT-Creation-Date: 2020-08-22 18:16+0000\n" 11 | "PO-Revision-Date: 2020-08-22 22:16+0400\n" 12 | "Last-Translator: Luke Frisken \n" 13 | "Language-Team: Spanish \n" 14 | "Language: es\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: src/lib.rs:534 21 | msgid "only en" 22 | msgstr "" 23 | 24 | #: src/lib.rs:535 25 | msgid "only ru" 26 | msgstr "" 27 | 28 | #: src/lib.rs:536 29 | msgid "only es" 30 | msgstr "solo es" 31 | 32 | #: src/lib.rs:537 33 | msgid "only fr" 34 | msgstr "" 35 | -------------------------------------------------------------------------------- /i18n-embed/i18n/po/fr/i18n_embed.po: -------------------------------------------------------------------------------- 1 | # French translations for i18n-embed package. 2 | # Copyright (C) 2020 THE i18n-embed'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the i18n-embed package. 4 | # Luke Frisken , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: i18n-embed 0.7.0\n" 9 | "Report-Msgid-Bugs-To: l.frisken@gmail.com\n" 10 | "POT-Creation-Date: 2020-08-22 18:16+0000\n" 11 | "PO-Revision-Date: 2020-08-22 22:16+0400\n" 12 | "Last-Translator: Luke Frisken \n" 13 | "Language-Team: French \n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: src/lib.rs:534 21 | msgid "only en" 22 | msgstr "" 23 | 24 | #: src/lib.rs:535 25 | msgid "only ru" 26 | msgstr "" 27 | 28 | #: src/lib.rs:536 29 | msgid "only es" 30 | msgstr "" 31 | 32 | #: src/lib.rs:537 33 | msgid "only fr" 34 | msgstr "seulement fr" 35 | -------------------------------------------------------------------------------- /i18n-embed/i18n/po/ru/i18n_embed.po: -------------------------------------------------------------------------------- 1 | # Russian translations for i18n-embed package. 2 | # Copyright (C) 2020 THE i18n-embed'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the i18n-embed package. 4 | # Luke Frisken , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: i18n-embed 0.7.0\n" 9 | "Report-Msgid-Bugs-To: l.frisken@gmail.com\n" 10 | "POT-Creation-Date: 2020-08-22 18:16+0000\n" 11 | "PO-Revision-Date: 2020-08-22 22:16+0400\n" 12 | "Last-Translator: Luke Frisken \n" 13 | "Language-Team: Russian \n" 14 | "Language: ru\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 20 | 21 | #: src/lib.rs:534 22 | msgid "only en" 23 | msgstr "" 24 | 25 | #: src/lib.rs:535 26 | msgid "only ru" 27 | msgstr "только ру" 28 | 29 | #: src/lib.rs:536 30 | msgid "only es" 31 | msgstr "" 32 | 33 | #: src/lib.rs:537 34 | msgid "only fr" 35 | msgstr "" 36 | -------------------------------------------------------------------------------- /i18n-embed/src/assets.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use rust_embed::RustEmbed; 4 | 5 | use crate::I18nEmbedError; 6 | 7 | /// A trait to handle the retrieval of localization assets. 8 | pub trait I18nAssets { 9 | /// Get localization asset files that correspond to the specified `file_path`. Returns an empty 10 | /// [`Vec`] if the asset does not exist, or unable to obtain the asset due to a non-critical 11 | /// error. 12 | fn get_files(&self, file_path: &str) -> Vec>; 13 | /// Get an iterator over the file paths of the localization assets. There may be duplicates 14 | /// where multiple files exist for the same file path. 15 | fn filenames_iter(&self) -> Box + '_>; 16 | /// A method to allow users of this trait to subscribe to change events, and reload assets when 17 | /// they have changed. The subscription will be cancelled when the returned [`Watcher`] is 18 | /// dropped. 19 | /// 20 | /// **NOTE**: The implementation of this method is optional, don't rely on it functioning for all 21 | /// implementations. 22 | fn subscribe_changed( 23 | &self, 24 | #[allow(unused_variables)] changed: std::sync::Arc, 25 | ) -> Result, I18nEmbedError> { 26 | Ok(Box::new(())) 27 | } 28 | } 29 | 30 | impl Watcher for () {} 31 | 32 | impl I18nAssets for T 33 | where 34 | T: RustEmbed, 35 | { 36 | fn get_files(&self, file_path: &str) -> Vec> { 37 | Self::get(file_path) 38 | .map(|file| file.data) 39 | .into_iter() 40 | .collect() 41 | } 42 | 43 | fn filenames_iter(&self) -> Box> { 44 | Box::new(Self::iter().map(|filename| filename.to_string())) 45 | } 46 | 47 | #[allow(unused_variables)] 48 | fn subscribe_changed( 49 | &self, 50 | changed: std::sync::Arc, 51 | ) -> Result, I18nEmbedError> { 52 | Ok(Box::new(())) 53 | } 54 | } 55 | 56 | /// A wrapper for [`rust_embed::RustEmbed`] that supports notifications when files have changed on 57 | /// the file system. A wrapper is required to provide `base_dir` as this is unavailable in the type 58 | /// derived by the [`rust_embed::RustEmbed`] macro. 59 | /// 60 | /// ⚠️ *This type requires the following crate features to be activated: `autoreload`.* 61 | #[cfg(feature = "autoreload")] 62 | #[derive(Debug)] 63 | pub struct RustEmbedNotifyAssets { 64 | base_dir: std::path::PathBuf, 65 | embed: core::marker::PhantomData, 66 | } 67 | 68 | #[cfg(feature = "autoreload")] 69 | impl RustEmbedNotifyAssets { 70 | /// Construct a new [`RustEmbedNotifyAssets`]. 71 | pub fn new(base_dir: impl Into) -> Self { 72 | Self { 73 | base_dir: base_dir.into(), 74 | embed: core::marker::PhantomData, 75 | } 76 | } 77 | } 78 | 79 | #[cfg(feature = "autoreload")] 80 | impl I18nAssets for RustEmbedNotifyAssets 81 | where 82 | T: RustEmbed, 83 | { 84 | fn get_files(&self, file_path: &str) -> Vec> { 85 | T::get(file_path) 86 | .map(|file| file.data) 87 | .into_iter() 88 | .collect() 89 | } 90 | 91 | fn filenames_iter(&self) -> Box> { 92 | Box::new(T::iter().map(|filename| filename.to_string())) 93 | } 94 | 95 | fn subscribe_changed( 96 | &self, 97 | changed: std::sync::Arc, 98 | ) -> Result, I18nEmbedError> { 99 | let base_dir = &self.base_dir; 100 | if base_dir.is_dir() { 101 | log::debug!("Watching for changed files in {:?}", self.base_dir); 102 | notify_watcher(base_dir, changed).map_err(Into::into) 103 | } else { 104 | log::debug!("base_dir {base_dir:?} does not yet exist, unable to watch for changes"); 105 | Ok(Box::new(())) 106 | } 107 | } 108 | } 109 | 110 | /// An [I18nAssets] implementation which pulls assets from the OS 111 | /// file system. 112 | #[cfg(feature = "filesystem-assets")] 113 | #[derive(Debug)] 114 | pub struct FileSystemAssets { 115 | base_dir: std::path::PathBuf, 116 | #[cfg(feature = "autoreload")] 117 | notify_changes_enabled: bool, 118 | } 119 | 120 | #[cfg(feature = "filesystem-assets")] 121 | impl FileSystemAssets { 122 | /// Create a new `FileSystemAssets` instance, all files will be 123 | /// read from within the specified base directory. 124 | pub fn try_new>(base_dir: P) -> Result { 125 | let base_dir = base_dir.into(); 126 | 127 | if !base_dir.exists() { 128 | return Err(I18nEmbedError::DirectoryDoesNotExist(base_dir)); 129 | } 130 | 131 | if !base_dir.is_dir() { 132 | return Err(I18nEmbedError::PathIsNotDirectory(base_dir)); 133 | } 134 | 135 | Ok(Self { 136 | base_dir, 137 | #[cfg(feature = "autoreload")] 138 | notify_changes_enabled: false, 139 | }) 140 | } 141 | 142 | /// Enable the notification of changes in the [`I18nAssets`] implementation. 143 | #[cfg(feature = "autoreload")] 144 | pub fn notify_changes_enabled(mut self, enabled: bool) -> Self { 145 | self.notify_changes_enabled = enabled; 146 | self 147 | } 148 | } 149 | 150 | /// An error that occurs during notification of changes when the `autoreload feature is enabled.` 151 | /// 152 | /// ⚠️ *This type requires the following crate features to be activated: `filesystem-assets`.* 153 | #[cfg(feature = "autoreload")] 154 | #[derive(Debug)] 155 | pub struct NotifyError(notify::Error); 156 | 157 | #[cfg(feature = "autoreload")] 158 | impl From for NotifyError { 159 | fn from(value: notify::Error) -> Self { 160 | Self(value) 161 | } 162 | } 163 | 164 | #[cfg(feature = "autoreload")] 165 | impl From for I18nEmbedError { 166 | fn from(value: notify::Error) -> Self { 167 | Self::Notify(value.into()) 168 | } 169 | } 170 | 171 | #[cfg(feature = "autoreload")] 172 | impl std::fmt::Display for NotifyError { 173 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 174 | self.0.fmt(f) 175 | } 176 | } 177 | 178 | #[cfg(feature = "autoreload")] 179 | impl std::error::Error for NotifyError {} 180 | 181 | #[cfg(feature = "autoreload")] 182 | fn notify_watcher( 183 | base_dir: &std::path::Path, 184 | changed: std::sync::Arc, 185 | ) -> notify::Result> { 186 | let mut watcher = notify::recommended_watcher(move |event_result| { 187 | let event: notify::Event = match event_result { 188 | Ok(event) => event, 189 | Err(error) => { 190 | log::error!("{error}"); 191 | return; 192 | } 193 | }; 194 | match event.kind { 195 | notify::EventKind::Any 196 | | notify::EventKind::Create(_) 197 | | notify::EventKind::Modify(_) 198 | | notify::EventKind::Remove(_) 199 | | notify::EventKind::Other => changed(), 200 | _ => {} 201 | } 202 | })?; 203 | 204 | notify::Watcher::watch(&mut watcher, base_dir, notify::RecursiveMode::Recursive)?; 205 | 206 | Ok(Box::new(watcher)) 207 | } 208 | 209 | /// An entity that watches for changes to localization resources. 210 | /// 211 | /// NOTE: Currently we rely in the implicit [`Drop`] implementation to remove file system watches, 212 | /// in the future ther may be new methods added to this trait. 213 | pub trait Watcher {} 214 | 215 | #[cfg(feature = "autoreload")] 216 | impl Watcher for notify::RecommendedWatcher {} 217 | 218 | #[cfg(feature = "filesystem-assets")] 219 | impl I18nAssets for FileSystemAssets { 220 | fn get_files(&self, file_path: &str) -> Vec> { 221 | let full_path = self.base_dir.join(file_path); 222 | 223 | if !(full_path.is_file() && full_path.exists()) { 224 | return Vec::new(); 225 | } 226 | 227 | match std::fs::read(full_path) { 228 | Ok(contents) => vec![Cow::from(contents)], 229 | Err(e) => { 230 | log::error!( 231 | target: "i18n_embed::assets", 232 | "Unexpected error while reading localization asset file: {}", 233 | e); 234 | Vec::new() 235 | } 236 | } 237 | } 238 | 239 | fn filenames_iter(&self) -> Box> { 240 | Box::new( 241 | walkdir::WalkDir::new(&self.base_dir) 242 | .into_iter() 243 | .filter_map(|f| match f { 244 | Ok(f) => { 245 | if f.file_type().is_file() { 246 | match f.file_name().to_str() { 247 | Some(filename) => Some(filename.to_string()), 248 | None => { 249 | log::error!( 250 | target: "i18n_embed::assets", 251 | "Filename {:?} is not valid UTF-8.", 252 | f.file_name()); 253 | None 254 | } 255 | } 256 | } else { 257 | None 258 | } 259 | } 260 | Err(err) => { 261 | log::error!( 262 | target: "i18n_embed::assets", 263 | "Unexpected error while gathering localization asset filenames: {}", 264 | err); 265 | None 266 | } 267 | }), 268 | ) 269 | } 270 | 271 | /// See [`FileSystemAssets::notify_changes_enabled`] to enable this implementation. 272 | /// ⚠️ *This method requires the following crate features to be activated: `autoreload`.* 273 | #[cfg(feature = "autoreload")] 274 | fn subscribe_changed( 275 | &self, 276 | changed: std::sync::Arc, 277 | ) -> Result, I18nEmbedError> { 278 | if self.notify_changes_enabled { 279 | notify_watcher(&self.base_dir, changed).map_err(Into::into) 280 | } else { 281 | Ok(Box::new(())) 282 | } 283 | } 284 | } 285 | 286 | /// A way to multiplex implmentations of [`I18nAssets`]. 287 | pub struct AssetsMultiplexor { 288 | /// Assets that are multiplexed, ordered from most to least priority. 289 | assets: Vec>, 290 | } 291 | 292 | impl std::fmt::Debug for AssetsMultiplexor { 293 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 294 | f.debug_struct("AssetsMultiplexor") 295 | .field( 296 | "assets", 297 | &self.assets.iter().map(|_| "").collect::>(), 298 | ) 299 | .finish() 300 | } 301 | } 302 | 303 | impl AssetsMultiplexor { 304 | /// Construct a new [`AssetsMultiplexor`]. `assets` are specified in order of priority of 305 | /// processing for the [`crate::LanguageLoader`]. 306 | pub fn new( 307 | assets: impl IntoIterator>, 308 | ) -> Self { 309 | Self { 310 | assets: assets.into_iter().collect(), 311 | } 312 | } 313 | } 314 | 315 | #[allow(dead_code)] // We rely on the Drop implementation of the Watcher to remove the file system watch. 316 | struct Watchers(Vec>); 317 | 318 | impl Watcher for Watchers {} 319 | 320 | impl I18nAssets for AssetsMultiplexor { 321 | fn get_files(&self, file_path: &str) -> Vec> { 322 | self.assets 323 | .iter() 324 | .flat_map(|assets| assets.get_files(file_path)) 325 | .collect() 326 | } 327 | 328 | fn filenames_iter(&self) -> Box + '_> { 329 | Box::new( 330 | self.assets 331 | .iter() 332 | .flat_map(|assets| assets.filenames_iter()), 333 | ) 334 | } 335 | 336 | fn subscribe_changed( 337 | &self, 338 | changed: std::sync::Arc, 339 | ) -> Result, I18nEmbedError> { 340 | let watchers: Vec<_> = self 341 | .assets 342 | .iter() 343 | .map(|assets| assets.subscribe_changed(changed.clone())) 344 | .collect::>()?; 345 | Ok(Box::new(Watchers(watchers))) 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /i18n-embed/src/gettext.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the types and functions to interact with the 2 | //! `gettext` localization system. 3 | //! 4 | //! Most important is the [GettextLanguageLoader]. 5 | //! 6 | //! ⚠️ *This module requires the following crate features to be activated: `gettext-system`.* 7 | 8 | use crate::{domain_from_module, I18nAssets, I18nEmbedError, LanguageLoader}; 9 | 10 | pub use i18n_embed_impl::gettext_language_loader; 11 | 12 | use gettext as gettext_system; 13 | use parking_lot::RwLock; 14 | use unic_langid::LanguageIdentifier; 15 | 16 | /// [LanguageLoader] implementation for the `gettext` localization 17 | /// system. 18 | /// 19 | /// ⚠️ *This API requires the following crate features to be activated: `gettext-system`.* 20 | #[derive(Debug)] 21 | pub struct GettextLanguageLoader { 22 | current_language: RwLock, 23 | module: &'static str, 24 | fallback_language: LanguageIdentifier, 25 | } 26 | 27 | impl GettextLanguageLoader { 28 | /// Create a new `GettextLanguageLoader`. 29 | /// 30 | /// # Example 31 | /// 32 | /// ``` 33 | /// use i18n_embed::gettext::GettextLanguageLoader; 34 | /// 35 | /// GettextLanguageLoader::new(module_path!(), "en".parse().unwrap()); 36 | /// ``` 37 | pub fn new(module: &'static str, fallback_language: unic_langid::LanguageIdentifier) -> Self { 38 | Self { 39 | current_language: RwLock::new(fallback_language.clone()), 40 | module, 41 | fallback_language, 42 | } 43 | } 44 | 45 | fn load_src_language(&self) { 46 | let catalog = gettext_system::Catalog::empty(); 47 | tr::internal::set_translator(self.module, catalog); 48 | *(self.current_language.write()) = self.fallback_language().clone(); 49 | } 50 | } 51 | 52 | impl LanguageLoader for GettextLanguageLoader { 53 | /// The fallback language for the module this loader is responsible 54 | /// for. 55 | fn fallback_language(&self) -> &LanguageIdentifier { 56 | &self.fallback_language 57 | } 58 | 59 | /// The domain for the translation that this loader is associated with. 60 | fn domain(&self) -> &'static str { 61 | domain_from_module(self.module) 62 | } 63 | 64 | /// The language file name to use for this loader's domain. 65 | fn language_file_name(&self) -> String { 66 | format!("{}.mo", self.domain()) 67 | } 68 | 69 | /// Get the language which is currently loaded for this loader. 70 | fn current_language(&self) -> LanguageIdentifier { 71 | self.current_language.read().clone() 72 | } 73 | 74 | /// Load the languages `language_ids` using the resources packaged 75 | /// in the `i18n_assets` in order of fallback preference. This 76 | /// also sets the [LanguageLoader::current_language()] to the 77 | /// first in the `language_ids` slice. You can use 78 | /// [select()](super::select()) to determine which fallbacks are 79 | /// actually available for an arbitrary slice of preferences. 80 | /// 81 | /// **Note:** Gettext doesn't support loading multiple languages 82 | /// as multiple fallbacks. We only load the first of the requested 83 | /// languages, and the fallback is the src language. 84 | #[allow(single_use_lifetimes)] 85 | fn load_languages( 86 | &self, 87 | i18n_assets: &dyn I18nAssets, 88 | language_ids: &[unic_langid::LanguageIdentifier], 89 | ) -> Result<(), I18nEmbedError> { 90 | let language_id = language_ids 91 | .iter() 92 | .next() 93 | .ok_or(I18nEmbedError::RequestedLanguagesEmpty)?; 94 | 95 | if language_id == self.fallback_language() { 96 | self.load_src_language(); 97 | return Ok(()); 98 | } 99 | let (path, files) = self.language_files(language_id, i18n_assets); 100 | let file = match files.as_slice() { 101 | [first_file] => first_file, 102 | [first_file, ..] => { 103 | log::warn!( 104 | "Gettext system does not yet support merging language files for {path:?}" 105 | ); 106 | first_file 107 | } 108 | [] => { 109 | log::error!( 110 | target:"i18n_embed::gettext", 111 | "{} Setting current_language to fallback locale: \"{}\".", 112 | I18nEmbedError::LanguageNotAvailable(path, language_id.clone()), 113 | self.fallback_language); 114 | self.load_src_language(); 115 | return Ok(()); 116 | } 117 | }; 118 | 119 | let catalog = gettext_system::Catalog::parse(&**file).expect("could not parse the catalog"); 120 | tr::internal::set_translator(self.module, catalog); 121 | *(self.current_language.write()) = language_id.clone(); 122 | 123 | Ok(()) 124 | } 125 | 126 | fn reload(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> { 127 | self.load_languages(i18n_assets, &[self.current_language()]) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /i18n-embed/src/requester.rs: -------------------------------------------------------------------------------- 1 | use crate::{I18nEmbedError, Localizer}; 2 | use std::{collections::HashMap, sync::Weak}; 3 | 4 | /// A trait used by [I18nAssets](crate::I18nAssets) to ascertain which 5 | /// languages are being requested. 6 | pub trait LanguageRequester<'a> { 7 | /// Add a listener to this `LanguageRequester`. When the system 8 | /// reports that the currently requested languages has changed, 9 | /// each listener will have its 10 | /// [Localizer#select()](Localizer#select()) method called. [Weak] 11 | /// is used so that when the [Arc](std::sync::Arc) that it references 12 | /// is dropped, the listener will also be removed next time this 13 | /// requester is polled/updates. 14 | /// 15 | /// If you haven't already selected a language for the localizer 16 | /// you are adding here, you may want to manually call 17 | /// [#poll()](#poll()) after adding the listener/s. 18 | fn add_listener(&mut self, listener: Weak); 19 | /// Add a listener to this `LanguageRequester`. When the system 20 | /// reports that the currently requested languages has changed, 21 | /// each listener will have its 22 | /// [Localizer#select()](Localizer#select()) method called. As 23 | /// opposed to [LanguageRequester::add_listener()], this listener 24 | /// will not be removed. 25 | /// 26 | /// If you haven't already selected a language for the localizer 27 | /// you are adding here, you may want to manually call 28 | /// [#poll()](#poll()) after adding the listener/s. 29 | fn add_listener_ref(&mut self, listener: &'a dyn Localizer); 30 | /// Poll the system's currently selected language, and call 31 | /// [Localizer#select()](Localizer#select()) on each of the 32 | /// listeners. 33 | /// 34 | /// **NOTE:** Support for this across systems currently 35 | /// varies, it may not change when the system requested language 36 | /// changes during runtime without restarting your application. In 37 | /// the future some platforms may also gain support for automatic 38 | /// triggering when the requested display language changes. 39 | fn poll(&mut self) -> Result<(), I18nEmbedError>; 40 | /// Override the languages fed to the [Localizer](Localizer) listeners during 41 | /// a [#poll()](#poll()). Set this as `None` to disable the override. 42 | fn set_language_override( 43 | &mut self, 44 | language_override: Option, 45 | ) -> Result<(), I18nEmbedError>; 46 | /// The currently requested languages. 47 | fn requested_languages(&self) -> Vec; 48 | /// The languages reported to be available in the 49 | /// listener [Localizer](Localizer)s. 50 | fn available_languages(&self) -> Result, I18nEmbedError>; 51 | /// The languages currently loaded, keyed by the 52 | /// [LanguageLoader::domain()](crate::LanguageLoader::domain()). 53 | fn current_languages(&self) -> HashMap; 54 | } 55 | 56 | /// Provide the functionality for overrides and listeners for a 57 | /// [LanguageRequester](LanguageRequester) implementation. 58 | pub struct LanguageRequesterImpl<'a> { 59 | arc_listeners: Vec>, 60 | ref_listeners: Vec<&'a dyn Localizer>, 61 | language_override: Option, 62 | } 63 | 64 | impl<'a> LanguageRequesterImpl<'a> { 65 | /// Create a new [LanguageRequesterImpl](LanguageRequesterImpl). 66 | pub fn new() -> LanguageRequesterImpl<'a> { 67 | LanguageRequesterImpl { 68 | arc_listeners: Vec::new(), 69 | ref_listeners: Vec::new(), 70 | language_override: None, 71 | } 72 | } 73 | 74 | /// Set an override for the requested language which is used when the 75 | /// [LanguageRequesterImpl#poll()](LanguageRequester#poll()) method 76 | /// is called. If `None`, then no override is used. 77 | pub fn set_language_override( 78 | &mut self, 79 | language_override: Option, 80 | ) -> Result<(), I18nEmbedError> { 81 | self.language_override = language_override; 82 | Ok(()) 83 | } 84 | 85 | /// Add a weak reference to a [Localizer], which listens to 86 | /// changes to the current language. 87 | pub fn add_listener(&mut self, listener: Weak) { 88 | self.arc_listeners.push(listener); 89 | } 90 | 91 | /// Add a reference to [Localizer], which listens to changes to 92 | /// the current language. 93 | pub fn add_listener_ref(&mut self, listener: &'a dyn Localizer) { 94 | self.ref_listeners.push(listener); 95 | } 96 | 97 | /// With the provided `requested_languages` call 98 | /// [Localizer#select()](Localizer#select()) on each of the 99 | /// listeners. 100 | pub fn poll_without_override( 101 | &mut self, 102 | requested_languages: Vec, 103 | ) -> Result<(), I18nEmbedError> { 104 | let mut errors: Vec = Vec::new(); 105 | 106 | self.arc_listeners 107 | .retain(|listener| match listener.upgrade() { 108 | Some(arc_listener) => { 109 | if let Err(error) = arc_listener.select(&requested_languages) { 110 | errors.push(error); 111 | } 112 | 113 | true 114 | } 115 | None => false, 116 | }); 117 | 118 | for boxed_listener in &self.ref_listeners { 119 | if let Err(error) = boxed_listener.select(&requested_languages) { 120 | errors.push(error); 121 | } 122 | } 123 | 124 | if errors.is_empty() { 125 | Ok(()) 126 | } else if errors.len() == 1 { 127 | Err(errors.into_iter().next().unwrap()) 128 | } else { 129 | Err(I18nEmbedError::Multiple(errors)) 130 | } 131 | } 132 | 133 | /// With the provided `requested_languages` call 134 | /// [Localizer#select()](Localizer#select()) on each of the 135 | /// listeners. The `requested_languages` may be ignored if 136 | /// [#set_language_override()](#set_language_override()) has been 137 | /// set. 138 | pub fn poll( 139 | &mut self, 140 | requested_languages: Vec, 141 | ) -> Result<(), I18nEmbedError> { 142 | let languages = match &self.language_override { 143 | Some(language) => { 144 | log::debug!("Using language override: {}", language); 145 | vec![language.clone()] 146 | } 147 | None => requested_languages, 148 | }; 149 | 150 | self.poll_without_override(languages) 151 | } 152 | 153 | /// The languages reported to be available in the 154 | /// listener [Localizer](Localizer)s. 155 | pub fn available_languages( 156 | &self, 157 | ) -> Result, I18nEmbedError> { 158 | let mut available_languages = std::collections::HashSet::new(); 159 | 160 | for weak_arc_listener in &self.arc_listeners { 161 | if let Some(arc_listener) = weak_arc_listener.upgrade() { 162 | arc_listener 163 | .available_languages()? 164 | .iter() 165 | .for_each(|language| { 166 | available_languages.insert(language.clone()); 167 | }) 168 | } 169 | } 170 | 171 | for boxed_listener in &self.ref_listeners { 172 | boxed_listener 173 | .available_languages()? 174 | .iter() 175 | .for_each(|language| { 176 | available_languages.insert(language.clone()); 177 | }) 178 | } 179 | 180 | Ok(available_languages.into_iter().collect()) 181 | } 182 | 183 | /// Gets a `HashMap` with what each language is currently set 184 | /// (value) per domain (key). 185 | pub fn current_languages(&self) -> HashMap { 186 | let mut current_languages = HashMap::new(); 187 | for weak_listener in &self.arc_listeners { 188 | if let Some(localizer) = weak_listener.upgrade() { 189 | let loader = localizer.language_loader(); 190 | current_languages.insert(loader.domain().to_string(), loader.current_language()); 191 | } 192 | } 193 | 194 | current_languages 195 | } 196 | } 197 | 198 | impl Default for LanguageRequesterImpl<'_> { 199 | fn default() -> Self { 200 | LanguageRequesterImpl::new() 201 | } 202 | } 203 | 204 | impl std::fmt::Debug for LanguageRequesterImpl<'_> { 205 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 206 | let listeners_debug: String = self 207 | .arc_listeners 208 | .iter() 209 | .map(|l| match l.upgrade() { 210 | Some(l) => format!("{l:p}"), 211 | None => "None".to_string(), 212 | }) 213 | .collect::>() 214 | .join(", "); 215 | write!( 216 | f, 217 | "LanguageRequesterImpl(listeners: {}, language_override: {:?})", 218 | listeners_debug, self.language_override, 219 | ) 220 | } 221 | } 222 | 223 | /// A [LanguageRequester](LanguageRequester) for the desktop platform, 224 | /// supporting windows, linux and mac. It uses 225 | /// [locale_config](locale_config) to select the language based on the 226 | /// system selected language. 227 | /// 228 | /// ⚠️ *This API requires the following crate features to be activated: `desktop-requester`.* 229 | #[cfg(feature = "desktop-requester")] 230 | #[derive(Debug)] 231 | pub struct DesktopLanguageRequester<'a> { 232 | implementation: LanguageRequesterImpl<'a>, 233 | } 234 | 235 | #[cfg(feature = "desktop-requester")] 236 | impl<'a> LanguageRequester<'a> for DesktopLanguageRequester<'a> { 237 | fn requested_languages(&self) -> Vec { 238 | DesktopLanguageRequester::requested_languages() 239 | } 240 | 241 | fn add_listener(&mut self, listener: Weak) { 242 | self.implementation.add_listener(listener) 243 | } 244 | 245 | fn add_listener_ref(&mut self, listener: &'a dyn Localizer) { 246 | self.implementation.add_listener_ref(listener) 247 | } 248 | 249 | fn set_language_override( 250 | &mut self, 251 | language_override: Option, 252 | ) -> Result<(), I18nEmbedError> { 253 | self.implementation.set_language_override(language_override) 254 | } 255 | 256 | fn poll(&mut self) -> Result<(), I18nEmbedError> { 257 | self.implementation.poll(self.requested_languages()) 258 | } 259 | 260 | fn available_languages(&self) -> Result, I18nEmbedError> { 261 | self.implementation.available_languages() 262 | } 263 | 264 | fn current_languages(&self) -> HashMap { 265 | self.implementation.current_languages() 266 | } 267 | } 268 | 269 | #[cfg(feature = "desktop-requester")] 270 | impl Default for DesktopLanguageRequester<'_> { 271 | fn default() -> Self { 272 | DesktopLanguageRequester::new() 273 | } 274 | } 275 | 276 | #[cfg(feature = "desktop-requester")] 277 | impl DesktopLanguageRequester<'_> { 278 | /// Create a new `DesktopLanguageRequester`. 279 | pub fn new() -> Self { 280 | DesktopLanguageRequester { 281 | implementation: LanguageRequesterImpl::new(), 282 | } 283 | } 284 | 285 | /// The languages being requested by the operating 286 | /// system/environment according to the [locale_config] crate's 287 | /// implementation. 288 | pub fn requested_languages() -> Vec { 289 | use locale_config::{LanguageRange, Locale}; 290 | 291 | let current_locale = Locale::current(); 292 | 293 | let ids: Vec = current_locale 294 | .tags_for("messages") 295 | .filter_map(|tag: LanguageRange<'_>| match tag.to_string().parse() { 296 | Ok(tag) => Some(tag), 297 | Err(err) => { 298 | log::error!("Unable to parse your locale: {:?}", err); 299 | None 300 | } 301 | }) 302 | .collect(); 303 | 304 | log::info!("Current Locale: {:?}", ids); 305 | 306 | ids 307 | } 308 | } 309 | 310 | /// A [LanguageRequester](LanguageRequester) for the `web-sys` web platform. 311 | /// 312 | /// ⚠️ *This API requires the following crate features to be activated: `web-sys-requester`.* 313 | #[cfg(feature = "web-sys-requester")] 314 | #[derive(Debug)] 315 | pub struct WebLanguageRequester<'a> { 316 | implementation: LanguageRequesterImpl<'a>, 317 | } 318 | 319 | #[cfg(feature = "web-sys-requester")] 320 | impl WebLanguageRequester<'_> { 321 | /// Create a new `WebLanguageRequester`. 322 | pub fn new() -> Self { 323 | WebLanguageRequester { 324 | implementation: LanguageRequesterImpl::new(), 325 | } 326 | } 327 | 328 | /// The languages currently being requested by the browser context. 329 | pub fn requested_languages() -> Vec { 330 | use fluent_langneg::convert_vec_str_to_langids_lossy; 331 | let window = web_sys::window().expect("no global `window` exists"); 332 | let navigator = window.navigator(); 333 | let languages = navigator.languages(); 334 | 335 | let requested_languages = 336 | convert_vec_str_to_langids_lossy(languages.iter().map(|js_value| { 337 | js_value 338 | .as_string() 339 | .expect("language value should be a string.") 340 | })); 341 | 342 | requested_languages 343 | } 344 | } 345 | 346 | #[cfg(feature = "web-sys-requester")] 347 | impl Default for WebLanguageRequester<'_> { 348 | fn default() -> Self { 349 | WebLanguageRequester::new() 350 | } 351 | } 352 | 353 | #[cfg(feature = "web-sys-requester")] 354 | impl<'a> LanguageRequester<'a> for WebLanguageRequester<'a> { 355 | fn requested_languages(&self) -> Vec { 356 | Self::requested_languages() 357 | } 358 | 359 | fn add_listener(&mut self, listener: Weak) { 360 | self.implementation.add_listener(listener) 361 | } 362 | 363 | fn add_listener_ref(&mut self, listener: &'a dyn Localizer) { 364 | self.implementation.add_listener_ref(listener) 365 | } 366 | 367 | fn poll(&mut self) -> Result<(), I18nEmbedError> { 368 | self.implementation.poll(self.requested_languages()) 369 | } 370 | 371 | fn set_language_override( 372 | &mut self, 373 | language_override: Option, 374 | ) -> Result<(), I18nEmbedError> { 375 | self.implementation.set_language_override(language_override) 376 | } 377 | 378 | fn available_languages(&self) -> Result, I18nEmbedError> { 379 | self.implementation.available_languages() 380 | } 381 | 382 | fn current_languages(&self) -> HashMap { 383 | self.implementation.current_languages() 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /i18n-embed/src/util.rs: -------------------------------------------------------------------------------- 1 | /// Get the translation domain from the module path (first module in 2 | /// the module path). 3 | pub fn domain_from_module(module_path: &str) -> &str { 4 | module_path.split("::").next().unwrap_or(module_path) 5 | } 6 | -------------------------------------------------------------------------------- /i18n-embed/tests/loader.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "fluent-system", feature = "gettext-system"))] 2 | fn setup() { 3 | let _ = env_logger::try_init(); 4 | } 5 | 6 | #[cfg(feature = "fluent-system")] 7 | mod fluent { 8 | use super::setup; 9 | use fluent_langneg::NegotiationStrategy; 10 | use i18n_embed::{fluent::FluentLanguageLoader, LanguageLoader}; 11 | use rust_embed::RustEmbed; 12 | use unic_langid::LanguageIdentifier; 13 | 14 | #[derive(RustEmbed)] 15 | #[folder = "i18n/ftl"] 16 | struct Localizations; 17 | 18 | #[test] 19 | fn hello_world_en_us() { 20 | setup(); 21 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 22 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 23 | loader.load_languages(&Localizations, &[en_us]).unwrap(); 24 | pretty_assertions::assert_eq!("Hello World Localization!", loader.get("hello-world")); 25 | } 26 | 27 | #[test] 28 | fn fallback_en_gb_to_en_us() { 29 | setup(); 30 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 31 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 32 | 33 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 34 | loader.load_languages(&Localizations, &[en_gb]).unwrap(); 35 | pretty_assertions::assert_eq!("Hello World Localisation!", loader.get("hello-world")); 36 | pretty_assertions::assert_eq!("only US", loader.get("only-us")); 37 | } 38 | 39 | #[test] 40 | fn fallbacks_ru_to_en_gb_to_en_us() { 41 | setup(); 42 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 43 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 44 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 45 | 46 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 47 | loader.load_languages(&Localizations, &[ru, en_gb]).unwrap(); 48 | pretty_assertions::assert_eq!("Привет Мир Локализация!", loader.get("hello-world")); 49 | pretty_assertions::assert_eq!("only GB", loader.get("only-gb")); 50 | pretty_assertions::assert_eq!("only US", loader.get("only-us")); 51 | pretty_assertions::assert_eq!("только русский", loader.get("only-ru")); 52 | } 53 | 54 | #[test] 55 | fn args_fallback_ru_to_en_us() { 56 | setup(); 57 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 58 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 59 | 60 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 61 | loader.load_languages(&Localizations, &[ru]).unwrap(); 62 | 63 | let args = maplit::hashmap! { 64 | "userName" => "Tanya" 65 | }; 66 | pretty_assertions::assert_eq!( 67 | "Привет \u{2068}Tanya\u{2069}!", 68 | loader.get_args("only-ru-args", args) 69 | ); 70 | } 71 | 72 | #[test] 73 | fn attr() { 74 | setup(); 75 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 76 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 77 | loader.load_languages(&Localizations, &[en_us]).unwrap(); 78 | pretty_assertions::assert_eq!("World (US version)!", loader.get_attr("with-attr", "attr")); 79 | } 80 | 81 | #[test] 82 | fn attr_with_args() { 83 | setup(); 84 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 85 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 86 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 87 | loader.load_languages(&Localizations, &[en_gb]).unwrap(); 88 | let args = maplit::hashmap! { 89 | "name" => "Joe Doe" 90 | }; 91 | pretty_assertions::assert_eq!( 92 | "\u{2068}Joe Doe\u{2069}!", 93 | loader.get_attr_args("with-attr-and-args", "who", args) 94 | ); 95 | } 96 | 97 | #[test] 98 | fn has() { 99 | setup(); 100 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 101 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 102 | 103 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 104 | loader.load_languages(&Localizations, &[ru]).unwrap(); 105 | 106 | assert!(loader.has("only-ru-args")); 107 | assert!(loader.has("only-us")); 108 | assert!(!loader.has("non-existent-message")) 109 | } 110 | 111 | #[test] 112 | fn bidirectional_isolation_off() { 113 | setup(); 114 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 115 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 116 | loader.load_languages(&Localizations, &[en_us]).unwrap(); 117 | loader.set_use_isolating(false); 118 | let args = maplit::hashmap! { 119 | "thing" => "thing" 120 | }; 121 | let msg = loader.get_args("isolation-chars", args); 122 | assert_eq!("inject a thing here", msg); 123 | } 124 | 125 | #[test] 126 | fn bidirectional_isolation_on() { 127 | setup(); 128 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 129 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 130 | loader.load_languages(&Localizations, &[en_us]).unwrap(); 131 | let args = maplit::hashmap! { 132 | "thing" => "thing" 133 | }; 134 | let msg = loader.get_args("isolation-chars", args); 135 | assert_eq!("inject a \u{2068}thing\u{2069} here", msg); 136 | } 137 | 138 | #[test] 139 | fn multiline_lf() { 140 | setup(); 141 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 142 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 143 | loader.load_languages(&Localizations, &[en_us]).unwrap(); 144 | 145 | let msg = loader.get("multi-line"); 146 | assert_eq!( 147 | "This is a multi-line message.\n\n\ 148 | This is a multi-line message.\n\n\ 149 | Finished!", 150 | msg 151 | ); 152 | } 153 | 154 | #[test] 155 | fn multiline_crlf() { 156 | setup(); 157 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 158 | let loader = FluentLanguageLoader::new("test", ru.clone()); 159 | loader.load_languages(&Localizations, &[ru]).unwrap(); 160 | 161 | let msg = loader.get("multi-line"); 162 | assert_eq!( 163 | "Это многострочное сообщение.\n\n\ 164 | Это многострочное сообщение.\n\n\ 165 | Законченный!", 166 | msg 167 | ); 168 | } 169 | 170 | #[test] 171 | fn multiline_arguments_lf() { 172 | setup(); 173 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 174 | let loader = FluentLanguageLoader::new("test", en_us.clone()); 175 | loader.load_languages(&Localizations, &[en_us]).unwrap(); 176 | 177 | let args = maplit::hashmap! { 178 | "argOne" => "1", 179 | "argTwo" => "2", 180 | }; 181 | 182 | let msg = loader.get_args("multi-line-args", args); 183 | assert_eq!( 184 | "This is a multiline message with arguments.\n\n\ 185 | \u{2068}1\u{2069}\n\n\ 186 | This is a multiline message with arguments.\n\n\ 187 | \u{2068}2\u{2069}\n\n\ 188 | Finished!", 189 | msg 190 | ); 191 | } 192 | 193 | #[test] 194 | fn multiline_arguments_crlf() { 195 | setup(); 196 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 197 | let loader = FluentLanguageLoader::new("test", ru.clone()); 198 | loader.load_languages(&Localizations, &[ru]).unwrap(); 199 | 200 | let args = maplit::hashmap! { 201 | "argOne" => "1", 202 | "argTwo" => "2", 203 | }; 204 | 205 | let msg = loader.get_args("multi-line-args", args); 206 | assert_eq!( 207 | "Это многострочное сообщение с параметрами.\n\n\ 208 | \u{2068}1\u{2069}\n\n\ 209 | Это многострочное сообщение с параметрами.\n\n\ 210 | \u{2068}2\u{2069}\n\n\ 211 | Законченный!", 212 | msg 213 | ); 214 | } 215 | 216 | #[test] 217 | fn select_languages_get_default_fallback() { 218 | setup(); 219 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 220 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 221 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 222 | let loader = FluentLanguageLoader::new("test", en_us); 223 | 224 | loader 225 | .load_languages(&Localizations, &[ru.clone(), en_gb]) 226 | .unwrap(); 227 | 228 | let msg = loader.select_languages(&[ru.clone()]).get("only-ru"); 229 | assert_eq!("только русский", msg); 230 | 231 | let msg = loader.select_languages(&[ru]).get("only-gb"); 232 | assert_eq!("only GB (US Version)", msg); 233 | } 234 | 235 | #[test] 236 | fn select_languages_get_args_default_fallback() { 237 | setup(); 238 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 239 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 240 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 241 | let loader = FluentLanguageLoader::new("test", en_us); 242 | 243 | loader 244 | .load_languages(&Localizations, &[ru.clone(), en_gb]) 245 | .unwrap(); 246 | 247 | let args = maplit::hashmap! { 248 | "argOne" => "1", 249 | "argTwo" => "2", 250 | }; 251 | 252 | let msg = loader 253 | .select_languages(&[ru]) 254 | .get_args("multi-line-args", args); 255 | assert_eq!( 256 | "Это многострочное сообщение с параметрами.\n\n\ 257 | \u{2068}1\u{2069}\n\n\ 258 | Это многострочное сообщение с параметрами.\n\n\ 259 | \u{2068}2\u{2069}\n\n\ 260 | Законченный!", 261 | msg 262 | ); 263 | } 264 | 265 | #[test] 266 | fn select_languages_get_custom_fallback() { 267 | setup(); 268 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 269 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 270 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 271 | let loader = FluentLanguageLoader::new("test", en_us); 272 | 273 | loader 274 | .load_languages(&Localizations, &[ru.clone(), en_gb.clone()]) 275 | .unwrap(); 276 | 277 | let msg = loader 278 | .select_languages(&[ru.clone(), en_gb.clone()]) 279 | .get("only-gb"); 280 | assert_eq!("only GB", msg); 281 | 282 | let msg = loader.select_languages(&[ru, en_gb]).get("only-us"); 283 | assert_eq!("only US", msg); 284 | } 285 | 286 | #[test] 287 | fn select_languages_get_args_custom_fallback() { 288 | setup(); 289 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 290 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 291 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 292 | let loader = FluentLanguageLoader::new("test", en_us); 293 | 294 | loader 295 | .load_languages(&Localizations, &[ru.clone(), en_gb.clone()]) 296 | .unwrap(); 297 | 298 | let args = maplit::hashmap! { 299 | "userName" => "username", 300 | }; 301 | 302 | let msg = loader 303 | .select_languages(&[ru.clone()]) 304 | .get_args("only-gb-args", args.clone()); 305 | assert_eq!("Hello \u{2068}username\u{2069}! (US Version)", msg); 306 | 307 | let msg = loader 308 | .select_languages(&[ru, en_gb]) 309 | .get_args("only-gb-args", args.clone()); 310 | assert_eq!("Hello \u{2068}username\u{2069}!", msg); 311 | } 312 | 313 | #[test] 314 | fn select_languages_negotiate() { 315 | setup(); 316 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 317 | let en_gb: LanguageIdentifier = "en-GB".parse().unwrap(); 318 | let en_us: LanguageIdentifier = "en-US".parse().unwrap(); 319 | let loader = FluentLanguageLoader::new("test", en_us); 320 | 321 | loader.load_available_languages(&Localizations).unwrap(); 322 | 323 | let msg = loader 324 | .select_languages_negotiate(&[&ru, &en_gb], NegotiationStrategy::Filtering) 325 | .get("only-gb-us"); 326 | assert_eq!("only GB US (GB)", msg); 327 | } 328 | } 329 | 330 | #[cfg(feature = "gettext-system")] 331 | mod gettext { 332 | use super::setup; 333 | use i18n_embed::{gettext::GettextLanguageLoader, LanguageLoader}; 334 | use rust_embed::RustEmbed; 335 | use serial_test::serial; 336 | use tr::internal::with_translator; 337 | use unic_langid::LanguageIdentifier; 338 | 339 | /// Custom version of the tr! macro function, without the runtime 340 | /// formatting, with the module set to `i18n_embed` where the 341 | /// strings were originally extracted from. 342 | fn tr(msgid: &str) -> String { 343 | with_translator("i18n_embed", |t| t.translate(msgid, None).to_string()) 344 | } 345 | 346 | #[derive(RustEmbed)] 347 | #[folder = "i18n/mo"] 348 | struct Localizations; 349 | 350 | #[test] 351 | #[serial] 352 | fn only_en() { 353 | setup(); 354 | 355 | let loader = GettextLanguageLoader::new("i18n_embed", "en".parse().unwrap()); 356 | 357 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 358 | let en: LanguageIdentifier = "en".parse().unwrap(); 359 | 360 | loader.load_languages(&Localizations, &[ru]).unwrap(); 361 | 362 | // It should replace the ru with en 363 | loader.load_languages(&Localizations, &[en]).unwrap(); 364 | 365 | pretty_assertions::assert_eq!("only en", tr("only en")); 366 | pretty_assertions::assert_eq!("only ru", tr("only ru")); 367 | } 368 | 369 | #[test] 370 | #[serial] 371 | fn fallback_ru_to_en() { 372 | setup(); 373 | 374 | let loader = GettextLanguageLoader::new("i18n_embed", "en".parse().unwrap()); 375 | 376 | let ru: LanguageIdentifier = "ru".parse().unwrap(); 377 | 378 | assert!(Localizations::get("ru/i18n_embed.mo").is_some()); 379 | loader.load_languages(&Localizations, &[ru]).unwrap(); 380 | 381 | pretty_assertions::assert_eq!("только ру", tr("only ru")); 382 | pretty_assertions::assert_eq!("only en", tr("only en")); 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /i18n.toml: -------------------------------------------------------------------------------- 1 | # (Optional) Specify which subcrates to perform localization within, 2 | # the subcrate needs to have its own `i18n.toml`. 3 | subcrates = ["i18n-build"] 4 | 5 | # (Required) The language identifier of the language used in the 6 | # source code for gettext system, and the primary fallback language 7 | # (for which all strings must be present) when using the fluent 8 | # system. 9 | fallback_language = "en-US" 10 | 11 | # (Optional) Use the gettext localization system. 12 | [gettext] 13 | # (Required) The languages that the software will be translated into. 14 | target_languages = ["ru", "de", "fr"] 15 | 16 | # (Required) Path to the output directory, relative to `i18n.toml` of 17 | # the crate being localized. 18 | output_dir = "i18n" 19 | 20 | # (Optional) The reporting address for msgid bugs. This is the email 21 | # address or URL to which the translators shall report bugs in the 22 | # untranslated strings. 23 | msg_bugs_address = "l.frisken@gmail.com" 24 | 25 | # (Optional) Set the copyright holder for the generated files. 26 | copyright_holder = "Luke Frisken" -------------------------------------------------------------------------------- /i18n/TRANSLATORS: -------------------------------------------------------------------------------- 1 | # This file lists all PUBLIC individuals having contributed content to the translation. 2 | # Entries are in alphabetical order. 3 | 4 | Anna Abramova 5 | Christophe Chauvet 6 | Nick Flueckiger -------------------------------------------------------------------------------- /i18n/mo/de/cargo_i18n.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n/mo/de/cargo_i18n.mo -------------------------------------------------------------------------------- /i18n/mo/fr/cargo_i18n.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n/mo/fr/cargo_i18n.mo -------------------------------------------------------------------------------- /i18n/mo/ru/cargo_i18n.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/cargo-i18n/ceb3da0ee3acf91b17a7a52e02642267ddb47a3d/i18n/mo/ru/cargo_i18n.mo -------------------------------------------------------------------------------- /i18n/po/de/cargo_i18n.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cargo-i18n\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2021-08-26 12:51+0000\n" 6 | "Language: de\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "X-Generator: POEditor.com\n" 11 | 12 | #. The help message displayed when running `cargo i18n -h`. 13 | #: src/main.rs:29 14 | msgid "A Cargo sub-command to extract and build localization resources." 15 | msgstr "" 16 | "Ein Cargo-Unterbefehl zur Extrahierung und Erstellung von " 17 | "Lokalisierungsressourcen." 18 | 19 | #. The help message displayed when running `cargo i18n --help`. 20 | #. {0} is the short about message. 21 | #. {1} is a list of available languages. e.g. "en", "ru", etc 22 | #: src/main.rs:46 23 | msgid "" 24 | "{0}\n" 25 | "\n" 26 | "This command reads a configuration file (typically called \"i18n.toml\") in " 27 | "the root directory of your crate, and then proceeds to extract localization " 28 | "resources from your source files, and build them.\n" 29 | "\n" 30 | "If you are using the gettext localization system, you will need to have the " 31 | "following gettext tools installed: \"msgcat\", \"msginit\", \"msgmerge\" and " 32 | "\"msgfmt\". You will also need to have the \"xtr\" tool installed, which can " 33 | "be installed using \"cargo install xtr\".\n" 34 | "\n" 35 | "You can the \"i18n-embed\" library to conveniently embed the localizations " 36 | "inside your application.\n" 37 | "\n" 38 | "The display language used for this command is selected automatically using " 39 | "your system settings (as described at \n" 40 | "https://github.com/rust-locale/locale_config#supported-systems ) however you " 41 | "can override it using the -l, --language option.\n" 42 | "\n" 43 | "Logging for this command is available using the \"env_logger\" crate. You " 44 | "can enable debug logging using \"RUST_LOG=debug cargo i18n\"." 45 | msgstr "" 46 | 47 | #. The message displayed when running the binary outside of cargo using `cargo-18n`. 48 | #: src/main.rs:97 49 | msgid "" 50 | "This binary is designed to be executed as a cargo subcommand using \"cargo " 51 | "i18n\"." 52 | msgstr "" 53 | "Diese Binary ist für die Ausführung als Cargo-Unterbefehl mit \"cargo i18n\" " 54 | "vorgesehen." 55 | 56 | #. The help message for the `--path` command line argument. 57 | #: src/main.rs:109 58 | msgid "" 59 | "Path to the crate you want to localize (if not the current directory). The " 60 | "crate needs to contain \"i18n.toml\" in its root." 61 | msgstr "" 62 | "Pfad zum Crate, welches Du lokalisieren möchtest (wenn nicht das aktuelle " 63 | "Verzeichnis verwendet werden soll). Das Crate muss \"i18n.toml\" in seinem " 64 | "Stammverzeichnis enthalten." 65 | 66 | #. The help message for the `-c`, `--config-file-name` command line argument. 67 | #: src/main.rs:118 68 | msgid "The name of the i18n config file for this crate" 69 | msgstr "Der Name der i18n Konfigurationsdatei für dieses Crate" 70 | 71 | #. The help message for the `-l`, `--language` command line argument. 72 | #: src/main.rs:129 73 | msgid "" 74 | "Set the language to use for this application. Overrides the language " 75 | "selected automatically by your operating system." 76 | msgstr "" 77 | "Setze die Sprache, welche für die Applikation verwendet werden soll. Die " 78 | "gesetzte Sprache überschreibt die Sprache, welche vom Betriebssystem gesetzt " 79 | "worden ist." 80 | -------------------------------------------------------------------------------- /i18n/po/fr/cargo_i18n.po: -------------------------------------------------------------------------------- 1 | # French translations for cargo-i18n package. 2 | # Copyright (C) 2021 Luke Frisken 3 | # This file is distributed under the same license as the cargo-i18n package. 4 | # , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: cargo-i18n 0.2.7\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-29 17:21+0000\n" 11 | "PO-Revision-Date: 2021-11-29 20:06+0300\n" 12 | "Last-Translator: Christophe. chauvet\n" 13 | "Language: fr\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 18 | 19 | #. The help message displayed when running `cargo i18n -h`. 20 | #: src/main.rs:29 21 | msgid "A Cargo sub-command to extract and build localization resources." 22 | msgstr "" 23 | "Une sous-commande Cargo pour extraire et créer des ressources de traduction." 24 | 25 | #. The help message displayed when running `cargo i18n --help`. 26 | #. {0} is the short about message. 27 | #. {1} is a list of available languages. e.g. "en", "ru", etc 28 | #: src/main.rs:46 29 | msgid "" 30 | "{0}\n" 31 | "\n" 32 | "This command reads a configuration file (typically called \"i18n.toml\") in " 33 | "the root directory of your crate, and then proceeds to extract localization " 34 | "resources from your source files, and build them.\n" 35 | "\n" 36 | "If you are using the gettext localization system, you will need to have the " 37 | "following gettext tools installed: \"msgcat\", \"msginit\", \"msgmerge\" and " 38 | "\"msgfmt\". You will also need to have the \"xtr\" tool installed, which can " 39 | "be installed using \"cargo install xtr\".\n" 40 | "\n" 41 | "You can the \"i18n-embed\" library to conveniently embed the localizations " 42 | "inside your application.\n" 43 | "\n" 44 | "The display language used for this command is selected automatically using " 45 | "your system settings (as described at \n" 46 | "https://github.com/rust-locale/locale_config#supported-systems ) however you " 47 | "can override it using the -l, --language option.\n" 48 | "\n" 49 | "Logging for this command is available using the \"env_logger\" crate. You " 50 | "can enable debug logging using \"RUST_LOG=debug cargo i18n\"." 51 | msgstr "" 52 | "{0}\n" 53 | "\n" 54 | "Cette commande lit le fichier de configuration (généralement appelé \"i18n." 55 | "toml\") dans le répertoire racine de votre caisse, puis procède à " 56 | "l'extraction des ressources de traduction de vos fichiers sources et à leur " 57 | "construction.\n" 58 | "\n" 59 | "Si vous utilisez le système de localisation gettext, vous aurez besoin des " 60 | "outils gettext suivants installés : \"msgcat\", \"msginit\", \"msgmerge\" et " 61 | "\"msgfmt\". Vous aurez également besoin d'avoir installé l'outil \"xtr\", " 62 | "qui peut être installé à l'aide de la commande \"cargo install xtr\".\n" 63 | "\n" 64 | "Vous pouvez utiliser la bibliothèque \"i18n-embed\" pour intégrer facilement " 65 | "les localisations dans votre application.\n" 66 | "\n" 67 | "La langue d'affichage utilisée pour cette commande est sélectionnée " 68 | "automatiquement à l'aide des paramètres de votre système (comme décrit dans\n" 69 | "https://github.com/rust-locale/locale_config#supported-systems ) mais vous " 70 | "pouvez le remplacer en utilisant l'option -l, --language.\n" 71 | "\n" 72 | "La journalisation de cette commande est disponible à l'aide de la caisse " 73 | "\"env_logger\". Vous pouvez activer la journalisation de débogage en " 74 | "utilisant \"RUST_LOG=debug cargo i18n\"." 75 | 76 | #. The message displayed when running the binary outside of cargo using `cargo-18n`. 77 | #: src/main.rs:97 78 | msgid "" 79 | "This binary is designed to be executed as a cargo subcommand using \"cargo " 80 | "i18n\"." 81 | msgstr "" 82 | "Ce binaire est conçu pour être exécuté en tant que sous-commande cargo en " 83 | "utilisant la commande \"cargo i18n\"." 84 | 85 | #. The help message for the `--path` command line argument. 86 | #: src/main.rs:109 87 | msgid "" 88 | "Path to the crate you want to localize (if not the current directory). The " 89 | "crate needs to contain \"i18n.toml\" in its root." 90 | msgstr "" 91 | "Chemin d'accès à la caisse que vous souhaitez localiser (s'il ne s'agit pas " 92 | "du répertoire actuel). La caisse doit contenir un fichier \"i18n.toml\" à la " 93 | "racine." 94 | 95 | #. The help message for the `-c`, `--config-file-name` command line argument. 96 | #: src/main.rs:118 97 | msgid "The name of the i18n config file for this crate" 98 | msgstr "Le nom du fichier de configuration i18n pour cette caisse" 99 | 100 | #. The help message for the `-l`, `--language` command line argument. 101 | #: src/main.rs:129 102 | msgid "" 103 | "Set the language to use for this application. Overrides the language " 104 | "selected automatically by your operating system." 105 | msgstr "" 106 | "Définissez la langue à utiliser pour cette application. Remplace la langue " 107 | "sélectionnée automatiquement par le système d'exploitation." 108 | -------------------------------------------------------------------------------- /i18n/po/ru/cargo_i18n.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cargo-i18n\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2021-11-29 17:06+0000\n" 6 | "Language: ru\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "X-Generator: POEditor.com\n" 11 | 12 | #. The help message displayed when running `cargo i18n -h`. 13 | #: src/main.rs:29 14 | msgid "A Cargo sub-command to extract and build localization resources." 15 | msgstr "Подкоманда Cargo для извлечения и компилирования ресурсов локализации." 16 | 17 | #. The help message displayed when running `cargo i18n --help`. 18 | #. {0} is the short about message. 19 | #. {1} is a list of available languages. e.g. "en", "ru", etc 20 | #: src/main.rs:46 21 | msgid "" 22 | "{0}\n" 23 | "\n" 24 | "This command reads a configuration file (typically called \"i18n.toml\") in " 25 | "the root directory of your crate, and then proceeds to extract localization " 26 | "resources from your source files, and build them.\n" 27 | "\n" 28 | "If you are using the gettext localization system, you will need to have the " 29 | "following gettext tools installed: \"msgcat\", \"msginit\", \"msgmerge\" and " 30 | "\"msgfmt\". You will also need to have the \"xtr\" tool installed, which can " 31 | "be installed using \"cargo install xtr\".\n" 32 | "\n" 33 | "You can the \"i18n-embed\" library to conveniently embed the localizations " 34 | "inside your application.\n" 35 | "\n" 36 | "The display language used for this command is selected automatically using " 37 | "your system settings (as described at \n" 38 | "https://github.com/rust-locale/locale_config#supported-systems ) however you " 39 | "can override it using the -l, --language option.\n" 40 | "\n" 41 | "Logging for this command is available using the \"env_logger\" crate. You " 42 | "can enable debug logging using \"RUST_LOG=debug cargo i18n\"." 43 | msgstr "" 44 | "{0}\n" 45 | "\n" 46 | "Эта команда читает файл конфигурации (обычно называемый \"i18n.toml\") в " 47 | "корневом каталоге вашего крэйтора, а затем продолжает извлекать ресурсы " 48 | "локализации из ваших исходных файлов и компилировать их.\n" 49 | "\n" 50 | "Если вы используете систему локализации gettext, вам необходимо установить " 51 | "следующие инструменты gettext: \"msgcat\", \"msginit\", \"msgmerge\" и " 52 | "\"msgfmt\". Вам также необходимо установить инструмент «xtr», который можно " 53 | "установить с помощью \"cargo install xtr\".\n" 54 | "\n" 55 | "Вы можете использовать пакет \"18n-embed\" для удобного встраивания " 56 | "локализаций в ваше приложение.\n" 57 | "\n" 58 | "Язык отображения, используемый для этой команды, выбирается автоматически с " 59 | "использованием системных настроек (как описано в\n" 60 | "https://github.com/rust-locale/locale_config#supported-systems ) однако вы " 61 | "можете переопределить его, используя -l, --language option.\n" 62 | "\n" 63 | "Протоколирование для этой команды доступно с использованием крэйтора " 64 | "\"env_logger\". Вы можете включить протоколирование отладки, используя " 65 | "\"RUST_LOG=debug cargo i18n\"" 66 | 67 | #. The message displayed when running the binary outside of cargo using `cargo-18n`. 68 | #: src/main.rs:97 69 | msgid "" 70 | "This binary is designed to be executed as a cargo subcommand using \"cargo " 71 | "i18n\"." 72 | msgstr "" 73 | "Этот двоичный файл предназначен для выполнения в качестве подкоманды Cargo с " 74 | "использованием \"cargo i18n\"." 75 | 76 | #. The help message for the `--path` command line argument. 77 | #: src/main.rs:109 78 | msgid "" 79 | "Path to the crate you want to localize (if not the current directory). The " 80 | "crate needs to contain \"i18n.toml\" in its root." 81 | msgstr "" 82 | "Путь к крэйтору, который вы хотите локализовать (если это не текущий " 83 | "каталог). Крэйтор должен содержать \"i18n.toml\" в своем корне." 84 | 85 | #. The help message for the `-c`, `--config-file-name` command line argument. 86 | #: src/main.rs:118 87 | msgid "The name of the i18n config file for this crate" 88 | msgstr "Имя файла конфигурации i18n для этого крэйтора." 89 | 90 | #. The help message for the `-l`, `--language` command line argument. 91 | #: src/main.rs:129 92 | msgid "" 93 | "Set the language to use for this application. Overrides the language " 94 | "selected automatically by your operating system." 95 | msgstr "" 96 | "Установите язык для использования в этом приложении. Переопределяет язык, " 97 | "выбранный автоматически вашей операционной системой." 98 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(doctest)] 2 | #[macro_use] 3 | extern crate doc_comment; 4 | 5 | #[cfg(doctest)] 6 | doctest!("../README.md"); 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{builder::PossibleValuesParser, crate_authors, crate_version, Arg, Command}; 3 | use i18n_build::run; 4 | use i18n_config::Crate; 5 | use i18n_embed::{ 6 | gettext::{gettext_language_loader, GettextLanguageLoader}, 7 | DefaultLocalizer, DesktopLanguageRequester, LanguageLoader, LanguageRequester, Localizer, 8 | }; 9 | use rust_embed::RustEmbed; 10 | use std::{ 11 | path::{Path, PathBuf}, 12 | sync::{Arc, OnceLock}, 13 | }; 14 | use tr::tr; 15 | use unic_langid::LanguageIdentifier; 16 | 17 | #[derive(RustEmbed)] 18 | #[folder = "i18n/mo"] 19 | struct Translations; 20 | 21 | static TRANSLATIONS: Translations = Translations {}; 22 | 23 | fn language_loader() -> &'static GettextLanguageLoader { 24 | static LANGUAGE_LOADER: OnceLock = OnceLock::new(); 25 | 26 | LANGUAGE_LOADER.get_or_init(|| gettext_language_loader!()) 27 | } 28 | 29 | /// Produce the message to be displayed when running `cargo i18n -h`. 30 | fn short_about() -> String { 31 | // The help message displayed when running `cargo i18n -h`. 32 | tr!("A Cargo sub-command to extract and build localization resources.") 33 | } 34 | 35 | fn available_languages(localizer: &dyn Localizer) -> Result> { 36 | Ok(localizer 37 | .available_languages()? 38 | .iter() 39 | .map(|li| li.to_string()) 40 | .collect()) 41 | } 42 | 43 | /// Produce the message to be displayed when running `cargo i18n --help`. 44 | fn long_about() -> String { 45 | tr!( 46 | // The help message displayed when running `cargo i18n --help`. 47 | // {0} is the short about message. 48 | // {1} is a list of available languages. e.g. "en", "ru", etc 49 | "{0} 50 | 51 | This command reads a configuration file (typically called \"i18n.toml\") \ 52 | in the root directory of your crate, and then proceeds to extract \ 53 | localization resources from your source files, and build them. 54 | 55 | If you are using the gettext localization system, you will \ 56 | need to have the following gettext tools installed: \"msgcat\", \ 57 | \"msginit\", \"msgmerge\" and \"msgfmt\". You will also need to have \ 58 | the \"xtr\" tool installed, which can be installed using \"cargo \ 59 | install xtr\". 60 | 61 | You can the \"i18n-embed\" library to conveniently embed the \ 62 | localizations inside your application. 63 | 64 | The display language used for this command is selected automatically \ 65 | using your system settings (as described at 66 | https://github.com/rust-locale/locale_config#supported-systems ) \ 67 | however you can override it using the -l, --language option. 68 | 69 | Logging for this command is available using the \"env_logger\" crate. \ 70 | You can enable debug logging using \"RUST_LOG=debug cargo i18n\".", 71 | short_about() 72 | ) 73 | } 74 | 75 | fn main() -> Result<()> { 76 | env_logger::init(); 77 | let mut language_requester = DesktopLanguageRequester::new(); 78 | 79 | let cargo_i18n_localizer: DefaultLocalizer<'static> = 80 | DefaultLocalizer::new(&*language_loader(), &TRANSLATIONS); 81 | 82 | let cargo_i18n_localizer_rc: Arc = Arc::new(cargo_i18n_localizer); 83 | let i18n_build_localizer_rc: Arc = Arc::new(i18n_build::localizer()); 84 | 85 | language_requester.add_listener(Arc::downgrade(&cargo_i18n_localizer_rc)); 86 | language_requester.add_listener(Arc::downgrade(&i18n_build_localizer_rc)); 87 | language_requester.poll()?; 88 | 89 | let fallback_locale: &'static str = 90 | String::leak(language_loader().fallback_language().to_string()); 91 | let available_languages = available_languages(&*cargo_i18n_localizer_rc)?; 92 | let available_languages_slice: Vec<&'static str> = available_languages 93 | .into_iter() 94 | .map(|l| String::leak(l) as &str) 95 | .collect(); 96 | 97 | let matches = Command::new("cargo-i18n") 98 | .bin_name("cargo") 99 | .term_width(80) 100 | .about( 101 | tr!( 102 | // The message displayed when running the binary outside of cargo using `cargo-18n`. 103 | "This binary is designed to be executed as a cargo subcommand using \"cargo i18n\".") 104 | ) 105 | .version(crate_version!()) 106 | .author(crate_authors!()) 107 | .subcommand(Command::new("i18n") 108 | .about(short_about()) 109 | .long_about(long_about()) 110 | .version(crate_version!()) 111 | .author(crate_authors!()) 112 | .arg(Arg::new("path") 113 | .help( 114 | // The help message for the `--path` command line argument. 115 | tr!("Path to the crate you want to localize (if not the current directory). The crate needs to contain \"i18n.toml\" in its root.") 116 | ) 117 | .long("path") 118 | .num_args(1) 119 | ) 120 | .arg(Arg::new("config-file-name") 121 | .help( 122 | tr!( 123 | // The help message for the `-c`, `--config-file-name` command line argument. 124 | "The name of the i18n config file for this crate") 125 | ) 126 | .long("config-file-name") 127 | .short('c') 128 | .num_args(1) 129 | .default_value("i18n.toml") 130 | ) 131 | .arg(Arg::new("language") 132 | .help( 133 | tr!( 134 | // The help message for the `-l`, `--language` command line argument. 135 | "Set the language to use for this application. Overrides the language selected automatically by your operating system." 136 | ) 137 | ) 138 | .long("language") 139 | .short('l') 140 | .num_args(1) 141 | .default_value(fallback_locale) 142 | .value_parser(PossibleValuesParser::new(available_languages_slice)) 143 | ) 144 | ) 145 | .get_matches(); 146 | 147 | if let Some(i18n_matches) = matches.subcommand_matches("i18n") { 148 | let config_file_name: &String = i18n_matches 149 | .get_one("config-file-name") 150 | .expect("expected a default config file name to be present"); 151 | 152 | let language: &String = i18n_matches 153 | .get_one("language") 154 | .expect("expected a default language to be present"); 155 | let li: LanguageIdentifier = language.parse()?; 156 | 157 | language_requester.set_language_override(Some(li))?; 158 | language_requester.poll()?; 159 | 160 | let path = i18n_matches 161 | .get_one::("path") 162 | .map(ToOwned::to_owned) 163 | .unwrap_or_else(|| PathBuf::from(".")); 164 | 165 | let config_file_path = Path::new(config_file_name).to_path_buf(); 166 | 167 | i18n_build::util::check_path_exists(&path)?; 168 | i18n_build::util::check_path_exists(path.join(&config_file_path))?; 169 | 170 | let crt: Crate = Crate::from(path, None, config_file_path)?; 171 | run(crt)?; 172 | } 173 | 174 | Ok(()) 175 | } 176 | --------------------------------------------------------------------------------