SEO//13 dk okuma

Rust i18n: Rust Uygulamaları için Uluslararasılaştırma

Eray Gündoğmuş
Paylaş

Rust i18n: Rust Uygulamaları için Uluslararasılaştırma

Rust'ın sistem programlama kökleri, öncelikli olarak düşük seviyeli, yerel ayardan bağımsız kodlar için kullanıldığını düşündürebilir; ancak gerçeklik oldukça farklıdır. Rust; web servisleri, masaüstü uygulamaları, WebAssembly modülleri ve birden fazla dilde global kullanıcılara hizmet etmesi gereken CLI'lar için güç kaynağı olmaktadır.

Rust i18n ekosistemi, pek çok dilin ekosistemine kıyasla daha genç olmakla birlikte, özellikle Mozilla'nın Fluent projesi ve Unicode'un ICU4X girişimi etrafında önemli yatırımlar görmüştür. Bu kılavuz, uluslararasılaştırılmış Rust uygulamaları geliştirmek için başlıca kütüphaneleri, kalıpları ve değiş tokuşları ele almaktadır.

Rust i18n Ekosistemi

CrateAmaç
fluent / fluent-bundleMozilla'nın Fluent yerelleştirme sistemi
fluent-templatesTemplate engine'ler için Fluent entegrasyonu
i18n-embedDerleme zamanı yerel ayar gömme ve çalışma zamanı yükleme
rust-i18nJSON/YAML/TOML desteğiyle makro tabanlı i18n
icu (ICU4X)Unicode CLDR tabanlı sayı, tarih, çoğul biçimlendirme
unic-langidBCP 47 dil tanımlayıcısı ayrıştırma
accept-languageHTTP Accept-Language başlığı ayrıştırma
chronoTarih/saat işleme (yerel ayardan bağımsız)

Yaklaşım 1: fluent-bundle ile Fluent

Mozilla'nın Fluent sistemi, gettext ve anahtar-değer string haritalarının sınırlamalarını gidermek için tasarlanmış bir yerelleştirme sistemidir. Fluent, dilbilimsel karmaşıklığı kodunuzdan çıkararak çevirmenlerin bununla ilgilenebileceği çeviri dosyalarına taşır.

Neden Fluent?

Geleneksel yaklaşımlar (t("key")), dilbilimsel kararları geliştiricinin eline bırakır: "Çoğul form için {count, plural, one {item} other {items}} gerekli mi?" Fluent ise bu kararları çevirmene bırakır: çevirmen, kendi dili için doğru çoğul biçimlerini yazar.

Fluent ayrıca şunları destekler:

  • Öznitelik mesajları (birden fazla çevrilmiş varyanta sahip tek bir varlık)
  • Mesaj referansları (çevrilmiş metni diğer mesajlarda yeniden kullanma)
  • Terimler (marka adları gibi paylaşılan, yerelleştirilemeyen tanımlar)
  • Seçiciler (değişkenlere, sayılara ve tarihlere dayalı koşullar)

Fluent Sözdizimi Örneği

# en-US/main.ftl

# Basit mesaj
welcome = Welcome to our app!

# Değişkenli mesaj
greeting = Hello, { $name }!

# Çoğul seçimli mesaj
emails =
    { $count ->
        [0]    You have no new emails.
        [one]  You have one new email.
       *[other] You have { $count } new emails.
    }

# Öznitelikli mesaj (etiket + araç ipucu içeren UI öğeleri için)
submit-button =
    .label = Submit
    .tooltip = Click to submit your form

# Terimler (marka adları, çevrilmez)
-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 Kullanımı

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."
}

Yaklaşım 2: rust-i18n (Makro Tabanlı)

Daha basit uygulamalar veya tanıdık t!() makro yaklaşımını tercih eden geliştiriciler için rust-i18n, YAML çeviri dosyalarıyla desteklenen ergonomik bir arayüz sunar.

Kurulum

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

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

Çeviri Dosyaları

# 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"

t! Makrosunun Kullanımı

use rust_i18n::t;

// i18n'i başlat (derleme zamanında yerel ayar dosyalarını yükler)
rust_i18n::i18n!("locales");

fn main() {
    // Aktif yerel ayarı belirle
    rust_i18n::set_locale("fr");
    
    // Basit çeviri
    println!("{}", t!("welcome")); // "Bienvenue !"
    
    // Değişken enterpolasyonuyla
    println!("{}", t!("greeting", name = "Alice")); // "Bonjour, Alice !"
    
    // Çoğul işleme
    for count in [0, 1, 5] {
        println!("{}", t!("items", count = count)); 
    }
}

Web Handler'da Yerel Ayar Kullanımı (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'ı ayrıştır ve en uygun yerel ayarı seç
    let locale = negotiate_locale(&accept_language, &["en", "fr", "de"]);
    
    // Bu istek için yerel ayarı belirle (not: rust-i18n'de bu thread-local'dır)
    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()
}

Yerel Ayara Duyarlı Biçimlendirme için ICU4X

