rust-i18n-support-4.1.0/.cargo_vcs_info.json 0000644 00000000154 10461020230 0014344 0 ustar {
"git": {
"sha1": "97cf091c24e4bc09a0acb397a8d9d7da8b6abc56"
},
"path_in_vcs": "crates/support"
} rust-i18n-support-4.1.0/Cargo.lock 0000644 00000031003 10461020230 0012314 0 ustar # 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 = "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-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.1.0"
dependencies = [
"arc-swap",
"base62",
"globwalk",
"itertools",
"normpath",
"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.1.0/Cargo.toml 0000644 00000003125 10461020230 0012343 0 ustar # 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.1.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"
[features]
codegen = [
"dep:serde",
"dep:serde_json",
"dep:serde_yaml",
"dep:toml",
"dep:globwalk",
"dep:normpath",
"dep:itertools",
]
[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"
optional = true
[dependencies.itertools]
version = "0.11.0"
optional = true
[dependencies.normpath]
version = "1.1.1"
optional = true
[dependencies.serde]
version = "1"
features = ["derive"]
optional = true
[dependencies.serde_json]
version = "1"
optional = true
[dependencies.serde_yaml]
version = "0.9.33"
optional = true
[dependencies.siphasher]
version = "1.0"
[dependencies.toml]
version = "0.8.8"
optional = true
[dependencies.triomphe]
version = "0.1.11"
features = ["arc-swap"]
rust-i18n-support-4.1.0/Cargo.toml.orig 0000644 0000000 0000000 00000001534 10461020230 0016004 0 ustar 0000000 0000000 [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.1.0"
[features]
codegen = [
"dep:serde",
"dep:serde_json",
"dep:serde_yaml",
"dep:toml",
"dep:globwalk",
"dep:normpath",
"dep:itertools",
]
[dependencies]
arc-swap.workspace = true
base62.workspace = true
siphasher.workspace = true
triomphe.workspace = true
# codegen-only deps
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
serde_yaml = { workspace = true, optional = true }
toml = { workspace = true, optional = true }
globwalk = { workspace = true, optional = true }
normpath = { workspace = true, optional = true }
itertools = { workspace = true, optional = true }
rust-i18n-support-4.1.0/README.md 0000644 0000000 0000000 00000035703 10461020230 0014401 0 ustar 0000000 0000000 # Rust I18n
[](https://github.com/longbridge/rust-i18n/actions/workflows/ci.yml) [](https://docs.rs/rust-i18n/) [](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)
- `load-path` feature for runtime locale file loading via `try_load_locales`. By default, YAML/TOML parsing deps are compile-time only and not included in the binary.
## 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
- [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev)
- [tracker](https://github.com/ShenMian/tracker)
## License
MIT
rust-i18n-support-4.1.0/src/atomic_str.rs 0000644 0000000 0000000 00000002616 10461020230 0016420 0 ustar 0000000 0000000 use 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.1.0/src/backend.rs 0000644 0000000 0000000 00000014732 10461020230 0015645 0 ustar 0000000 0000000 use 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.1.0/src/config.rs 0000644 0000000 0000000 00000014436 10461020230 0015524 0 ustar 0000000 0000000 //! 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.1.0/src/cow_str.rs 0000644 0000000 0000000 00000004571 10461020230 0015736 0 ustar 0000000 0000000 use 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.1.0/src/lib.rs 0000644 0000000 0000000 00000030417 10461020230 0015022 0 ustar 0000000 0000000 mod atomic_str;
mod backend;
mod cow_str;
mod minify_key;
pub use atomic_str::AtomicStr;
pub use backend::{Backend, BackendExt, CombinedBackend, SimpleBackend};
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,
};
#[cfg(feature = "codegen")]
mod config;
#[cfg(feature = "codegen")]
pub use config::I18nConfig;
pub fn is_debug() -> bool {
std::env::var("RUST_I18N_DEBUG").unwrap_or_else(|_| "0".to_string()) == "1"
}
#[cfg(feature = "codegen")]
use normpath::PathExt;
#[cfg(feature = "codegen")]
use std::fs::File;
#[cfg(feature = "codegen")]
use std::io::prelude::*;
#[cfg(feature = "codegen")]
use std::{collections::BTreeMap, path::Path};
#[cfg(feature = "codegen")]
type Locale = String;
#[cfg(feature = "codegen")]
type Value = serde_json::Value;
#[cfg(feature = "codegen")]
type Translations = BTreeMap;
#[cfg(feature = "codegen")]
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();
}
}
}
#[cfg(feature = "codegen")]
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),
}
}
#[cfg(feature = "codegen")]
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)
}
#[cfg(feature = "codegen")]
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),
}
}
#[cfg(feature = "codegen")]
fn parse_file_v1(locale: &str, data: &serde_json::Value) -> Translations {
Translations::from([(locale.to_string(), data.clone())])
}
#[cfg(feature = "codegen")]
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 {
for (locale, text) in sub_messages {
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() {
let key = format_keys(&[key_prefix, key]);
if let Some(sub_trs) = parse_file_v2(&key, value) {
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
}
#[cfg(feature = "codegen")]
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
}
#[cfg(feature = "codegen")]
fn format_keys(keys: &[&str]) -> String {
keys.iter()
.filter(|k| !k.is_empty())
.map(|k| k.to_string())
.collect::>()
.join(".")
}
#[cfg(feature = "codegen")]
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(all(test, feature = "codegen"))]
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.1.0/src/minify_key.rs 0000644 0000000 0000000 00000011626 10461020230 0016420 0 ustar 0000000 0000000 use 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");
}
}