SEO//13 Min. Lesezeit

Rust i18n: Internationalisierung für Rust-Anwendungen

Eray Gündoğmuş
Teilen

Rust i18n: Internationalisierung für Rust-Anwendungen

Rusts Wurzeln in der Systemprogrammierung lassen vermuten, dass es vor allem für Low-Level-Code ohne Locale-Bezug eingesetzt wird – doch die Realität sieht ganz anders aus. Rust treibt Webservices, Desktop-Anwendungen, WebAssembly-Module und CLIs an, die globale Nutzer in mehreren Sprachen bedienen müssen.

Das Rust i18n-Ökosystem ist jünger als das vieler anderer Sprachen, hat aber erhebliche Investitionen erhalten – insbesondere rund um Mozillas Fluent-Projekt und das ICU4X-Projekt von Unicode. Dieser Leitfaden behandelt die wichtigsten Bibliotheken, Muster und Abwägungen für die Entwicklung internationalisierter Rust-Anwendungen.

Das Rust i18n-Ökosystem

CrateZweck
fluent / fluent-bundleMozillas Fluent-Lokalisierungssystem
fluent-templatesFluent-Integration für Template-Engines
i18n-embedCompile-Time-Locale-Einbettung und Runtime-Laden
rust-i18nMakro-basiertes i18n mit JSON/YAML/TOML-Unterstützung
icu (ICU4X)Unicode CLDR-basierte Zahlen-, Datums- und Pluralformatierung
unic-langidBCP 47 Sprachbezeichner-Parsing
accept-languageHTTP Accept-Language-Header-Parsing
chronoDatums-/Zeitverarbeitung (locale-unabhängig)

Ansatz 1: Fluent mit fluent-bundle

Mozillas Fluent ist ein Lokalisierungssystem, das entwickelt wurde, um die Grenzen von gettext und Schlüssel-Wert-String-Maps zu überwinden. Fluent verlagert sprachliche Komplexität aus dem Code in die Übersetzungsdateien, wo Übersetzer damit umgehen können.

Warum Fluent?

Traditionelle Ansätze (t("key")) legen sprachliche Entscheidungen in die Hände der Entwickler: „Brauche ich {count, plural, one {item} other {items}}?" Fluent überträgt diese Entscheidungen an den Übersetzer: Der Übersetzer schreibt die korrekten Pluralformen für seine Sprache.

Fluent unterstützt außerdem:

  • Attribut-Nachrichten (eine einzelne Entität mit mehreren übersetzten Varianten)
  • Nachrichten-Referenzen (übersetzten Text in anderen Nachrichten wiederverwenden)
  • Terme (gemeinsame, nicht lokalisierbare Definitionen wie Markennamen)
  • Selektoren (Bedingungen basierend auf Variablen, Zahlen und Daten)

Fluent-Syntax-Beispiel

# en-US/main.ftl

# Einfache Nachricht
welcome = Welcome to our app!

# Nachricht mit einer Variable
greeting = Hello, { $name }!

# Nachricht mit Plural-Auswahl
emails =
    { $count ->
        [0]    You have no new emails.
        [one]  You have one new email.
       *[other] You have { $count } new emails.
    }

# Nachricht mit Attributen (für UI-Elemente mit Label + Tooltip)
submit-button =
    .label = Submit
    .tooltip = Click to submit your form

# Terme (Markennamen, nicht übersetzbar)
-brand-name = Acme Corp

referral = Thank you for using { -brand-name }!
# fr/main.ftl

welcome = Bienvenue dans notre application !

greeting = Bonjour, { $name } !

emails =
    { $count ->
        [0]    Vous n'avez pas de nouveaux e-mails.
        [one]  Vous avez un nouvel e-mail.
       *[other] Vous avez { $count } nouveaux e-mails.
    }

submit-button =
    .label = Soumettre
    .tooltip = Cliquez pour soumettre votre formulaire

referral = Merci d'utiliser { -brand-name } !

fluent-bundle verwenden

use fluent::{FluentBundle, FluentResource};
use fluent_langneg::negotiate_languages;
use unic_langid::LanguageIdentifier;

