pottie4r 2

Başarılı
Katılım
2 Ocak 2024
Mesajlar
42
Makaleler
1
Beğeniler
44
Çoğumuz ASP.NET Core'da localization eklerken birkaç satır kod yazıp geçiyoruz. Peki IStringLocalizer çağırdığımızda arka tarafta hangi servisler çalışıyor? Culture bilgisi nasıl belirleniyor? Bu soruların cevaplarını işte bu makalede vermeye çalıştım. Umarım işinize yarar.

Bölüm 1: Localization Servisleri​

Lokalizasyon için ASP.NET Core bünyesinde birçok interface bulunmaktadır. Bu yapı sayesinde ASP.NET bize oldukça modüler bir yaklaşım sunmakta. Şimdi buradan bazı interface'leri ele alalım.

IStringLocalizer ve StringLocalizer Wrapper'ı​

IStringLocalizer interface'imizin amacı adından da belli olduğu üzere string'lerimizi hardcoded değil de Culture'a göre yerelleştirilmiş olarak getirilmesini sağlamaktır. Kendisi ASP.NET Core'da şu şekilde tanımlanmıştır:
C#:
public interface IStringLocalizer
  {
    /// <summary>Gets the string resource with the given name.</summary>
    /// <param name="name">The name of the string resource.</param>
    /// <returns>The string resource as a <see cref="T:Microsoft.Extensions.Localization.LocalizedString" />.</returns>
    LocalizedString this[string name] { get; }

    /// <summary>
    /// Gets the string resource with the given name and formatted with the supplied arguments.
    /// </summary>
    /// <param name="name">The name of the string resource.</param>
    /// <param name="arguments">The values to format the string with.</param>
    /// <returns>The formatted string resource as a <see cref="T:Microsoft.Extensions.Localization.LocalizedString" />.</returns>
    LocalizedString this[string name, params object[] arguments] { get; }

    /// <summary>Gets all string resources.</summary>
    /// <param name="includeParentCultures">
    /// A <see cref="T:System.Boolean" /> indicating whether to include strings from parent cultures.
    /// </param>
    /// <returns>The strings.</returns>
    IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
  }
Bu interface'i implemente etmekte olan sınıflardan birisi ResourceManagerStringLocalizer sınıfıdır. Bu sınıf, kendisine Factory sınıfı tarafından sağlanan Resource Manager'i kullanarak resources dosyasından (.resx) istenen string'i bulur ve bize döndürür. Örneğin "Resources/Controllers/HomeController.tr.resx" yolunda bulunup derlenip embed edilen resource manifest'i kullanarak ilgili lokalize edilmiş string'i verir.

StringLocalizer implementasyonu ise bir sarmalayıcı (wrapper) sınıftır. Biz bir controller'da yahut bir serviste IStringLocalizer<T> çağırdığımızda default olarak DI Container bu jenerik yapıyı arkadaki factory mekanizmasıyla çözümler.


IStringLocalizer<T> Generic Türü​

Yukarıda StringLocalizer implementasyonunun bir sarmalayıcı (wrapper) olduğundan bahsetmiştik. Peki ama neden doğrudan IStringLocalizer kullanmak yerine çoğunlukla IStringLocalizer<T> jenerik arayüzünü tercih ediyoruz?

Buradaki asıl olay, ASP.NET Core’un Dependency Injection (DI) container'ının sunduğu pratiklikte yatıyor. Biz bir Controller veya Service constructor'ında jenerik olan IStringLocalizer<HomeController> tipini talep ettiğimizde, çalışma zamanında (runtime) arka planda kabaca şu adımlar gerçekleşir:

  1. DI Container, jenerik tipi çözümlerken IStringLocalizerFactory instance'ına gider.
  2. Factory'ye parametre olarak typeof(HomeController) bilgisi geçilir.
  3. Factory, HomeController tipinin namespace ve assembly bilgilerini kullanarak otomatik olarak doğru .resx dosyasının adını ve konumunu (baseName ve location) hesaplar.
  4. Sonuç olarak o tipe özel yapılandırılmış bir ResourceManagerStringLocalizer üretilir ve sarmalanarak bize sunulur.
Bu sayede her servis için manuel olarak dosya yolu belirtmek zorunda kalmayız. Sadece IStringLocalizer<T> istememiz, doğru kaynak dosyasıyla eşleşmemiz için yeterlidir.