ICU4X, Rust'ta CLDR tabanlı uluslararasılaştırma ilkellerini sağlayan bir Unicode projesidir. Sayı biçimlendirme, tarih/saat biçimlendirme, çoğul kurallar ve daha fazlasını; Unicode'un Common Locale Data Repository (CLDR) veri kaynağını kullanarak yönetir.

Sayı Biçimlendirme

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

fn format_number(amount: f64, locale_str: &str) -> String {
    // Not: ICU4X, derlenmiş yerel ayarların sabit bir kümesini destekler
    // Dinamik yerel ayar seçimi için data provider kullanın
    let locale = locale!("de"); // Almanca yerel ayar
    
    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)
}

Çoğul Kurallar

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

fn get_plural_category(count: f64, locale_str: &str) -> PluralCategory {
    // İngilizce çoğul kuralları
    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", // yer tutucu, gerçek uygulama biçimlendirirdi
        _ => "items",
    }
}

Çeviri Dosyalarını Çalışma Zamanında ve Derleme Zamanında Yükleme

Rust, derleme zamanı ve çalışma zamanı çeviri yüklemesi arasında tercih yapmanıza olanak tanır:

Derleme Zamanı Gömme (CLI için Önerilen)

use std::collections::HashMap;

// Çeviri dosyalarını derleme zamanında göm
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
}

Derleme zamanı gömme, yerel ayar dosyalarına çalışma zamanı bağımlılığı olmayan tek bir ikili dosya üretir. Bu, CLI araçları ve gömülü uygulamalar için idealdir.

Çalışma Zamanı Yükleme (Web Servisleri için Önerilen)

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
}

Çalışma zamanı yükleme, yeniden derleme yapmadan çevirilerin güncellenmesine olanak tanır; çevirilerin sık güncellendiği web servisleri için kullanışlıdır.

WebAssembly ve Rust i18n

Rust, WebAssembly'ye derlenir ve Rust i18n kütüphaneleri bazı kısıtlamalarla WASM ortamında çalışır:

  • Tarayıcı WASM ortamlarında dosya sistemi erişimi mevcut değildir
  • Derleme zamanı gömme (include_str!) WASM için iyi çalışır
  • ICU4X, WASM dahil kısıtlı ortamlarda çalışmak üzere özel olarak tasarlanmıştır
#[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)
    }
}

Rust CLI'lar için i18n

CLI uygulamaları, sistem yerel ayarını ortam değişkenlerinden tespit eder:

use std::env;

pub fn detect_system_locale() -> String {
    // Öncelik: LANG > LC_ALL > LC_MESSAGES > varsayılan
    for var in &["LANG", "LC_ALL", "LC_MESSAGES"] {
        if let Ok(val) = env::var(var) {
            // LANG genellikle "en_US.UTF-8" biçimindedir
            let locale = val
                .split('.')  // Kodlama sonekini kaldır
                .next()
                .unwrap_or("en")
                .replace('_', "-");  // BCP 47'ye dönüştür (en_US → en-US)
            
            if !locale.is_empty() {
                return locale;
            }
        }
    }
    
    // Windows: windows crate aracılığıyla GetUserDefaultLocaleName kullan
    #[cfg(target_os = "windows")]
    {
        // ... Windows yerel ayar tespiti
    }
    
    "en".to_string()
}

Rust i18n Testi

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

    #[test]
    fn test_english_plurals() {
        rust_i18n::set_locale("en");
        
        // Tüm çoğul biçimleri test et
        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() {
        // Çeviri dosyası olmayan yerel ayar, İngilizce'ye geri döner
        rust_i18n::set_locale("xx");
        
        assert_eq!(t!("welcome"), "Welcome!"); // İngilizce geri dönüş
    }
}

Rust için geçerli kapsamlı test stratejileri için bkz. i18n test araçları, stratejileri ve otomasyon.

Fluent ve rust-i18n Arasında Seçim

KriterFluentrust-i18n
Dilbilimsel ifade gücüYüksekOrta
Çevirmen dostuÇokOrta
Geliştirici ergonomisiDaha ayrıntılıBasit makrolar
Çoğul işlemeFTL'de yerelAnahtar sonekleri
Topluluk benimsemesiBüyüyorDaha fazla benimseme
En iyi kullanımKarmaşık uygulamalar, topluluk çevirisiDaha basit uygulamalar, geliştirici ekipleri

Karmaşık dilbilimsel ihtiyaçları olan veya büyük çevirmen topluluklarına sahip uygulamalar için Fluent'in ifade gücü, ek kurulum zahmetine değer. Daha basit uygulamalar veya geliştirici yönetimli çeviriler için rust-i18n'nin ergonomisi öne çıkar.


Uygulamanızı better-i18n ile dünyaya açın

better-i18n; yapay zeka destekli çeviriler, git-native iş akışları ve global CDN dağıtımını tek bir geliştirici odaklı platformda bir araya getirir. Elektronik tablolar yönetmeyi bırakın, her dilde yayın yapmaya başlayın.

Ücretsiz başlayın → · Özellikleri keşfedin · Belgeleri okuyun

Comments

Loading comments...