rust-i18n-support-4.0.0/.cargo_vcs_info.json0000644000000001541046102023000143430ustar { "git": { "sha1": "a931914a1c1f9853f8d0251b49b4accbc4cbde55" }, "path_in_vcs": "crates/support" }rust-i18n-support-4.0.0/Cargo.lock0000644000000320011046102023000123120ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "base62" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" dependencies = [ "rustversion", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "serde", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "globset" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata", "regex-syntax", ] [[package]] name = "globwalk" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" dependencies = [ "bitflags", "ignore", "walkdir", ] [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "ignore" version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util", ] [[package]] name = "indexmap" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "normpath" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" dependencies = [ "windows-sys", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rust-i18n-support" version = "4.0.0" dependencies = [ "arc-swap", "base62", "globwalk", "itertools", "lazy_static", "normpath", "proc-macro2", "regex", "serde", "serde_json", "serde_yaml", "siphasher", "toml", "triomphe", ] [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "syn" version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "toml_write", "winnow", ] [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "triomphe" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" dependencies = [ "arc-swap", "serde", "stable_deref_trait", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] rust-i18n-support-4.0.0/Cargo.toml0000644000000027271046102023000123510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "rust-i18n-support" version = "4.0.0" build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Support for rust-i18n crate." readme = "README.md" license = "MIT" repository = "https://github.com/longbridge/rust-i18n" [lib] name = "rust_i18n_support" path = "src/lib.rs" [dependencies.arc-swap] version = "1.6.0" [dependencies.base62] version = "2.0.2" [dependencies.globwalk] version = "0.8.1" [dependencies.itertools] version = "0.11.0" [dependencies.lazy_static] version = "1" [dependencies.normpath] version = "1.1.1" [dependencies.proc-macro2] version = "1" features = ["span-locations"] [dependencies.regex] version = "1" [dependencies.serde] version = "1" features = ["derive"] [dependencies.serde_json] version = "1" [dependencies.serde_yaml] version = "0.9.33" [dependencies.siphasher] version = "1.0" [dependencies.toml] version = "0.8.8" [dependencies.triomphe] version = "0.1.11" features = ["arc-swap"] rust-i18n-support-4.0.0/Cargo.toml.orig000064400000000000000000000011231046102023000157750ustar 00000000000000[package] description = "Support for rust-i18n crate." edition = "2021" license = "MIT" name = "rust-i18n-support" readme = "../../README.md" repository = "https://github.com/longbridge/rust-i18n" version = "4.0.0" [dependencies] arc-swap.workspace = true base62.workspace = true globwalk.workspace = true itertools.workspace = true proc-macro2.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true siphasher.workspace = true toml.workspace = true normpath.workspace = true lazy_static.workspace = true regex.workspace = true triomphe.workspace = true rust-i18n-support-4.0.0/README.md000064400000000000000000000352471046102023000144030ustar 00000000000000# Rust I18n [![CI](https://github.com/longbridge/rust-i18n/actions/workflows/ci.yml/badge.svg)](https://github.com/longbridge/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. - Supports specifying a chain of fallback locales for missing translations. - Supports automatic lookup of language territory for fallback locale. For instance, if `zh-CN` is not available, it will fallback to `zh`. (Since v2.4.0) - Support short hashed keys for optimize memory usage and lookup speed. (Since v3.1.0) - Support format variables in [`t!`], and support format variables with [`std::fmt`](https://doc.rust-lang.org/std/fmt/) syntax. (Since v3.1.0) - Support for log missing translations at the warning level with `log-miss-tr` feature, the feature requires the `log` crate. (Since v3.1.0) ## 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. // This will load Configuration using the `[package.metadata.i18n]` section in `Cargo.toml` if exists. // Or you can pass arguments by `i18n!` to override it. i18n!("locales"); // 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"]); // Use a short hashed key as an identifier for long string literals // to optimize memory usage and lookup speed. // The key generation algorithm is `${Prefix}${Base62(SipHash13("msg"))}`. i18n!("locales", minify_key = true); // // Alternatively, you can customize the key length, prefix, // and threshold for the short hashed key. i18n!("locales", minify_key = true, minify_key_len = 12, minify_key_prefix = "t_", minify_key_thresh = 64 ); // Now, if the message length exceeds 64, the `t!` macro will automatically generate // a 12-byte short hashed key with a "t_" prefix for it, if not, it will use the original. // If no any argument, use config from Cargo.toml or default. i18n!(); ``` 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() { // Find the translation for the string literal `Hello` using the manually provided key `hello`. 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 (This version is the locale file version, not the rust-i18n version) of the locale file, and the default value is `1`. rust-i18n supports two style of config file, and those versions will always be keeping. - `_version: 1` - Split each locale into difference files, it is useful when your project wants to split to translate work. - `_version: 2` - Put all localized text into same file, it is easy to translate quickly by AI (e.g.: GitHub Copilot). When you write original text, just press Enter key, then AI will suggest you the translation text for other languages. You can choose as you like. ### Split Localized Texts into Difference Files > \_version: 1 You can also split the each language into difference files, and you can choose (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}" t_4Cct6Q289b12SkvF47dXIx: "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}", "t_4Cct6Q289b12SkvF47dXIx": "Hello, %{name}" } ``` ```toml hello = "Hello world" t_4Cct6Q289b12SkvF47dXIx = "Hello, %{name}" [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} # Generate short hashed keys using `minify_key=true, minify_key_thresh=10` t_4Cct6Q289b12SkvF47dXIx: 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 # macro_rules! t { # ($($all_tokens:tt)*) => {} # } # 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)" t!("Hello, %{name}, you serial number is: %{sn}", name = "Jason", sn = 123 : {:08}); // => "Hello, Jason, you serial number is: 000000123" # } ``` ### 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 std::borrow::Cow; 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> { return self.trs.keys().map(|k| Cow::from(k.as_str())).collect(); } fn translate(&self, locale: &str, key: &str) -> Option> { // Write your own lookup logic here. // For example load from database return self.trs.get(locale)?.get(key).map(|k| Cow::from(k.as_str())); } fn messages_for_locale(&self, locale: &str) -> Option, Cow<'_, str>)>> { None } } ``` Now you can init rust_i18n by extend your own backend: ```rust,no_run # use std::borrow::Cow; # struct RemoteI18n; # impl RemoteI18n { # fn new() -> Self { todo!() } # } # impl rust_i18n::Backend for RemoteI18n { # fn available_locales(&self) -> Vec> { todo!() } # fn translate(&self, locale: &str, key: &str) -> Option> { todo!() } fn messages_for_locale(&self, locale: &str) -> Option, Cow<'_, 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/longbridge/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/longbridge/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 3.1.0 --------------------------------------- Rust I18n command to help you extract all untranslated texts from source code. It will iterate all Rust files in the source directory and extract all untranslated texts that used `t!` macro. Then it will generate a YAML file and merge with the existing translations. https://github.com/longbridge/rust-i18n Usage: cargo i18n [OPTIONS] [-- ] Arguments: [SOURCE] Extract all untranslated I18n texts from source code [default: ./] Options: -t, --translate ... Manually add a translation to the localization file. This is useful for non-literal values in the `t!` macro. For example, if you have `t!(format!("Hello, {}!", "world"))` in your code, you can add a translation for it using `-t "Hello, world!"`, or provide a translated message using `-t "Hello, world! => Hola, world!"`. NOTE: The whitespace before and after the key and value will be trimmed. -h, --help Print help (see a summary with '-h') -V, --version Print version ``` ## 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 MacBook Pro (2023, Apple M3): ```bash t time: [32.637 ns 33.139 ns 33.613 ns] t_with_locale time: [24.616 ns 24.812 ns 25.071 ns] t_with_args time: [128.70 ns 128.97 ns 129.24 ns] t_with_args (str) time: [129.48 ns 130.08 ns 130.76 ns] t_with_args (many) time: [370.28 ns 374.46 ns 380.56 ns] t_with_threads time: [38.619 ns 39.506 ns 40.419 ns] t_lorem_ipsum time: [33.867 ns 34.286 ns 34.751 ns] ``` The result `101 ns (0.0001 ms)` means if there have **10K** translate texts, it will cost `1ms`. ## Use Cases Here are some most popular projects using `rust-i18n`: - [gpui-component](https://github.com/longbridge/gpui-component) - ~10K stars - [hyperswitch](https://github.com/juspay/hyperswitch) - 40K stars - [Seelen-UI](https://github.com/eythaann/Seelen-UI) - ~16K stars - [EasyTier](https://github.com/EasyTier/EasyTier) - ~10K stars - [trippy](https://github.com/fujiapple852/trippy) - 6.5K stars - [fresh](https://github.com/sinelaw/fresh) - 5.3K stars ## License MIT rust-i18n-support-4.0.0/src/atomic_str.rs000064400000000000000000000026161046102023000164170ustar 00000000000000use std::fmt; use std::ops::Deref; use arc_swap::{ArcSwapAny, Guard}; use triomphe::Arc; /// A thread-safe atomically reference-counting string. pub struct AtomicStr(ArcSwapAny>); /// A thread-safe view the string that was stored when `AtomicStr::as_str()` was called. struct GuardedStr(Guard>); impl Deref for GuardedStr { type Target = str; fn deref(&self) -> &Self::Target { self.0.as_str() } } impl AtomicStr { /// Create a new `AtomicStr` with the given value. pub fn new(value: &str) -> Self { let arced = Arc::new(value.into()); Self(ArcSwapAny::new(arced)) } /// Get the string slice. pub fn as_str(&self) -> impl Deref { GuardedStr(self.0.load()) } /// Replaces the value at self with src. pub fn replace(&self, src: impl Into) { let arced = Arc::new(src.into()); self.0.store(arced); } } impl From<&str> for AtomicStr { fn from(value: &str) -> Self { Self::new(value) } } impl fmt::Display for AtomicStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.as_str()) } } #[cfg(test)] mod tests { use super::*; fn test_str(s: &str) { assert_eq!(s, "hello"); } #[test] fn test_atomic_str() { let s = AtomicStr::from("hello"); test_str(&s.as_str()); } } rust-i18n-support-4.0.0/src/backend.rs000064400000000000000000000147321046102023000156440ustar 00000000000000use std::borrow::Cow; use std::collections::HashMap; /// I18n backend trait pub trait Backend: Send + Sync + 'static { /// Return the available locales fn available_locales(&self) -> Vec>; /// Get the translation for the given locale and key fn translate(&self, locale: &str, key: &str) -> Option>; /// Get all translations for the given locale fn messages_for_locale(&self, locale: &str) -> Option, Cow<'_, str>)>>; } pub trait BackendExt: Backend { /// Extend backend to add more translations fn extend(self, other: T) -> CombinedBackend where Self: Sized, { CombinedBackend(self, other) } } pub struct CombinedBackend(A, B); impl Backend for CombinedBackend where A: Backend, B: Backend, { fn available_locales(&self) -> Vec> { let mut available_locales = self.0.available_locales(); for locale in self.1.available_locales() { if !available_locales.contains(&locale) { available_locales.push(locale); } } available_locales } #[inline] fn translate(&self, locale: &str, key: &str) -> Option> { self.1 .translate(locale, key) .or_else(|| self.0.translate(locale, key)) } fn messages_for_locale(&self, locale: &str) -> Option, Cow<'_, str>)>> { match ( self.1.messages_for_locale(locale), self.0.messages_for_locale(locale), ) { (None, None) => None, (None, a) => a, (b, None) => b, (Some(b), Some(a)) => Some( b.into_iter() .chain( a.into_iter() .filter(|(k, _)| self.1.translate(locale, k).is_none()), ) .collect(), ), } } } /// Simple KeyValue storage backend pub struct SimpleBackend { /// All translations key is flatten key, like `en.hello.world` translations: HashMap, HashMap, Cow<'static, str>>>, } impl FromIterator<( Cow<'static, str>, HashMap, Cow<'static, str>>, )> for SimpleBackend { fn from_iter< I: IntoIterator< Item = ( Cow<'static, str>, HashMap, Cow<'static, str>>, ), >, >( iter: I, ) -> Self { Self { translations: iter.into_iter().collect(), } } } impl SimpleBackend { /// Create a new SimpleBackend. pub fn new() -> Self { SimpleBackend { translations: HashMap::new(), } } /// Add more translations for the given locale. /// /// ```no_run /// # use std::collections::HashMap; /// # use rust_i18n_support::SimpleBackend; /// # let mut backend = SimpleBackend::new(); /// let mut trs = HashMap::new(); /// trs.insert("hello".into(), "Hello".into()); /// trs.insert("foo".into(), "Foo bar".into()); /// backend.add_translations("en".into(), trs); /// ``` pub fn add_translations( &mut self, locale: Cow<'static, str>, data: HashMap, Cow<'static, str>>, ) { let trs = self.translations.entry(locale.into()).or_default(); trs.extend(data); } } impl Backend for SimpleBackend { fn available_locales(&self) -> Vec> { let mut locales = self.translations.keys().cloned().collect::>(); locales.sort(); locales } fn translate(&self, locale: &str, key: &str) -> Option> { if let Some(trs) = self.translations.get(locale) { return trs.get(key).cloned(); } None } fn messages_for_locale(&self, locale: &str) -> Option, Cow<'_, str>)>> { self.translations .get(locale) .map(|trs| trs.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) } } impl BackendExt for SimpleBackend {} impl Default for SimpleBackend { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use std::borrow::Cow; use std::collections::HashMap; use super::SimpleBackend; use super::{Backend, BackendExt}; #[test] fn test_simple_backend() { let mut backend = SimpleBackend::new(); let mut data = HashMap::new(); data.insert("hello".into(), "Hello".into()); data.insert("foo".into(), "Foo bar".into()); backend.add_translations("en".into(), data); let mut data_cn = HashMap::new(); data_cn.insert("hello".into(), "δ½ ε₯½".into()); data_cn.insert("foo".into(), "Foo ζ΅‹θ―•".into()); backend.add_translations("zh-CN".into(), data_cn); assert_eq!(backend.translate("en", "hello"), Some(Cow::from("Hello"))); assert_eq!(backend.translate("en", "foo"), Some(Cow::from("Foo bar"))); assert_eq!(backend.translate("zh-CN", "hello"), Some(Cow::from("δ½ ε₯½"))); assert_eq!( backend.translate("zh-CN", "foo"), Some(Cow::from("Foo ζ΅‹θ―•")) ); assert_eq!(backend.available_locales(), vec!["en", "zh-CN"]); } #[test] fn test_combined_backend() { let mut backend = SimpleBackend::new(); let mut data = HashMap::new(); data.insert("hello".into(), "Hello".into()); data.insert("foo".into(), "Foo bar".into()); backend.add_translations("en".into(), data); let mut data_cn = HashMap::new(); data_cn.insert("hello".into(), "δ½ ε₯½".into()); data_cn.insert("foo".into(), "Foo ζ΅‹θ―•".into()); backend.add_translations("zh-CN".into(), data_cn); let mut backend2 = SimpleBackend::new(); let mut data2 = HashMap::new(); data2.insert("hello".into(), "Hello2".into()); backend2.add_translations("en".into(), data2); let mut data_cn2 = HashMap::new(); data_cn2.insert("hello".into(), "δ½ ε₯½2".into()); backend2.add_translations("zh-CN".into(), data_cn2); let combined = backend.extend(backend2); assert_eq!(combined.translate("en", "hello"), Some(Cow::from("Hello2"))); assert_eq!( combined.translate("zh-CN", "hello"), Some(Cow::from("δ½ ε₯½2")) ); assert_eq!(combined.available_locales(), vec!["en", "zh-CN"]); } } rust-i18n-support-4.0.0/src/config.rs000064400000000000000000000144361046102023000155230ustar 00000000000000//! This crate defines `struct`s that can be deserialized with Serde //! to load and inspect `Cargo.toml` metadata. //! //! See `Manifest::from_slice`. use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::fs; use std::io; use std::io::Read; use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct I18nConfig { #[serde(default = "default_locale")] pub default_locale: String, #[serde(default = "available_locales")] pub available_locales: Vec, #[serde(default = "load_path")] pub load_path: String, #[serde(default = "fallback")] pub fallback: Vec, #[serde(default = "minify_key")] pub minify_key: bool, #[serde(default = "minify_key_len")] pub minify_key_len: usize, #[serde(default = "minify_key_prefix")] pub minify_key_prefix: String, #[serde(default = "minify_key_thresh")] pub minify_key_thresh: usize, } impl Default for I18nConfig { fn default() -> Self { Self { default_locale: "en".to_string(), available_locales: vec!["en".to_string()], load_path: "./locales".to_string(), fallback: vec![], minify_key: crate::DEFAULT_MINIFY_KEY, minify_key_len: crate::DEFAULT_MINIFY_KEY_LEN, minify_key_prefix: crate::DEFAULT_MINIFY_KEY_PREFIX.to_string(), minify_key_thresh: crate::DEFAULT_MINIFY_KEY_THRESH, } } } impl I18nConfig { pub fn new() -> Self { Self::default() } pub fn load(cargo_root: &Path) -> io::Result { let cargo_file = cargo_root.join("Cargo.toml"); let mut file = fs::File::open(&cargo_file) .unwrap_or_else(|e| panic!("Fail to open {}, {}", cargo_file.display(), e)); let mut contents = String::new(); file.read_to_string(&mut contents)?; Self::parse(&contents) } pub fn parse(contents: &str) -> io::Result { let package_metadata = contents.contains("[package.metadata.i18n]"); let workspace_metadata = contents.contains("[workspace.metadata.i18n]"); if !contents.contains("[i18n]") && !package_metadata && !workspace_metadata { return Ok(I18nConfig::default()); } let contents = if package_metadata { contents.replace("[package.metadata.i18n]", "[i18n]") } else if workspace_metadata { contents.replace("[workspace.metadata.i18n]", "[i18n]") } else { contents.to_string() }; let mut config: MainConfig = toml::from_str(&contents) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; // Push default_locale config .i18n .available_locales .insert(0, config.i18n.default_locale.clone()); // unqiue config.i18n.available_locales = config.i18n.available_locales.into_iter().unique().collect(); Ok(config.i18n) } } fn default_locale() -> String { I18nConfig::default().default_locale } fn available_locales() -> Vec { I18nConfig::default().available_locales } fn load_path() -> String { I18nConfig::default().load_path } fn fallback() -> Vec { I18nConfig::default().fallback } fn minify_key() -> bool { I18nConfig::default().minify_key } fn minify_key_len() -> usize { I18nConfig::default().minify_key_len } fn minify_key_prefix() -> String { I18nConfig::default().minify_key_prefix } fn minify_key_thresh() -> usize { I18nConfig::default().minify_key_thresh } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub struct MainConfig { pub i18n: I18nConfig, } #[test] fn test_parse() { let contents = r#" [i18n] default-locale = "en" available-locales = ["zh-CN"] load-path = "./my-locales" fallback = ["zh"] minify-key = true minify-key-len = 12 minify-key-prefix = "T_" minify-key-thresh = 16 "#; let cfg = I18nConfig::parse(contents).unwrap(); assert_eq!(cfg.default_locale, "en"); assert_eq!(cfg.available_locales, vec!["en", "zh-CN"]); assert_eq!(cfg.load_path, "./my-locales"); assert_eq!(cfg.fallback, vec!["zh"]); assert!(cfg.minify_key); assert_eq!(cfg.minify_key_len, 12); assert_eq!(cfg.minify_key_prefix, "T_"); assert_eq!(cfg.minify_key_thresh, 16); let contents = r#" [i18n] available-locales = ["zh-CN", "de", "de"] load-path = "./my-locales" "#; let cfg = I18nConfig::parse(contents).unwrap(); assert_eq!(cfg.default_locale, "en"); assert_eq!(cfg.available_locales, vec!["en", "zh-CN", "de"]); assert_eq!(cfg.load_path, "./my-locales"); let contents = ""; let cfg = I18nConfig::parse(contents).unwrap(); assert_eq!(cfg.default_locale, "en"); assert_eq!(cfg.available_locales, vec!["en"]); assert_eq!(cfg.load_path, "./locales"); } #[test] fn test_parse_with_metadata() { let contents = r#" [package.metadata.i18n] default-locale = "en" available-locales = ["zh-CN"] load-path = "./my-locales" fallback = ["zh"] minify-key = true minify-key-len = 12 minify-key-prefix = "T_" minify-key-thresh = 16 "#; let cfg = I18nConfig::parse(contents).unwrap(); assert_eq!(cfg.default_locale, "en"); assert_eq!(cfg.available_locales, vec!["en", "zh-CN"]); assert_eq!(cfg.load_path, "./my-locales"); assert_eq!(cfg.fallback, vec!["zh"]); assert!(cfg.minify_key); assert_eq!(cfg.minify_key_len, 12); assert_eq!(cfg.minify_key_prefix, "T_"); assert_eq!(cfg.minify_key_thresh, 16); } #[test] fn test_load_default() { let workdir = Path::new(env!["CARGO_MANIFEST_DIR"]); let cfg = I18nConfig::load(workdir).unwrap(); assert_eq!(cfg.default_locale, "en"); assert_eq!(cfg.available_locales, vec!["en"]); assert_eq!(cfg.load_path, "./locales"); } #[test] fn test_load() { let workdir = Path::new(env!["CARGO_MANIFEST_DIR"]); let cargo_root = workdir.join("../../examples/foo"); let cfg = I18nConfig::load(&cargo_root).unwrap(); assert_eq!(cfg.default_locale, "en"); assert_eq!(cfg.available_locales, vec!["en", "zh-CN"]); } rust-i18n-support-4.0.0/src/cow_str.rs000064400000000000000000000045711046102023000157350ustar 00000000000000use std::borrow::Cow; use std::sync::Arc; /// A wrapper for `Cow<'a, str>` that is specifically designed for use with the `t!` macro. /// /// This wrapper provides additional functionality or optimizations when handling strings in the `t!` macro. pub struct CowStr<'a>(Cow<'a, str>); impl<'a> CowStr<'a> { pub fn as_str(&self) -> &str { self.0.as_ref() } pub fn into_inner(self) -> Cow<'a, str> { self.0 } } macro_rules! impl_convert_from_numeric { ($typ:ty) => { impl<'a> From<$typ> for CowStr<'a> { fn from(val: $typ) -> Self { Self(Cow::from(format!("{}", val))) } } }; } impl_convert_from_numeric!(i8); impl_convert_from_numeric!(i16); impl_convert_from_numeric!(i32); impl_convert_from_numeric!(i64); impl_convert_from_numeric!(i128); impl_convert_from_numeric!(isize); impl_convert_from_numeric!(u8); impl_convert_from_numeric!(u16); impl_convert_from_numeric!(u32); impl_convert_from_numeric!(u64); impl_convert_from_numeric!(u128); impl_convert_from_numeric!(usize); impl<'a> From> for CowStr<'a> { #[inline] fn from(s: Arc) -> Self { Self(Cow::Owned(s.to_string())) } } impl<'a> From> for CowStr<'a> { #[inline] fn from(s: Box) -> Self { Self(Cow::Owned(s.to_string())) } } impl<'a> From<&'a str> for CowStr<'a> { #[inline] fn from(s: &'a str) -> Self { Self(Cow::Borrowed(s)) } } impl<'a> From<&&'a str> for CowStr<'a> { #[inline] fn from(s: &&'a str) -> Self { Self(Cow::Borrowed(s)) } } impl<'a> From> for CowStr<'a> { #[inline] fn from(s: Arc<&'a str>) -> Self { Self(Cow::Borrowed(*s)) } } impl<'a> From> for CowStr<'a> { #[inline] fn from(s: Box<&'a str>) -> Self { Self(Cow::Borrowed(*s)) } } impl<'a> From for CowStr<'a> { #[inline] fn from(s: String) -> Self { Self(Cow::from(s)) } } impl<'a> From<&'a String> for CowStr<'a> { #[inline] fn from(s: &'a String) -> Self { Self(Cow::Borrowed(s)) } } impl<'a> From> for CowStr<'a> { #[inline] fn from(s: Arc) -> Self { Self(Cow::Owned(s.to_string())) } } impl<'a> From> for CowStr<'a> { #[inline] fn from(s: Box) -> Self { Self(Cow::from(*s)) } } rust-i18n-support-4.0.0/src/lib.rs000064400000000000000000000322571046102023000150250ustar 00000000000000use normpath::PathExt; use std::fs::File; use std::io::prelude::*; use std::{collections::BTreeMap, path::Path}; mod atomic_str; mod backend; mod config; mod cow_str; mod minify_key; pub use atomic_str::AtomicStr; pub use backend::{Backend, BackendExt, SimpleBackend}; pub use config::I18nConfig; pub use cow_str::CowStr; pub use minify_key::{ minify_key, MinifyKey, DEFAULT_MINIFY_KEY, DEFAULT_MINIFY_KEY_LEN, DEFAULT_MINIFY_KEY_PREFIX, DEFAULT_MINIFY_KEY_THRESH, }; type Locale = String; type Value = serde_json::Value; type Translations = BTreeMap; pub fn is_debug() -> bool { std::env::var("RUST_I18N_DEBUG").unwrap_or_else(|_| "0".to_string()) == "1" } /// Merge JSON Values, merge b into a fn merge_value(a: &mut Value, b: &Value) { match (a, b) { (Value::Object(a), Value::Object(b)) => { for (k, v) in b { merge_value(a.entry(k.clone()).or_insert(Value::Null), v); } } (a, b) => { *a = b.clone(); } } } // Load locales into flatten key, value HashMap pub fn load_locales bool>( locales_path: &str, ignore_if: F, ) -> BTreeMap> { match try_load_locales(locales_path, ignore_if, false) { Ok(locales) => locales, Err(error) => panic!("{}", error), } } pub fn try_load_locales bool>( locales_path: &str, ignore_if: F, report_file_lookup_errors: bool, ) -> Result>, String> { let mut result: BTreeMap> = BTreeMap::new(); let mut translations = BTreeMap::new(); let locales_path = match Path::new(locales_path).normalize() { Ok(p) => p, Err(e) => { if is_debug() { println!("cargo:i18n-error={}", e); } return if report_file_lookup_errors { Err(format!("Path '{locales_path}' cannot be normalized: '{e}'")) } else { Ok(result) }; } }; let locales_path = match locales_path.as_path().to_str() { Some(p) => p, None => { if is_debug() { println!("cargo:i18n-error=could not convert path"); } return if report_file_lookup_errors { Err("Could not convert path.".to_string()) } else { Ok(result) }; } }; let path_pattern = format!("{locales_path}/**/*.{{yml,yaml,json,toml}}"); if is_debug() { println!("cargo:i18n-locale={}", &path_pattern); } // check dir exists if !Path::new(locales_path).exists() { if is_debug() { println!("cargo:i18n-error=path not exists: {}", locales_path); } return if report_file_lookup_errors { Err(format!("Path '{locales_path}' not found.")) } else { Ok(result) }; } for entry in globwalk::glob(&path_pattern) .map_err(|error| format!("Failed to read glob pattern: {error}"))? { let entry = entry.unwrap().into_path(); if is_debug() { println!("cargo:i18n-load={}", &entry.display()); } if ignore_if(&entry.display().to_string()) { continue; } let locale = entry .file_stem() .and_then(|s| s.to_str()) .and_then(|s| s.split('.').last()) .unwrap(); let ext = entry.extension().and_then(|s| s.to_str()).unwrap(); let file = File::open(&entry) .map_err(|error| format!("Failed to open file '{entry:?}': {error}"))?; let mut reader = std::io::BufReader::new(file); let mut content = String::new(); reader .read_to_string(&mut content) .map_err(|error| format!("Read file '{entry:?}' failed: {error}."))?; let trs = parse_file(&content, ext, locale).map_err(|error| { format!("Parse file `{}` failed, reason: {}", entry.display(), error) })?; trs.into_iter().for_each(|(k, new_value)| { translations .entry(k) .and_modify(|old_value| merge_value(old_value, &new_value)) .or_insert(new_value); }); } translations.iter().for_each(|(locale, trs)| { result.insert(locale.to_string(), flatten_keys("", trs)); }); Ok(result) } // Parse Translations from file to support multiple formats fn parse_file(content: &str, ext: &str, locale: &str) -> Result { let result = match ext { "yml" | "yaml" => serde_yaml::from_str::(content) .map_err(|err| format!("Invalid YAML format, {}", err)), "json" => serde_json::from_str::(content) .map_err(|err| format!("Invalid JSON format, {}", err)), "toml" => toml::from_str::(content) .map_err(|err| format!("Invalid TOML format, {}", err)), _ => Err("Invalid file extension".into()), }; match result { Ok(v) => match get_version(&v) { 2 => { if let Some(trs) = parse_file_v2("", &v) { return Ok(trs); } Err("Invalid locale file format, please check the version field".into()) } _ => Ok(parse_file_v1(locale, &v)), }, Err(e) => Err(e), } } /// Locale file format v1 /// /// For example: /// ```yml /// welcome: Welcome /// foo: Foo bar /// ``` fn parse_file_v1(locale: &str, data: &serde_json::Value) -> Translations { Translations::from([(locale.to_string(), data.clone())]) } /// Locale file format v2 /// Iter all nested keys, if the value is not a object (Map), then convert into multiple locale translations /// /// If the final value is Map, then convert them and insert into trs /// /// For example (only support 1 level): /// /// ```yml /// _version: 2 /// welcome.first: /// en: Welcome /// zh-CN: 欒迎 /// welcome1: /// en: Welcome 1 /// zh-CN: 欒迎 1 /// ``` /// /// into /// /// ```yml /// en.welcome.first: Welcome /// zh-CN.welcome.first: 欒迎 /// en.welcome1: Welcome 1 /// zh-CN.welcome1: 欒迎 1 /// ``` fn parse_file_v2(key_prefix: &str, data: &serde_json::Value) -> Option { let mut trs = Translations::new(); if let serde_json::Value::Object(messages) = data { for (key, value) in messages { if let serde_json::Value::Object(sub_messages) = value { // If all values are string, then convert them into multiple locale translations for (locale, text) in sub_messages { // Ignore if the locale is not a locale // e.g: // en: Welcome // zh-CN: 欒迎 if text.is_string() { let key = format_keys(&[key_prefix, key]); let sub_trs = BTreeMap::from([(key, text.clone())]); let sub_value = serde_json::to_value(&sub_trs).unwrap(); trs.entry(locale.clone()) .and_modify(|old_value| merge_value(old_value, &sub_value)) .or_insert(sub_value); continue; } if text.is_object() { // Parse the nested keys // If the value is object (Map), iter them and convert them and insert into trs let key = format_keys(&[key_prefix, key]); if let Some(sub_trs) = parse_file_v2(&key, value) { // Merge the sub_trs into trs for (locale, sub_value) in sub_trs { trs.entry(locale) .and_modify(|old_value| merge_value(old_value, &sub_value)) .or_insert(sub_value); } } } } } } } if !trs.is_empty() { return Some(trs); } None } /// Get `_version` from JSON root /// If `_version` is not found, then return 1 as default. fn get_version(data: &serde_json::Value) -> usize { if let Some(version) = data.get("_version") { return version.as_u64().unwrap_or(1) as usize; } 1 } /// Join the keys with dot, if any key is empty, omit it. fn format_keys(keys: &[&str]) -> String { keys.iter() .filter(|k| !k.is_empty()) .map(|k| k.to_string()) .collect::>() .join(".") } fn flatten_keys(prefix: &str, trs: &Value) -> BTreeMap { let mut v = BTreeMap::::new(); let prefix = prefix.to_string(); match &trs { serde_json::Value::String(s) => { v.insert(prefix, s.to_string()); } serde_json::Value::Object(o) => { for (k, vv) in o { let key = if prefix.is_empty() { k.clone() } else { format!("{}.{}", prefix, k) }; v.extend(flatten_keys(key.as_str(), vv)); } } serde_json::Value::Null => { v.insert(prefix, "".into()); } serde_json::Value::Bool(s) => { v.insert(prefix, format!("{}", s)); } serde_json::Value::Number(s) => { v.insert(prefix, format!("{}", s)); } serde_json::Value::Array(_) => { v.insert(prefix, "".into()); } } v } #[cfg(test)] mod tests { use super::{merge_value, parse_file}; #[test] fn test_merge_value() { let a = serde_json::from_str::( r#"{"foo": "Foo", "dar": { "a": "1", "b": "2" }}"#, ) .unwrap(); let b = serde_json::from_str::( r#"{"foo": "Foo1", "bar": "Bar", "dar": { "b": "21" }}"#, ) .unwrap(); let mut c = a; merge_value(&mut c, &b); assert_eq!(c["foo"], "Foo1"); assert_eq!(c["bar"], "Bar"); assert_eq!(c["dar"]["a"], "1"); assert_eq!(c["dar"]["b"], "21"); } #[test] fn test_parse_file_in_yaml() { let content = "foo: Foo\nbar: Bar"; let mut trs = parse_file(content, "yml", "en").expect("Should ok"); assert_eq!(trs["en"]["foo"], "Foo"); assert_eq!(trs["en"]["bar"], "Bar"); trs = parse_file(content, "yaml", "en").expect("Should ok"); assert_eq!(trs["en"]["foo"], "Foo"); trs = parse_file(content, "yml", "zh-CN").expect("Should ok"); assert_eq!(trs["zh-CN"]["foo"], "Foo"); parse_file(content, "foo", "en").expect_err("Should error"); } #[test] fn test_parse_file_in_json() { let content = r#" { "foo": "Foo", "bar": "Bar" } "#; let trs = parse_file(content, "json", "en").expect("Should ok"); assert_eq!(trs["en"]["foo"], "Foo"); assert_eq!(trs["en"]["bar"], "Bar"); } #[test] fn test_parse_file_in_toml() { let content = r#" foo = "Foo" bar = "Bar" "#; let trs = parse_file(content, "toml", "en").expect("Should ok"); assert_eq!(trs["en"]["foo"], "Foo"); assert_eq!(trs["en"]["bar"], "Bar"); } #[test] fn test_get_version() { let json = serde_yaml::from_str::("_version: 2").unwrap(); assert_eq!(super::get_version(&json), 2); let json = serde_yaml::from_str::("_version: 1").unwrap(); assert_eq!(super::get_version(&json), 1); // Default fallback to 1 let json = serde_yaml::from_str::("foo: Foo").unwrap(); assert_eq!(super::get_version(&json), 1); } #[test] fn test_parse_file_in_json_with_nested_locale_texts() { let content = r#"{ "_version": 2, "welcome": { "en": "Welcome", "zh-CN": "欒迎", "zh-HK": "歑迎" } }"#; let trs = parse_file(content, "json", "filename").expect("Should ok"); assert_eq!(trs["en"]["welcome"], "Welcome"); assert_eq!(trs["zh-CN"]["welcome"], "欒迎"); assert_eq!(trs["zh-HK"]["welcome"], "歑迎"); } #[test] fn test_parse_file_in_yaml_with_nested_locale_texts() { let content = r#" _version: 2 welcome: en: Welcome zh-CN: 欒迎 jp: γ‚ˆγ†γ“γ welcome.sub: en: Welcome 1 zh-CN: 欒迎 1 jp: γ‚ˆγ†γ“γ 1 "#; let trs = parse_file(content, "yml", "filename").expect("Should ok"); assert_eq!(trs["en"]["welcome"], "Welcome"); assert_eq!(trs["zh-CN"]["welcome"], "欒迎"); assert_eq!(trs["jp"]["welcome"], "γ‚ˆγ†γ“γ"); assert_eq!(trs["en"]["welcome.sub"], "Welcome 1"); assert_eq!(trs["zh-CN"]["welcome.sub"], "欒迎 1"); assert_eq!(trs["jp"]["welcome.sub"], "γ‚ˆγ†γ“γ 1"); } } rust-i18n-support-4.0.0/src/minify_key.rs000064400000000000000000000116261046102023000164170ustar 00000000000000use siphasher::sip128::SipHasher13; use std::borrow::Cow; use std::sync::LazyLock; /// The default value of `minify_key` feature. pub const DEFAULT_MINIFY_KEY: bool = false; /// The length of auto-generated translation key pub const DEFAULT_MINIFY_KEY_LEN: usize = 24; /// The prefix of auto-generated translation key pub const DEFAULT_MINIFY_KEY_PREFIX: &str = ""; /// The minimum length of the value to be generated the translation key pub const DEFAULT_MINIFY_KEY_THRESH: usize = 127; // The hasher for generate the literal translation key static TR_KEY_HASHER: LazyLock = LazyLock::new(SipHasher13::new); /// Calculate a 128-bit siphash of a value. pub fn hash128 + ?Sized>(value: &T) -> u128 { TR_KEY_HASHER.hash(value.as_ref()).as_u128() } /// Generate a translation key from a value. /// /// # Arguments /// /// * `value` - The value to be generated. /// * `len` - The length of the translation key. /// * `prefix` - The prefix of the translation key. /// * `threshold` - The minimum length of the value to be generated. /// /// # Returns /// /// * If `value.len() <= threshold` then returns the origin value. /// * Otherwise, returns a base62 encoded 128 bits hashed translation key. /// pub fn minify_key<'r>(value: &'r str, len: usize, prefix: &str, threshold: usize) -> Cow<'r, str> { if value.len() <= threshold { return Cow::Borrowed(value); } let encoded = base62::encode(hash128(value)); let len = len.min(encoded.len()); format!("{}{}", prefix, &encoded[..len]).into() } /// A trait for generating translation key from a value. pub trait MinifyKey<'a> { /// Generate translation key from a value. fn minify_key(&'a self, len: usize, prefix: &str, threshold: usize) -> Cow<'a, str>; } impl<'a> MinifyKey<'a> for str { #[inline] fn minify_key(&'a self, len: usize, prefix: &str, threshold: usize) -> Cow<'a, str> { minify_key(self, len, prefix, threshold) } } impl<'a> MinifyKey<'a> for &str { #[inline] fn minify_key(&'a self, len: usize, prefix: &str, threshold: usize) -> Cow<'a, str> { minify_key(self, len, prefix, threshold) } } impl<'a> MinifyKey<'a> for String { #[inline] fn minify_key(&'a self, len: usize, prefix: &str, threshold: usize) -> Cow<'a, str> { if self.len() <= threshold { return Cow::Borrowed(self); } minify_key(self, len, prefix, threshold) } } impl<'a> MinifyKey<'a> for &String { #[inline] fn minify_key(&'a self, len: usize, prefix: &str, threshold: usize) -> Cow<'a, str> { if self.len() <= threshold { return Cow::from(*self); } minify_key(self, len, prefix, threshold) } } impl<'a> MinifyKey<'a> for Cow<'a, str> { #[inline] fn minify_key(&'a self, len: usize, prefix: &str, threshold: usize) -> Cow<'a, str> { if self.len() <= threshold { return Cow::Borrowed(self); } minify_key(self, len, prefix, threshold) } } impl<'a> MinifyKey<'a> for &Cow<'a, str> { #[inline] fn minify_key(&'a self, len: usize, prefix: &str, threshold: usize) -> Cow<'a, str> { if self.len() <= threshold { return Cow::Borrowed(*self); } minify_key(self, len, prefix, threshold) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_minify_key() { let msg = "Hello, world!"; assert_eq!( minify_key(msg, 24, DEFAULT_MINIFY_KEY_PREFIX, 0), "1LokVzuiIrh1xByyZG4wjZ" ); assert_eq!( msg.minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 0), "1LokVzuiIrh1xByyZG4wjZ" ); let msg = "Hello, world!".to_string(); assert_eq!( minify_key(&msg, 24, DEFAULT_MINIFY_KEY_PREFIX, 0), "1LokVzuiIrh1xByyZG4wjZ" ); assert_eq!( msg.minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 0), "1LokVzuiIrh1xByyZG4wjZ" ); assert_eq!( msg.minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 128), "Hello, world!" ); let msg = &msg; assert_eq!( msg.minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 0), "1LokVzuiIrh1xByyZG4wjZ" ); let msg = Cow::Owned("Hello, world!".to_owned()); assert_eq!( minify_key(&msg, 24, DEFAULT_MINIFY_KEY_PREFIX, 0), "1LokVzuiIrh1xByyZG4wjZ" ); assert_eq!( msg.minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 0), "1LokVzuiIrh1xByyZG4wjZ" ); assert_eq!( msg.minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 128), "Hello, world!" ); assert_eq!("".minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 0), ""); assert_eq!( "1".minify_key(24, DEFAULT_MINIFY_KEY_PREFIX, 0), "knx7vOJBRfzgQvNfEkbEi" ); assert_eq!("1".minify_key(24, "t_", 0), "t_knx7vOJBRfzgQvNfEkbEi"); } }