IStringLocalizerFactory ve ResourceManagerStringLocalizerFactory İmplementasyonu​

Bu interface kendisini implemente eden her instance'ın bir IStringLocalizer üretebileceğini söylüyor. Interface tanımı ise ASP.NET Core repo'sunda şu şekilde okunabiliyor:
C#:
  public interface IStringLocalizerFactory
  {
    /// <summary>
    /// Creates an <see cref="T:Microsoft.Extensions.Localization.IStringLocalizer" /> using the <see cref="T:System.Reflection.Assembly" /> and
    /// <see cref="P:System.Type.FullName" /> of the specified <see cref="T:System.Type" />.
    /// </summary>
    /// <param name="resourceSource">The <see cref="T:System.Type" />.</param>
    /// <returns>The <see cref="T:Microsoft.Extensions.Localization.IStringLocalizer" />.</returns>
    IStringLocalizer Create(Type resourceSource);

    /// <summary>
    /// Creates an <see cref="T:Microsoft.Extensions.Localization.IStringLocalizer" />.
    /// </summary>
    /// <param name="baseName">The base name of the resource to load strings from.</param>
    /// <param name="location">The location to load resources from.</param>
    /// <returns>The <see cref="T:Microsoft.Extensions.Localization.IStringLocalizer" />.</returns>
    IStringLocalizer Create(string baseName, string location);
  }
ASP.NET Core'da başka herhangi bir düzenleme yapmaksızın Localization servislerini yükleyip çağırdığınızda varsayılan olarak ResourceManagerStringLocalizerFactory kullanılacaktır.
Bu sınıfın içeriğini okumak için buradan kendi GitHub reposundan okuyabilirsiniz.

Bu sınıf temel olarak kendisinden IStringLocalizer talep edildiğinde Resource dosyasının (.resx) yerini belirler, bu Resource dosyasını okumakla yükümlü olan ResourceManagerStringLocalizer (dikkat bu öge bir factory değil, işi yapan ögedir) ögesini yaratır ve bilgileri iletir. Ayrıca içerisinde aynı jenerik tipler için IStringLocalizer talep edildiğinde yeniden üretmekle vakit ve kaynak israf etmemesini sağlayacak önbellek sistemi mevcuttur.

Yani en genelinde biz şunu yaparız:
C#:
// Configure services...
builder.Services.AddLocalization();

// IStringLocalizerFactory için ResourceManagerStringLocalizerFactory kaydedilir.
// IStringLocalizer<T> için StringLocalizer<T> kaydedilir.

// Controller'da ise şöyle kullanılır:

public class HomeController : Controller {
    private IStringLocalizer<HomeController> _localizer;
    public HomeController(IStringLocalizer<HomeController> localizer) {
        _localizer = localizer;
    }
 
    public View Index() {
        ViewBag["Karsilama"] = _localizer["Karsilama"];
    }
}

// Burada Constructor'dan localizer istediğimizde DI -> IStringLocalizerFactory'i çözer -> ResourceManagerStringLocalizerFactory
// Factory -> Create(typeof(HomeController)) girer -> sonuç: ResourceManagerStringLocalizer
// Derlenmiş olan RESX dosyasına göre string'i verir.

Başka bir şekilde (bir diyagram ile) durumu şu şekilde özetleyebiliriz. Örneğin biz HomeController'da IStringLocalizer<HomeController> istediğimizde şu akış işliyor.
Kod:
IStringLocalizer<T>
        |
        v
StringLocalizer<T>
        |
        v
IStringLocalizerFactory
        |
        v
ResourceManagerStringLocalizerFactory
        |
        v
ResourceManagerStringLocalizer
        |
        v
.resx

Bölüm 2: Culture, Dil Tespiti ve İletimi​

