Why Your Rust Application Needs Internationalization (and How Not to Mess It Up)
\n\nYou wrote the perfect parser in Rust. It flies faster than lightning and consumes less memory than a modest titmouse. But there is one problem: your text is only output in English. And users from Tokyo, Berlin, and Saint Petersburg want to see the interface in their native language.
\n\nInternationalization (i18n) is not just about "translating buttons." It is an architectural decision that can turn your code into an elegant constructor or a hellish dump of if-else statements. In this article, we will break down how to do internationalization in Rust correctly: without pain, with examples, and with respect for the future developer's brain (yours).
\n\nWe will talk about two powerful approaches: the fluent library (Mozilla's recommendation) and the classic rust-i18n based on gettext. By the end of the article, you will be able to add support for Russian, English, and any other language to your tool in 15 minutes.
Battle Plan: What are i18n and l10n in the Rust World
\n\nBefore diving into the code, let's agree on terms:
\n- \n
- Internationalization (i18n) — preparing the code for translation. You extract all strings into separate files and teach the program to pick up the required locale. \n
- Localization (l10n) — the translation itself. Creating files for each language. \n
- Locale — a combination of language and region (e.g.,
ru-RU,en-US,de-DE). \n
In Rust, there are several paths. We will look at the two most popular:
\n- \n
- Fluent — a modern system from Mozilla. Powerful, flexible, with support for pluralization and context. \n
- rust-i18n — macros inspired by Ruby on Rails. Simpler, but less flexible. \n
The choice depends on the task. For CLI utilities, rust-i18n is sufficient. For web applications or complex GUIs (e.g., on Tauri), it is better to use Fluent.
Method #1: Rust Internationalization with Fluent (Professional Approach)
\n\nInstalling and Configuring Fluent
\n\nLet's create a new project and add dependencies:
\n\ncargo new i18n_demo\ncd i18n_demo\ncargo add fluent fluent-bundle fluent-langtag unic-langid\n\n\nThe file structure will be like this:
\n\nproject/\n├── Cargo.toml\n├── locales/\n│ ├── ru/\n│ │ └── main.ftl\n│ └── en/\n│ └── main.ftl\n└── src/\n └── main.rs\n\n\nCreating Localization Files (FTL)
\n\nFluent uses the FTL (Fluent Translation List) format. It is clean and powerful:
\n\nlocales/en/main.ftl:
\ngreeting = Hello, { $name }!\ngoodbye = Goodbye\napples = { $count ->\n [one] { $count } apple\n [few] { $count } apples\n *[other] { $count } apples\n}\n\n\nlocales/ru/main.ftl:
\ngreeting = Привет, { $name }!\ngoodbye = До свидания\napples = { $count ->\n [one] { $count } яблоко\n [few] { $count } яблока\n [many] { $count } яблок\n *[other] { $count } яблок\n}\n\n\nNote the pluralization. In Russian, there are four forms (one, two-four, five-twenty, the rest), while in English there are two (one, the rest). Fluent handles this automatically.
\n\nWriting Rust Code to Load Locales
\n\nNow for the interesting part — the code that will select the desired language based on the user's system:
\n\nuse fluent_bundle::{FluentBundle, FluentResource};\nuse unic_langid::LanguageIdentifier;\nuse std::fs;\nuse std::path::Path;\n\nfn load_locale(locale: &str) -> FluentBundle<FluentResource> {\n let langid: LanguageIdentifier = locale.parse().expect("Invalid locale");\n let mut bundle = FluentBundle::new(vec![langid]);\n\n let file_path = format!("locales/{}/main.ftl", locale);\n let source = fs::read_to_string(&file_path)\n .expect(&fo