Интернационализация Rust приложений: от нуля до мультиязычности

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему вашему 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. Это макросы, которые делают всю грязную работу за вас.

Установка и настройка

Блоги

Рекомендаций по книгам