Her şey güzel. ASP.NET Core'un IStringLocalizer interface'i ve implementasyonları ve factory'leri hakkında bilgi sahibi olduk. Peki ASP.NET Core kullanıcının hangi dilde içeriği talep ettiğini nasıl anlayacak? Bunun için framework, HTTP Request pipeline'ında RequestLocalizationMiddleware kullanır. Kaynak kodunu buradan okuyabilirsiniz. Constructor'a baktığımızda aldığı parametrelerden birisinin IOptions ile sarmalanmış olan RequestLocalizationOptions olduğunu görebiliriz.
C#:
public RequestLocalizationMiddleware(RequestDelegate next, IOptions<RequestLocalizationOptions> options, ILoggerFactory loggerFactory)
    {
        ArgumentNullException.ThrowIfNull(options);

        _next = next ?? throw new ArgumentNullException(nameof(next));
        _logger = loggerFactory?.CreateLogger<RequestLocalizationMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
        _options = options.Value;
    }
Bu RequestLocalizationOptions'ı biz Program.cs dosyasında app.UseRequestLocalization() çağırırken Action üzerinden düzenleyebiliyoruz. Bu RequestLocalizationOptions'ın içine baktığımızda da RequestCultureProviders ögesini görüyoruz. Bu ögede RequestCultureProvider'lar tutulmakta. Constructor içinde de RequestCultureProviders'ın varsayılan olarak şu şekilde oluşturulduğunu görmekteyiz.

C#:
public RequestLocalizationOptions()
{
   RequestCultureProviders = new List<IRequestCultureProvider>
   {
       new QueryStringRequestCultureProvider { Options = this },
       new CookieRequestCultureProvider { Options = this },
       new AcceptLanguageHeaderRequestCultureProvider { Options = this }
   };
}

Bu ...CultureProvider şeklinde olan sınıflarımızın hepsinin amacı genel olarak HTTP isteği kapsamınca talep edilen dili bulmaktır. Miras aldıkları RequestCultureProvider base sınıfında DetermineProviderCultureResult metodu bulunur. RequestLocalizationMiddleware ise bu Provider'lar üzerinden talep edilen dili algılayıp ExecutionContext'te tutulmasına yardımcı olur.

Bölüm 3: Varsayılan Provider'ların Anatomisi ve Custom Provider Yazmak​

Bölüm 2'de bahsettiğin RequestCultureProviders listesindeki üç varsayılan provider'ın çalışma mantığı ve öncelik sırası (yukarıdan aşağıya doğru) şu şekildedir:
ProviderTespit YöntemiÖrnek Kullanım
QueryStringRequestCultureProvider URL'deki query parametrelerine bakar.
Kod:
 ?culture=tr-TR&ui-culture=tr-TR
CookieRequestCultureProvider Belirlenen bir cookie değerini okur.
Kod:
 .AspNetCore.Culture=c=en-US|uic=en-US
AcceptLanguageHeaderRequestCultureProvider Tarayıcının gönderdiği HTTP Header'ını okur.
Kod:
 Accept-Language: tr-TR,tr;q=0.9,en;q=0.8
Middleware, bu listeyi sırasıyla döner. İlk null dönmeyen (yani kültürü başarıyla tespit eden) provider'ın sonucunu kabul eder ve süreci sonlandırır. Eğer hiçbirinden sonuç çıkmazsa, RequestLocalizationOptions.DefaultRequestCulture değerini baz alır.

Kendi Custom Culture Provider'ımızı yazalım.​

Gerçek senaryolarda bazen dili URL path'inden (/api/tr/products) ya da istekteki özel bir HTTP Header'ından (X-Language) okumak isteyebiliriz. Gelin, gelen isteklerdeki X-Language custom header'ını okuyan bir provider yazalım:
C#:
using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Localization;using System;using System.Threading.Tasks;

public class HeaderRequestCultureProvider : RequestCultureProvider{private readonly string _headerName;

public HeaderRequestCultureProvider(string headerName = "X-Language")
{
    _headerName = headerName;
}

public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
    ArgumentNullException.ThrowIfNull(httpContext);

    // İstekteki header'ı oku
    if (httpContext.Request.Headers.TryGetValue(_headerName, out var languageHeader))
    {
        var requestCulture = languageHeader.ToString();

        if (!string.IsNullOrWhiteSpace(requestCulture))
        {
            // Hem Culture hem de UICulture için aynı değeri set ediyoruz
            var result = new ProviderCultureResult(requestCulture, requestCulture);
            return Task.FromResult<ProviderCultureResult>(result);
        }
    }

    // Eğer header bulunamazsa null dönerek sıradaki provider'a pasla
    return Task.FromResult<ProviderCultureResult>(null);}
}

Bölüm 4: View ve Model Seviyesinde Yerelleştirme.​