fn create_bundle(locale: &str, ftl_string: &str) -> FluentBundle<FluentResource> {
    let langid: LanguageIdentifier = locale.parse().expect("Invalid locale");
    let mut bundle = FluentBundle::new(vec![langid]);

    let resource = FluentResource::try_new(ftl_string.to_string())
        .expect("Failed to parse FTL");
    
    bundle.add_resource(resource).expect("Failed to add resource");
    bundle
}

fn translate_emails(bundle: &FluentBundle<FluentResource>, count: i64) -> String {
    let msg = bundle.get_message("emails").expect("Message not found");
    let pattern = msg.value().expect("Message has no value");
    
    let mut args = fluent::FluentArgs::new();
    args.set("count", count);
    
    let mut errors = vec![];
    let value = bundle.format_pattern(pattern, Some(&args), &mut errors);
    
    if !errors.is_empty() {
        eprintln!("Fluent errors: {:?}", errors);
    }
    
    value.into_owned()
}

fn main() {
    let en_ftl = include_str!("../locales/en-US/main.ftl");
    let bundle = create_bundle("en-US", en_ftl);
    
    println!("{}", translate_emails(&bundle, 0));  // "You have no new emails."
    println!("{}", translate_emails(&bundle, 1));  // "You have one new email."
    println!("{}", translate_emails(&bundle, 5));  // "You have 5 new emails."
}

Ansatz 2: rust-i18n (Makro-basiert)

Für einfachere Anwendungen oder Entwickler, die einen vertrauten t!()-Makro-Ansatz bevorzugen, bietet rust-i18n eine ergonomische Schnittstelle, die durch YAML-Übersetzungsdateien unterstützt wird.

Einrichtung

# Cargo.toml
[dependencies]
rust-i18n = "3"

[package.metadata.i18n]
available-locales = ["en", "fr", "de", "ja"]
default-locale = "en"
load-path = "locales"

Übersetzungsdateien

# locales/en.yaml
welcome: "Welcome!"
greeting: "Hello, %{name}!"
items:
  zero: "No items"
  one: "One item"
  other: "%{count} items"
# locales/fr.yaml
welcome: "Bienvenue !"
greeting: "Bonjour, %{name} !"
items:
  zero: "Aucun élément"
  one: "Un élément"
  other: "%{count} éléments"

Das t!-Makro verwenden

use rust_i18n::t;

// i18n initialisieren (lädt Locale-Dateien zur Compile-Zeit)
rust_i18n::i18n!("locales");

fn main() {
    // Aktive Locale setzen
    rust_i18n::set_locale("fr");
    
    // Einfache Übersetzung
    println!("{}", t!("welcome")); // "Bienvenue !"
    
    // Mit Variableninterpolation
    println!("{}", t!("greeting", name = "Alice")); // "Bonjour, Alice !"
    
    // Pluralbehandlung
    for count in [0, 1, 5] {
        println!("{}", t!("items", count = count)); 
    }
}

Locale in einem Web-Handler verwenden (Axum)

use axum::{
    extract::{Extension, TypedHeader},
    headers::AcceptLanguage,
    response::Json,
};
use rust_i18n::t;
use serde_json::json;

async fn greet_handler(
    TypedHeader(accept_language): TypedHeader<AcceptLanguage>,
    Extension(state): Extension<AppState>,
) -> Json<serde_json::Value> {
    // Accept-Language parsen und beste Locale auswählen
    let locale = negotiate_locale(&accept_language, &["en", "fr", "de"]);
    
    // Locale für diese Anfrage setzen (Hinweis: in rust-i18n ist dies thread-local)
    rust_i18n::set_locale(&locale);
    
    Json(json!({
        "message": t!("welcome"),
        "locale": locale,
    }))
}

fn negotiate_locale(accept: &AcceptLanguage, supported: &[&str]) -> String {
    for quality_value in accept.iter() {
        let lang = quality_value.item.to_string();
        let prefix = lang.split('-').next().unwrap_or(&lang);
        if supported.contains(&prefix) {
            return prefix.to_string();
        }
    }
    "en".to_string()
}

