# Rust I18n [![CI](https://github.com/longbridgeapp/rust-i18n/actions/workflows/ci.yml/badge.svg)](https://github.com/longbridgeapp/rust-i18n/actions/workflows/ci.yml) [![Docs](https://docs.rs/rust-i18n/badge.svg)](https://docs.rs/rust-i18n/) [![Crates.io](https://img.shields.io/crates/v/rust-i18n.svg)](https://crates.io/crates/rust-i18n) > 🎯 Let's make I18n things to easy! Rust I18n is a crate for loading localized text from a set of (YAML, JSON or TOML) mapping files. The mappings are converted into data readable by Rust programs at compile time, and then localized text can be loaded by simply calling the provided `t!` macro. Unlike other I18n libraries, Rust I18n's goal is to provide a simple and easy-to-use API. The API of this crate is inspired by [ruby-i18n](https://github.com/ruby-i18n/i18n) and [Rails I18n](https://guides.rubyonrails.org/i18n.html). ## Features - Codegen on compile time for includes translations into binary. - Global `t!` macro for loading localized text in everywhere. - Use YAML (default), JSON or TOML format for mapping localized text, and support mutiple files merging. - `cargo i18n` Command line tool for checking and extract untranslated texts into YAML files. - Support all localized texts in one file, or split into difference files by locale. ## Usage Add crate dependencies in your Cargo.toml and setup I18n config: ```toml [dependencies] rust-i18n = "3" ``` Load macro and init translations in `lib.rs` or `main.rs`: ```rust,compile_fail,no_run // Load I18n macro, for allow you use `t!` macro in anywhere. #[macro_use] extern crate rust_i18n; // Init translations for current crate. i18n!("locales"); // Or just use `i18n!`, default locales path is: "locales" in current crate. // // i18n!(); // Config fallback missing translations to "en" locale. // Use `fallback` option to set fallback locale. // // i18n!("locales", fallback = "en"); // Or more than one fallback with priority. // // i18n!("locales", fallback = ["en", "es]); ``` Or you can import by use directly: ```rust,no_run // You must import in each files when you wants use `t!` macro. use rust_i18n::t; rust_i18n::i18n!("locales"); fn main() { println!("{}", t!("hello")); // Use `available_locales!` method to get all available locales. println!("{:?}", rust_i18n::available_locales!()); } ``` ## Locale file You can use `_version` key to specify the version of the locale file, and the default value is `1`. ### Split Localized Texts into Difference Files > \_version: 1 You can also split the each language into difference files, and you can choise (YAML, JSON, TOML), for example: `en.json`: ```bash . β”œβ”€β”€ Cargo.lock β”œβ”€β”€ Cargo.toml β”œβ”€β”€ locales β”‚ β”œβ”€β”€ zh-CN.yml β”‚ β”œβ”€β”€ en.yml └── src β”‚ └── main.rs ``` ```yml _version: 1 hello: 'Hello world' messages.hello: 'Hello, %{name}' ``` Or use JSON or TOML format, just rename the file to `en.json` or `en.toml`, and the content is like this: ```json { "_version": 1, "hello": "Hello world", "messages.hello": "Hello, %{name}" } ``` ```toml hello = "Hello world" [messages] hello = "Hello, %{name}" ``` ### All Localized Texts in One File > \_version: 2 Make sure all localized files (containing the localized mappings) are located in the `locales/` folder of the project root directory: ```bash . β”œβ”€β”€ Cargo.lock β”œβ”€β”€ Cargo.toml β”œβ”€β”€ locales β”‚ β”œβ”€β”€ app.yml β”‚ β”œβ”€β”€ some-module.yml └── src β”‚ └── main.rs └── sub_app β”‚ └── locales β”‚ β”‚ └── app.yml β”‚ └── src β”‚ β”‚ └── main.rs β”‚ └── Cargo.toml ``` In the localized files, specify the localization keys and their corresponding values, for example, in `app.yml`: ```yml _version: 2 hello: en: Hello world zh-CN: δ½ ε₯½δΈ–η•Œ messages.hello: en: Hello, %{name} zh-CN: δ½ ε₯½οΌŒ%{name} ``` This is useful when you use [GitHub Copilot](https://github.com/features/copilot), after you write a first translated text, then Copilot will auto generate other locale's translations for you. ### Get Localized Strings in Rust Import the `t!` macro from this crate into your current scope: ```rust,no_run use rust_i18n::t; ``` Then, simply use it wherever a localized string is needed: ```rust,no_run # fn _rust_i18n_translate(locale: &str, key: &str) -> String { todo!() } # fn main() { use rust_i18n::t; t!("hello"); // => "Hello world" t!("hello", locale = "zh-CN"); // => "δ½ ε₯½δΈ–η•Œ" t!("messages.hello", name = "world"); // => "Hello, world" t!("messages.hello", "name" => "world"); // => "Hello, world" t!("messages.hello", locale = "zh-CN", name = "Jason", count = 2); // => "δ½ ε₯½οΌŒJason (2)" t!("messages.hello", locale = "zh-CN", "name" => "Jason", "count" => 3 + 2); // => "δ½ ε₯½οΌŒJason (5)" # } ``` ### Current Locale You can use `rust_i18n::set_locale` to set the global locale at runtime, so that you don't have to specify the locale on each `t!` invocation. ```rust rust_i18n::set_locale("zh-CN"); let locale = rust_i18n::locale(); assert_eq!(*locale, "zh-CN"); ``` ### Extend Backend Since v2.0.0 rust-i18n support extend backend for cusomize your translation implementation. For example, you can use HTTP API for load translations from remote server: ```rust,no_run # pub mod reqwest { # pub mod blocking { # pub struct Response; # impl Response { # pub fn text(&self) -> Result> { todo!() } # } # pub fn get(_url: &str) -> Result> { todo!() } # } # } # use std::collections::HashMap; use rust_i18n::Backend; pub struct RemoteI18n { trs: HashMap>, } impl RemoteI18n { fn new() -> Self { // fetch translations from remote URL let response = reqwest::blocking::get("https://your-host.com/assets/locales.yml").unwrap(); let trs = serde_yaml::from_str::>>(&response.text().unwrap()).unwrap(); return Self { trs }; } } impl Backend for RemoteI18n { fn available_locales(&self) -> Vec<&str> { return self.trs.keys().map(|k| k.as_str()).collect(); } fn translate(&self, locale: &str, key: &str) -> Option<&str> { // Write your own lookup logic here. // For example load from database return self.trs.get(locale)?.get(key).map(|k| k.as_str()); } } ``` Now you can init rust_i18n by extend your own backend: ```rust,no_run # struct RemoteI18n; # impl RemoteI18n { # fn new() -> Self { todo!() } # } # impl rust_i18n::Backend for RemoteI18n { # fn available_locales(&self) -> Vec<&str> { todo!() } # fn translate(&self, locale: &str, key: &str) -> Option<&str> { todo!() } # } rust_i18n::i18n!("locales", backend = RemoteI18n::new()); ``` This also will load local translates from ./locales path, but your own `RemoteI18n` will priority than it. Now you call `t!` will lookup translates from your own backend first, if not found, will lookup from local files. ## Example A minimal example of using rust-i18n can be found [here](https://github.com/longbridgeapp/rust-i18n/tree/main/examples). ## I18n Ally I18n Ally is a VS Code extension for helping you translate your Rust project. You can add [i18n-ally-custom-framework.yml](https://github.com/longbridgeapp/rust-i18n/blob/main/.vscode/i18n-ally-custom-framework.yml) to your project `.vscode` directory, and then use I18n Ally can parse `t!` marco to show translate text in VS Code editor. ## Extractor > **Experimental** We provided a `cargo i18n` command line tool for help you extract the untranslated texts from the source code and then write into YAML file. > In current only output YAML, and use `_version: 2` format. You can install it via `cargo install rust-i18n-cli`, then you get `cargo i18n` command. ```bash $ cargo install rust-i18n-cli ``` ### Extractor Config πŸ’‘ NOTE: `package.metadata.i18n` config section in Cargo.toml is just work for `cargo i18n` command, if you don't use that, you don't need this config. ```toml [package.metadata.i18n] # The available locales for your application, default: ["en"]. # available-locales = ["en", "zh-CN"] # The default locale, default: "en". # default-locale = "en" # Path for your translations YAML file, default: "locales". # This config for let `cargo i18n` command line tool know where to find your translations. # You must keep this path same as the one you pass to method `rust_i18n::i18n!`. # load-path = "locales" ``` Rust I18n providered a `i18n` bin for help you extract the untranslated texts from the source code and then write into YAML file. ```bash $ cargo install rust-i18n-cli # Now you have `cargo i18n` command ``` After that the untranslated texts will be extracted and saved into `locales/TODO.en.yml` file. You also can special the locale by use `--locale` option: ```bash $ cd your_project_root_directory $ cargo i18n Checking [en] and generating untranslated texts... Found 1 new texts need to translate. ---------------------------------------- Writing to TODO.en.yml Checking [fr] and generating untranslated texts... Found 11 new texts need to translate. ---------------------------------------- Writing to TODO.fr.yml Checking [zh-CN] and generating untranslated texts... All thing done. Checking [zh-HK] and generating untranslated texts... Found 11 new texts need to translate. ---------------------------------------- Writing to TODO.zh-HK.yml ``` Run `cargo i18n -h` to see details. ```bash $ cargo i18n -h cargo-i18n 0.5.0 --------------------------------------- Rust I18n command for help you simply to extract all untranslated texts from soruce code. It will iter all Rust files in and extract all untranslated texts that used `t!` macro. And then generate a YAML file and merge for existing texts. https://github.com/longbridgeapp/rust-i18n USAGE: cargo i18n [OPTIONS] [--] [source] FLAGS: -h, --help Prints help information -V, --version Prints version information ARGS: Path of your Rust crate root [default: ./] ``` ## Debugging the Codegen Process The `RUST_I18N_DEBUG` environment variable can be used to print out some debugging infos when code is being generated at compile time. ```bash $ RUST_I18N_DEBUG=1 cargo build ``` ## Benchmark Benchmark `t!` method, result on Apple M1: ```bash t time: [58.274 ns 60.222 ns 62.390 ns] t_with_locale time: [55.395 ns 57.106 ns 59.081 ns] t_with_args time: [167.46 ns 170.94 ns 175.64 ns] t_with_args (str) time: [164.85 ns 165.91 ns 167.41 ns] t_with_args (many) time: [444.04 ns 452.17 ns 463.44 ns] t_with_threads time: [414.26 ns 422.97 ns 433.53 ns] ``` The result `101 ns (0.0001 ms)` means if there have 10K translate texts, it will cost 1ms. ## License MIT