Inhaltsverzeichnis
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
| Crate | Zweck |
|---|---|
fluent / fluent-bundle | Mozillas Fluent-Lokalisierungssystem |
fluent-templates | Fluent-Integration für Template-Engines |
i18n-embed | Compile-Time-Locale-Einbettung und Runtime-Laden |
rust-i18n | Makro-basiertes i18n mit JSON/YAML/TOML-Unterstützung |
icu (ICU4X) | Unicode CLDR-basierte Zahlen-, Datums- und Pluralformatierung |
unic-langid | BCP 47 Sprachbezeichner-Parsing |
accept-language | HTTP Accept-Language-Header-Parsing |
chrono | Datums-/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
| Kriterium | Fluent | rust-i18n |
|---|---|---|
| Sprachliche Ausdrucksstärke | Hoch | Mittel |
| Übersetzerfreundlich | Sehr | Moderat |
| Entwickler-Ergonomie | Ausführlicher | Einfache Makros |
| Pluralbehandlung | Nativ in FTL | Schlüssel-Suffixe |
| Community-Adoption | Wächst | Stärkere Verbreitung |
| Am besten für | Komplexe Apps, Community-Übersetzung | Einfachere 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