ICU4X für locale-bewusste Formatierung

ICU4X ist ein Unicode-Projekt, das CLDR-basierte Internationalisierungs-Primitive in Rust bereitstellt. Es verarbeitet Zahlenformatierung, Datums-/Zeitformatierung, Pluralregeln und mehr – unter Verwendung des Common Locale Data Repository (CLDR) von Unicode als Datenquelle.

Zahlenformatierung

use icu::decimal::FixedDecimalFormatter;
use icu::locid::locale;
use fixed_decimal::FixedDecimal;

fn format_number(amount: f64, locale_str: &str) -> String {
    // Hinweis: ICU4X unterstützt eine feste Menge kompilierter Locales
    // Für dynamische Locale-Auswahl den Data Provider verwenden
    let locale = locale!("de"); // Deutsche Locale
    
    let fdf = FixedDecimalFormatter::try_new(
        &locale.into(),
        Default::default(),
    ).expect("locale data should be present");
    
    let decimal = FixedDecimal::from(amount as i64);
    fdf.format_to_string(&decimal)
}

Pluralregeln

use icu::plurals::{PluralRules, PluralRuleType, PluralCategory};
use icu::locid::locale;

fn get_plural_category(count: f64, locale_str: &str) -> PluralCategory {
    // Englische Pluralregeln
    let locale = locale!("en");
    let pr = PluralRules::try_new(
        &locale.into(),
        PluralRuleType::Cardinal,
    ).expect("locale data should be present");
    
    pr.category_for(FixedDecimal::from(count as i64))
}

fn translate_items(count: i64, locale: &str) -> &'static str {
    let category = get_plural_category(count as f64, locale);
    match (locale, category) {
        ("en", PluralCategory::One) => "1 item",
        ("en", _) if count == 0 => "No items",
        ("en", _) => "items", // Platzhalter, echte App würde formatieren
        _ => "items",
    }
}

Übersetzungsdateien zur Laufzeit vs. zur Compile-Zeit laden

Rust gibt Ihnen die Wahl zwischen Compile-Time- und Runtime-Laden von Übersetzungen:

Compile-Time-Einbettung (empfohlen für CLIs)

use std::collections::HashMap;

// Übersetzungsdateien zur Compile-Zeit einbetten
static EN_TRANSLATIONS: &str = include_str!("../locales/en.json");
static FR_TRANSLATIONS: &str = include_str!("../locales/fr.json");

fn load_translations() -> HashMap<String, HashMap<String, String>> {
    let mut translations = HashMap::new();
    translations.insert(
        "en".to_string(),
        serde_json::from_str(EN_TRANSLATIONS).expect("Valid JSON"),
    );
    translations.insert(
        "fr".to_string(),
        serde_json::from_str(FR_TRANSLATIONS).expect("Valid JSON"),
    );
    translations
}

Die Compile-Time-Einbettung erzeugt ein einzelnes Binary ohne Runtime-Abhängigkeiten von Locale-Dateien. Dies ist ideal für CLI-Tools und eingebettete Anwendungen.

Runtime-Laden (empfohlen für Webservices)

use std::collections::HashMap;
use std::path::Path;

fn load_translations_from_dir(dir: &Path) -> HashMap<String, HashMap<String, String>> {
    let mut translations = HashMap::new();
    
    for entry in std::fs::read_dir(dir).expect("locale dir exists") {
        let entry = entry.expect("valid dir entry");
        let path = entry.path();
        
        if path.extension().map_or(false, |ext| ext == "json") {
            let locale = path
                .file_stem()
                .expect("file has stem")
                .to_string_lossy()
                .to_string();
            
            let content = std::fs::read_to_string(&path)
                .expect("locale file readable");
            
            let strings: HashMap<String, String> = 
                serde_json::from_str(&content).expect("valid JSON");
            
            translations.insert(locale, strings);
        }
    }
    
    translations
}

Das Runtime-Laden ermöglicht die Aktualisierung von Übersetzungen ohne Neukompilierung – nützlich für Webservices, bei denen Übersetzungen häufig aktualisiert werden.

