Почему вашему Rust-приложению нужна интернационализация (и как не наломать дров)
Вы написали идеальный парсер на Rust. Он летает быстрее молнии и жрёт меньше памяти, чем скромная синица. Но есть одна проблема: ваш текст выводится только на английском. А пользователи из Токио, Берлина и Санкт-Петербурга хотят видеть интерфейс на родном языке.
Интернационализация (i18n) — это не просто «перевести кнопки». Это архитектурное решение, которое может превратить ваш код в элегантный конструктор или в адскую свалку из if-else. В этой статье мы разберём, как сделать интернационализацию в Rust правильно: без боли, с примерами и с уважением к мозгам будущего разработчика (вашему).
Мы поговорим о двух мощных подходах: библиотеке fluent (рекомендация Mozilla) и классическом rust-i18n на основе gettext. К концу статьи вы сможете добавить поддержку русского, английского и любого другого языка в свою тулзу за 15 минут.
План битвы: что такое i18n и l10n в мире Rust
Прежде чем кидаться в код, давайте договоримся о терминах:
- Интернационализация (i18n) — подготовка кода к переводу. Вы выносите все строки в отдельные файлы и учите программу подхватывать нужную локаль.
- Локализация (l10n) — сам перевод. Создание файлов для каждого языка.
- Локаль — комбинация языка и региона (например,
ru-RU,en-US,de-DE).
В Rust есть несколько путей. Мы рассмотрим два самых популярных:
- Fluent — современная система от Mozilla. Мощная, гибкая, с поддержкой плюрализации и контекста.
- rust-i18n — макросы, вдохновлённые Ruby on Rails. Проще, но менее гибкая.
Выбор зависит от задачи. Для CLI-утилит хватит rust-i18n. Для веб-приложений или сложных GUI (например, на Tauri) лучше взять Fluent.
Способ №1: Интернационализация Rust с Fluent (профессиональный подход)
Установка и настройка Fluent
Создадим новый проект и добавим зависимости:
cargo new i18n_demo
cd i18n_demo
cargo add fluent fluent-bundle fluent-langtag unic-langid
Структура файлов будет такой:
проект/
├── Cargo.toml
├── locales/
│ ├── ru/
│ │ └── main.ftl
│ └── en/
│ └── main.ftl
└── src/
└── main.rs
Создаём файлы локализации (FTL)
Fluent использует формат FTL (Fluent Translation List). Это чистота и мощь:
locales/en/main.ftl:
greeting = Hello, { $name }!
goodbye = Goodbye
apples = { $count ->
[one] { $count } apple
[few] { $count } apples
*[other] { $count } apples
}
locales/ru/main.ftl:
greeting = Привет, { $name }!
goodbye = До свидания
apples = { $count ->
[one] { $count } яблоко
[few] { $count } яблока
[many] { $count } яблок
*[other] { $count } яблок
}
Обратите внимание на плюрализацию. В русском языке четыре формы (один, два-четыре, пять-двадцать, остальное), а в английском — две (один, остальное). Fluent справляется с этим автоматически.
Пишем код на Rust для загрузки локалей
Теперь самое интересное — код, который будет выбирать нужный язык в зависимости от системы пользователя:
use fluent_bundle::{FluentBundle, FluentResource};
use unic_langid::LanguageIdentifier;
use std::fs;
use std::path::Path;
fn load_locale(locale: &str) -> FluentBundle<FluentResource> {
let langid: LanguageIdentifier = locale.parse().expect("Неверная локаль");
let mut bundle = FluentBundle::new(vec![langid]);
let file_path = format!("locales/{}/main.ftl", locale);
let source = fs::read_to_string(&file_path)
.expect(&format!("Не найден файл: {}", file_path));
let resource = FluentResource::try_new(source)
.expect("Ошибка парсинга FTL");
bundle.add_resource(resource)
.expect("Ошибка добавления ресурса");
bundle
}
fn main() {
// Определяем системную локаль (упрощённо)
let system_locale = std::env::var("LANG")
.unwrap_or_else(|_| "en_US.UTF-8".to_string())
.split('.')
.next()
.unwrap_or("en")
.replace('_', "-");
let locale = if system_locale.starts_with("ru") { "ru" } else { "en" };
let bundle = load_locale(locale);
// Используем паттерн "Message" для получения строк
let mut errors = vec![];
let greeting = bundle.get_message("greeting")
.and_then(|msg| msg.value())
.map(|value| {
let mut pattern = value.clone();
bundle.format_pattern(&mut pattern, None, &mut errors)
})
.unwrap_or_else(|| "Привет, мир!".to_string());
println!("{}", greeting.replace("{$name}", "Иван"));
}
Этот код автоматически определит язык системы (через переменную LANG) и подгрузит нужный файл. Если пользователь из России — увидит приветствие на русском, из США — на английском.
Продвинутая плюрализация в деле
Добавим вывод количества яблок:
fn pluralize_example(bundle: &FluentBundle<FluentResource>, count: u32) {
let mut errors = vec![];
let msg = bundle.get_message("apples").unwrap();
let mut args = fluent_bundle::FluentArgs::new();
args.set("count", count);
if let Some(value) = msg.value() {
let mut pattern = value.clone();
let result = bundle.format_pattern(&mut pattern, Some(&args), &mut errors);
println!("{}", result);
}
}
Вызов pluralize_example(&bundle, 1) вернёт "1 apple" для английского и "1 яблоко" для русского. pluralize_example(&bundle, 3) — "3 apples" и "3 яблока" соответственно. Магия плюрализации в Rust работает без единого if-else.
Способ №2: Интернационализация Rust с rust-i18n (быстрый старт)
Если вам не нужны сложные формы и вы хотите минимум кода — используйте rust-i18n. Это макросы, которые делают всю грязную работу за вас.