rust-i18n: compile-time translations for Rust with a t! macro
A better and simply I18n crate for Rust.
At a glance
- What is it?
- rust-i18n loads localized text from YAML, JSON or TOML files, generates the mappings at compile time so they end up inside the binary, and exposes a single t! macro for lookups. It supports fallback chains, hashed short keys, format variables and a command line tool for extracting untranslated strings.
- Who is it for?
- rust-i18n fits a Rust service or CLI that needs many locales, wants the translation tables compiled into the binary, and would rather have one macro than a runtime locale object with a per-request API. It does not fit a project that must load or change translations at runtime without recompiling, unless you turn on the load-path feature and accept the parsing dependencies that come with it.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 24 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Translations become binary data through a build step
rust-i18n is a crate for loading localized text from a set of YAML, JSON or TOML mapping files. The key design decision is that the mappings are converted into data readable by Rust programs at compile time, and the localized text is then loaded by calling the t! macro. The consequence is that the translation tables are baked into the binary rather than read from disk at startup, which is why the README notes that by default the YAML and TOML parsing dependencies are compile-time only and are not included in the binary. That is a real advantage for a statically linked service or a CLI that ships as a single file, and it is also a real constraint, because a user who wants to add a language without a rebuild cannot. The API is explicitly modelled on ruby-i18n and Rails I18n, and the project's stated goal is a simple and easy-to-use API rather than a feature catalogue, which is a reasonable position for a crate most teams will configure once and never think about again.
One dependency, one macro, and an i18n! call to initialise
The whole setup is two steps. You add the dependency:
[dependencies]
rust-i18n = "4"Then you initialise the crate, usually in lib.rs or main.rs, and the macro is imported so it is usable everywhere:
#[macro_use]
extern crate rust_i18n;
i18n!("locales");The i18n! call takes the locales directory and, if there is a [package.metadata.i18n] section in Cargo.toml, reads its configuration from there, with arguments passed to i18n! overriding it. An empty i18n!() call uses the config from Cargo.toml or the default. The alternative style is to import the macro per file with use rust_i18n::t, which the README notes you must do in each file where you want to use t!. Once initialised, the lookup is a macro call, either t!("hello") for the current locale, t!("hello", locale = "zh-CN") to force one, or t!("messages.hello", name = "world") to pass format variables, and available_locales!() returns the list of locales that were compiled in.
Fallback chains and automatic territory lookup
Missing translations are the normal case in a multi-locale product, and rust-i18n has two mechanisms for them. The first is an explicit fallback chain passed to the initialiser, either a single locale or a list with priority:
i18n!("locales", fallback = "en");i18n!("locales", fallback = ["en", "es"]);The second is automatic territory lookup, available since version 2.4.0, where a missing zh-CN falls back to zh without you writing the rule yourself. That is the behaviour most products want, and the fact that it arrived in a minor version suggests the API has been kept backward compatible for a long time. A third feature, available since 3.1.0, logs missing translations at warning level through the log-miss-tr feature, which requires the log crate. Together these three give a reasonable answer to the question every localisation library has to answer: what happens when a translator has not finished.
Hashed short keys trade readability for lookup speed
Since version 3.1.0 the crate can replace long message keys with short hashed ones. The key generation algorithm is given in the README as ${Prefix}${Base62(SipHash13("msg"))}, and the behaviour is opt-in through minify_key. The configuration lets you set the key length, the prefix and the threshold at which hashing kicks in:
i18n!("locales",
minify_key = true,
minify_key_len = 12,
minify_key_prefix = "t_",
minify_key_thresh = 64
);So a message longer than 64 characters gets a twelve byte hashed key with a t_ prefix, and shorter ones keep their original key. The trade-off is direct and worth stating plainly. You save memory and gain lookup speed on long keys, and you lose the ability to read the key and infer which string it is, which matters when you are grepping a codebase or debugging a missing translation. The example locale file shows the resulting key shape, t_4Cct6Q289b12SkvF47dXIx, sitting alongside human-readable keys, which suggests the intended pattern is to use readable keys in source and let the generated ones appear only in the locale files.
Two locale file formats, and the project keeps both
Locale files carry a _version key, which is the version of the locale file format rather than the version of the crate, and the default is 1. The README states that both styles are supported and that those versions will always be kept. Version 1 splits each locale into separate files, one per language, which suits a project that wants to hand translation work out per language. Version 2 puts all localized text into one file with the locales nested under each key, which the README says is easy to translate quickly by AI, and it gives the Copilot example explicitly. A version 1 file looks like this:
_version: 1
hello: "Hello world"
messages.hello: "Hello, %{name}"
t_4Cct6Q289b12SkvF47dXIx: "Hello, %{name}"And a version 2 file nests the locales instead:
_version: 2
hello:
en: Hello world
zh-CN: 你好世界
messages.hello:
en: Hello, %{name}
zh-CN: 你好,%{name}Both formats can also be written as JSON or TOML by renaming the file, so the choice is about workflow rather than about the format itself. The version 2 layout is worth a caveat: it is convenient for machine translation, but it puts every locale in one file, which means a translator for one language needs edit access to the file that contains all of them.
cargo i18n for extraction, and the workspace layout of the crate
The crate ships a command line tool, cargo i18n, for checking and extracting untranslated texts into YAML files, and it is the piece that closes the loop between code and translation. The repository is a Cargo workspace with a crates directory holding rust-i18n-extract, rust-i18n-macro, rust-i18n-support and the main rust-i18n package, and the examples directory shows how the pieces are meant to be used, with separate projects for metadata configuration, minified keys, workspaces, sharing locales between workspace members, extending the crate, and a size probe for the binary. The load-path feature, listed alongside log-miss-tr, enables runtime locale file loading through try_load_locales, and that feature is what pulls the parsing code into the binary. The Makefile in the root shows the test matrix, running the main crate test, the workspace test, and then the tests for the app-workspace and share-in-workspace examples, since those exercise the layout rules that the crate depends on.
Editorial conclusion
rust-i18n fits a Rust service or CLI that needs many locales, wants the translation tables compiled into the binary, and would rather have one macro than a runtime locale object with a per-request API. It does not fit a project that must load or change translations at runtime without recompiling, unless you turn on the load-path feature and accept the parsing dependencies that come with it. Before adopting it, decide which locale file version to standardise on, since the split-by-locale and all-in-one-file formats are both kept, and check that the compile-time extraction step fits your translation workflow, because a string that never appears in the locale files cannot be translated.
Frequently asked questions
How do I set up rust-i18n in a Rust project?
Add rust-i18n = "4" to Cargo.toml, then call i18n!("locales") in lib.rs or main.rs to load the translations, importing the macro with #[macro_use] extern crate rust_i18n or use rust_i18n::t in each file. After that, t!("hello") looks up a string in the current locale.
Does rust-i18n support fallback locales?
Yes. You can pass a single fallback locale or a list with priority, such as i18n!("locales", fallback = ["en", "es"]). Since version 2.4.0 it also falls back automatically by territory, so a missing zh-CN resolves to zh.
What is the difference between the two locale file versions?
Version 1 splits each locale into its own file, which suits handing translation work out per language. Version 2 puts every locale in one file with the languages nested under each key, which the README says is easy to translate quickly with AI tools. Both are supported and the project states both versions will always be kept.
Can rust-i18n load translations at runtime?
Not by default. The parsing dependencies are compile-time only and are not included in the binary. The load-path feature enables runtime locale loading through try_load_locales, and it also pulls the parsing code into the built artifact.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/longbridge-rust-i18n)Was this useful?