WebAssembly und Rust i18n

Rust kompiliert zu WebAssembly, und Rust i18n-Bibliotheken funktionieren in WASM mit einigen Einschränkungen:

  • Dateisystemzugriff ist in Browser-WASM-Umgebungen nicht verfügbar
  • Compile-Time-Einbettung (include_str!) funktioniert gut für WASM
  • ICU4X wurde speziell entwickelt, um in eingeschränkten Umgebungen einschließlich WASM zu funktionieren
#[cfg(target_arch = "wasm32")]
mod wasm {
    use wasm_bindgen::prelude::*;
    use rust_i18n::t;
    
    rust_i18n::i18n!("locales");
    
    #[wasm_bindgen]
    pub fn translate(key: &str, locale: &str) -> String {
        rust_i18n::set_locale(locale);
        t!(key)
    }
}

i18n für Rust CLIs

CLI-Anwendungen erkennen die System-Locale aus Umgebungsvariablen:

use std::env;

pub fn detect_system_locale() -> String {
    // Priorität: LANG > LC_ALL > LC_MESSAGES > Standard
    for var in &["LANG", "LC_ALL", "LC_MESSAGES"] {
        if let Ok(val) = env::var(var) {
            // LANG ist typischerweise "en_US.UTF-8"
            let locale = val
                .split('.')  // Kodierungs-Suffix entfernen
                .next()
                .unwrap_or("en")
                .replace('_', "-");  // Nach BCP 47 konvertieren (en_US → en-US)
            
            if !locale.is_empty() {
                return locale;
            }
        }
    }
    
    // Windows: GetUserDefaultLocaleName über windows crate verwenden
    #[cfg(target_os = "windows")]
    {
        // ... Windows-Locale-Erkennung
    }
    
    "en".to_string()
}

Rust i18n testen

#[cfg(test)]
mod tests {
    use rust_i18n::t;

    #[test]
    fn test_english_plurals() {
        rust_i18n::set_locale("en");
        
        // Alle Pluralformen testen
        assert_eq!(t!("items", count = 0), "No items");
        assert_eq!(t!("items", count = 1), "One item");
        assert_eq!(t!("items", count = 5), "5 items");
    }

    #[test]
    fn test_french_translation() {
        rust_i18n::set_locale("fr");
        
        assert_eq!(t!("welcome"), "Bienvenue !");
        assert_eq!(t!("greeting", name = "Marie"), "Bonjour, Marie !");
    }

    #[test]
    fn test_fallback_to_english() {
        // Locale ohne Übersetzungsdatei fällt auf Englisch zurück
        rust_i18n::set_locale("xx");
        
        assert_eq!(t!("welcome"), "Welcome!"); // Englischer Fallback
    }
}

Umfassende Teststrategien für Rust finden Sie unter i18n-Testwerkzeuge, Strategien und Automatisierung.

Zwischen Fluent und rust-i18n wählen

KriteriumFluentrust-i18n
Sprachliche AusdrucksstärkeHochMittel
ÜbersetzerfreundlichSehrModerat
Entwickler-ErgonomieAusführlicherEinfache Makros
PluralbehandlungNativ in FTLSchlüssel-Suffixe
Community-AdoptionWächstStärkere Verbreitung
Am besten fürKomplexe Apps, Community-ÜbersetzungEinfachere Apps, Entwicklerteams

Für Anwendungen mit komplexen sprachlichen Anforderungen oder großen Übersetzergemeinschaften ist Fluents Ausdrucksstärke die zusätzliche Einrichtung wert. Für einfachere Apps oder von Entwicklern verwaltete Übersetzungen überzeugt die Ergonomie von rust-i18n.


Machen Sie Ihre App mit better-i18n global

better-i18n kombiniert KI-gestützte Übersetzungen, git-native Workflows und globale CDN-Auslieferung in einer entwicklerorientierten Plattform. Hören Sie auf, Tabellen zu verwalten, und beginnen Sie, in jeder Sprache zu veröffentlichen.

Kostenlos starten → · Funktionen erkunden · Dokumentation lesen

Comments

Loading comments...