Yerelleştirme sadece Controller içindeki string'lerle sınırlı değildir. ASP.NET Core, arayüz (View) ve veri doğrulama (Model Validation) süreçleri için de özelleştirilmiş servisler sunar.

1. IHtmlLocalizer ve IViewLocalizer.​

UI (Razor Views) tarafında string'leri yerelleştirirken IStringLocalizer yerine IHtmlLocalizer veya IViewLocalizer tercih edilir. IViewLocalizer, Razor view'ın bulunduğu klasör yoluna göre otomatik olarak doğru .resx dosyasını arar.

IViewLocalizer, Views/Home/Index.cshtml için şu şekilde kullanılır:
HTML:
@using Microsoft.AspNetCore.Mvc.Localization@inject IViewLocalizer Localizer

2. Data Annotations (Model Validation) Yerelleştirmesi.​

API veya MVC projelerinde DTO/Model doğrulaması yaparken kullandığımız Required, StringLength gibi attribute'ların hata mesajlarını da yerelleştirebiliriz. Bunun için öncelikle Program.cs üzerinde bunu aktifleştirmeliyiz:
C#:
builder.Services.AddControllersWithViews().AddDataAnnotationsLocalization(options => {
     options.DataAnnotationLocalizerProvider = (type, factory) =>
           factory.Create(typeof(SharedResource)); // Ortak bir resource dosyası kullanmak için
     }
);
Ardından Model sınıfımızda hata mesajı anahtarını (Key) belirtmemiz yeterlidir:
C#:
public class LoginModel {
     [Required(ErrorMessage = "EmailRequired")]
     [EmailAddress(ErrorMessage = "InvalidEmail")]
     public string Email { get; set; } = string.Empty;
}

Bölüm 5: Pratik Bir Program.cs Konfigürasyonu.​

Tüm bu öğrendiklerimizi modern bir ASP.NET Core (.NET 8/9+) projesinde bir araya getirelim. İşte tüm pipeline'ın ve servislerin ayağa kaldırıldığı örnek bir Program.cs kesiti:
C#:
using System.Globalization;using Microsoft.AspNetCore.Localization;

var builder = WebApplication.CreateBuilder(args);
// 1. Localization Servislerini Kaydet
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddControllersWithViews().AddViewLocalization().AddDataAnnotationsLocalization();

var app = builder.Build();
// 2. Desteklenen Dilleri ve Seçenekleri Tanımla
var supportedCultures = new[]{new CultureInfo("tr-TR"),new CultureInfo("en-US")};
var localizationOptions = new RequestLocalizationOptions{DefaultRequestCulture = new RequestCulture("tr-TR"),SupportedCultures = supportedCultures,SupportedUICultures = supportedCultures};
// 3. Custom Provider'ımızı listenin EN BAŞINA ekleyelim (Öncelikli olması için)
localizationOptions.RequestCultureProviders.Insert(0, new HeaderRequestCultureProvider("X-Language"));
// 4. Middleware Pipeline'a Ekle
app.UseRequestLocalization(localizationOptions);

app.UseStaticFiles()
;app.UseRouting();app.UseAuthorization();
app.MapControllerRoute(name: "default",pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();

Bölüm 6: Arka Plandaki İşleyiş Özeti​

Middleware devreye girdiğinde akış şeması tam olarak şu şekilde çalışır:
  1. İstek gelir: Tarayıcı veya istemci GET /api/products isteğini gönderir (Header: X-Language: en-US).
  2. Middleware tetiklenir: RequestLocalizationMiddleware devreye girer.
  3. Provider'lar sorgulanır: HeaderRequestCultureProvider kontrol edilir -> en-US tespit edilir ve süreç durdurulur.
  4. Kültür ataması yapılır: Bulunan kültür bilgisi CultureInfo.CurrentCulture ve CultureInfo.CurrentUICulture statik property'lerine atanır. Bu atama o HTTP isteğinin ömrü boyunca (daha doğrusu Async Pipeline boyunca) geçerlidir.
  5. Localization çözümlenir: Controller veya View içinde IStringLocalizer çağrıldığında, CurrentUICulture bilgisine bakılarak (en-US) ilgili .resx dosyasından eşleşen değer çekilir.
 
Son düzenleyen: Moderatör: