Örnek projeyi github üzerinden indirmek veya görüntülemek için https://github.com/ayzdru/AspNetGlobalizationAndLocalization adresine gidebilirsiniz.

İlk Olarak
Küreselleşme, farklı kültürleri destekleyen ürünler tasarlama sürecidir. Küreselleşme, belirli coğrafi alanlarla ilgilidir.
Yerelleştirme, globalleştirilmiş bir projeyi belirli bir kültüre ve bölgeye uyarlama işlemidir.
Global ve Uluslararası bir yazılım projemiz olduğunda karşılaştığımız en büyük sorunlar Dil, Kültür, Parasal işlemler vs. gibi sorunlardır. Microsoft.Extensions.Localization ile bu sorunları çözebiliyoruz. Çok fazla dil seçeneğine sahip bir yazılım ürünü (özellikle İngilizce dil seçeneğine sahip) daha geniş bir kitleye erişmemizi sağlayacaktır. Örnek projede birden fazla seçeneği hep birlikte kullanacağım.
ASP.Net Identity
Projemizde Microsoft.AspNetCore.Identity paketini kullanıyorsak, default olarak ingilizce hata mesajlarıyla karşılacağız. Bunu çözmek için Identity'i konfigüre etmemiz gerekiyor.
IdentityErrorMessages.cs ve MultilanguageIdentityErrorDescriber.cs classlarını oluşturuyoruz.
public class IdentityErrorMessages
{
public const string ConcurrencyFailure = "İyimser eşzamanlılık hatası, nesne değiştirildi.";
public const string DefaultError = "Bilinmeyen bir hata oluştu.";
public const string DuplicateEmail = "'{0}' e-postası zaten alınmış.";
public const string DuplicateUserName = "'{0}' kullanıcı adı zaten alınmış.";
public const string InvalidEmail = "'{0}' e-postası geçersiz.";
public const string DuplicateRoleName = "Rol adı '{0}' zaten alınmış.";
public const string InvalidRoleName = "Rol adı '{0}' geçersiz.";
public const string InvalidToken = "Geçersiz token.";
public const string InvalidUserName = "'{0}' kullanıcı adı geçersiz, sadece harf veya rakam içerebilir.";
public const string LoginAlreadyAssociated = "Bu girişe sahip bir kullanıcı zaten var.";
public const string PasswordMismatch = "Yanlış şifre.";
public const string PasswordRequiresDigit = "Şifreler en az bir rakam içermelidir ('0'-'9').";
public const string PasswordRequiresLower = "Şifreler en az bir küçük harf içermelidir ('a'-'z').";
public const string PasswordRequiresNonAlphanumeric = "Şifreler en az bir alfasayısal olmayan karakter içermelidir.";
public const string PasswordRequiresUniqueChars = "Şifreler en az {0} farklı karakter kullanmalıdır.";
public const string PasswordRequiresUpper = "Şifreler en az bir büyük harfe sahip olmalıdır ('A'-'Z').";
public const string PasswordTooShort = "Şifreler en az {0} karakter olmalıdır.";
public const string UserAlreadyHasPassword = "Kullanıcının zaten bir şifre seti var.";
public const string UserAlreadyInRole = "Kullanıcı zaten '{0}' rolünde.";
public const string UserNotInRole = "Kullanıcı '{0}' rolünde değil.";
public const string UserLockoutNotEnabled = "Kilitleme bu kullanıcı için etkin değil.";
public const string RecoveryCodeRedemptionFailed = "Kurtarma kodu kullanımı başarısız oldu.";
}
public class MultilanguageIdentityErrorDescriber : IdentityErrorDescriber
{
private readonly IStringLocalizer<IdentityErrorMessages> _localizer;
public MultilanguageIdentityErrorDescriber(IStringLocalizer<IdentityErrorMessages> localizer)
{
_localizer = localizer;
}
public override IdentityError DuplicateEmail(string email)
{
return new IdentityError()
{
Code = nameof(DuplicateEmail),
Description = string.Format(_localizer[IdentityErrorMessages.DuplicateEmail], email)
};
}
public override IdentityError PasswordRequiresUniqueChars(int uniqueChars)
{
return new IdentityError { Code = nameof(PasswordRequiresUniqueChars), Description = _localizer[IdentityErrorMessages.PasswordRequiresUniqueChars] };
}
public override IdentityError RecoveryCodeRedemptionFailed()
{
return new IdentityError { Code = nameof(RecoveryCodeRedemptionFailed), Description = _localizer[IdentityErrorMessages.RecoveryCodeRedemptionFailed] };
}
public override IdentityError DefaultError() { return new IdentityError { Code = nameof(DefaultError), Description = _localizer[IdentityErrorMessages.DefaultError] }; }
public override IdentityError ConcurrencyFailure() { return new IdentityError { Code = nameof(ConcurrencyFailure), Description = _localizer[IdentityErrorMessages.ConcurrencyFailure] }; }
public override IdentityError PasswordMismatch() { return new IdentityError { Code = nameof(PasswordMismatch), Description = _localizer[IdentityErrorMessages.PasswordMismatch] }; }
public override IdentityError InvalidToken() { return new IdentityError { Code = nameof(InvalidToken), Description = _localizer[IdentityErrorMessages.InvalidToken] }; }
public override IdentityError LoginAlreadyAssociated() { return new IdentityError { Code = nameof(LoginAlreadyAssociated), Description = _localizer[IdentityErrorMessages.LoginAlreadyAssociated] }; }
public override IdentityError InvalidUserName(string userName) { return new IdentityError { Code = nameof(InvalidUserName), Description = string.Format(_localizer[IdentityErrorMessages.InvalidUserName], userName) }; }
public override IdentityError InvalidEmail(string email) { return new IdentityError { Code = nameof(InvalidEmail), Description = string.Format(_localizer[IdentityErrorMessages.InvalidEmail], email) }; }
public override IdentityError DuplicateUserName(string userName) { return new IdentityError { Code = nameof(DuplicateUserName), Description = string.Format(_localizer[IdentityErrorMessages.DuplicateUserName], userName) }; }
public override IdentityError InvalidRoleName(string role) { return new IdentityError { Code = nameof(InvalidRoleName), Description = string.Format(_localizer[IdentityErrorMessages.InvalidRoleName], role) }; }
public override IdentityError DuplicateRoleName(string role) { return new IdentityError { Code = nameof(DuplicateRoleName), Description = string.Format(_localizer[IdentityErrorMessages.DuplicateRoleName], role) }; }
public override IdentityError UserAlreadyHasPassword() { return new IdentityError { Code = nameof(UserAlreadyHasPassword), Description = _localizer[IdentityErrorMessages.UserAlreadyHasPassword] }; }
public override IdentityError UserLockoutNotEnabled() { return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = _localizer[IdentityErrorMessages.UserLockoutNotEnabled] }; }
public override IdentityError UserAlreadyInRole(string role) { return new IdentityError { Code = nameof(UserAlreadyInRole), Description = string.Format(_localizer[IdentityErrorMessages.UserAlreadyInRole], role) }; }
public override IdentityError UserNotInRole(string role) { return new IdentityError { Code = nameof(UserNotInRole), Description = string.Format(_localizer[IdentityErrorMessages.UserNotInRole], role) }; }
public override IdentityError PasswordTooShort(int length) { return new IdentityError { Code = nameof(PasswordTooShort), Description = string.Format(_localizer[IdentityErrorMessages.PasswordTooShort], length) }; }
public override IdentityError PasswordRequiresNonAlphanumeric() { return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = _localizer[IdentityErrorMessages.PasswordRequiresNonAlphanumeric] }; }
public override IdentityError PasswordRequiresDigit() { return new IdentityError { Code = nameof(PasswordRequiresDigit), Description = _localizer[IdentityErrorMessages.PasswordRequiresDigit] }; }
public override IdentityError PasswordRequiresLower() { return new IdentityError { Code = nameof(PasswordRequiresLower), Description = _localizer[IdentityErrorMessages.PasswordRequiresLower] }; }
public override IdentityError PasswordRequiresUpper() { return new IdentityError { Code = nameof(PasswordRequiresUpper), Description = _localizer[IdentityErrorMessages.PasswordRequiresUpper] }; }
}
Model Binding
Model Bind edildiğinde ortaya çıkan hatalar, varsayılan olarak ingilizce gösterilmektedir. Bunu çözmek için, MVC ayarlarını yapacağız.
ModelBindingMessages.cs classımızı oluşturuyoruz.
public class ModelBindingMessages
{
public const string ModelState_AttemptedValueIsInvalid = "{1} alanı için '{0}' değeri geçersiz.";
public const string ModelBinding_MissingBindRequiredMember = "'{0}' parametresi veya özelliği için bir değer verilmedi.";
public const string KeyValuePair_BothKeyAndValueMustBePresent = "Bir değer gerekli.";
public const string ModelBinding_MissingRequestBodyRequiredMember = "Boş olmayan bir istek gövdesi gereklidir.";
public const string ModelState_NonPropertyAttemptedValueIsInvalid = "'{0}' değeri geçerli değil.";
public const string ModelState_NonPropertyUnknownValueIsInvalid = "Sağlanan değer geçersiz.";
public const string HtmlGeneration_NonPropertyValueMustBeNumber = "Alan bir sayı olmalıdır.";
public const string ModelState_UnknownValueIsInvalid = "{0} alanı için sağlanan değer geçersiz.";
public const string HtmlGeneration_ValueIsInvalid = "'{0}' değeri geçersiz.";
public const string HtmlGeneration_ValueMustBeNumber = "{0} alanı bir sayı olmalıdır.";
public const string ModelBinding_NullValueNotValid = "'{0}' değeri geçersiz.";
}
Data Annotations
Binding Modellerimizde kullandığımız, [Required] gibi özelliklere çoklu dil desteği getireceğiz. Ben projede sadece Required özelliğini ekledim. Siz kullandığınız data annotationslara göre ekleme yapabilirsiniz. DataAnnotationsResources.resx buradaki linkten varsayılan değerleri görebilirsiniz.
DataAnnotationMessages.cs classımızı oluşturuyoruz.
public class DataAnnotationMessages
{
public const string Required = "{0} alanı zorunludur.";
}
Dil Kaynağı Bilgilerini Veritabanından Okuma
Varsayılan olarak Embedded Resource (.resx Gömülü Kaynak) kullanıyoruz. Çeviri değerlerini dinamik olarak veri tabanından çekebiliriz.
EFStringLocalizer.cs ve EFStringLocalizerFactory.cs classlarımızı oluşturuyoruz.
public class EFStringLocalizer : IStringLocalizer
{
private readonly ApplicationDbContext _db;
public EFStringLocalizer(ApplicationDbContext db)
{
_db = db;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.Select(r => new LocalizedString(r.Key, r.Value, true));
}
private string GetString(string name)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.FirstOrDefault(r => r.Key == name)?.Value;
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
return new EFStringLocalizer(_db);
}
}
public class EFStringLocalizerFactory : IStringLocalizerFactory
{
private readonly ApplicationDbContext _db;
public EFStringLocalizerFactory(ApplicationDbContext db)
{
_db = db;
}
public IStringLocalizer Create(Type resourceSource)
{
return new EFStringLocalizer(_db);
}
public IStringLocalizer Create(string baseName, string location)
{
return new EFStringLocalizer(_db);
}
}
Entity Framework Kullanarak Veri Tabanı Kayıtlarını Çoklu Dil Desteği ile Getirme ve Düzenleme
Örneğin;
Bir e-ticaret siteniz var. Veri tabanına bir ürün eklediniz. Ürünün adı 'Küçük Prens'. Veri tabanındaki bu kaydı dil seçeneğine göre getirebiliriz. İngilizce olduğunda 'The Little Prince' olur. Hatta bu kaydın Türkçe ve İngilizce karşılıklarını düzenleyebiliriz. Bunun için ben Xaki.AspNetCore paketini kullanacağım.

Resource Dosyalarını İsimlendirme
Ben Embedded Resource dosyalarını SubFolder şeklinde kullanıyorum.

Siz dilerseniz, Resources/Pages.Index.en.resx şeklinde nokta ile ayırarak kullanabilirsiniz. Bu arada IHtmlLocalizer kullanarak dil dosyalarındaki htmlleri ekrana parametre alacak şekilde yazdırabilirsiniz.
Son olarak Startup.cs dosyamıza gerekli düzenlemeleri yapıyoruz.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>().AddErrorDescriber<MultilanguageIdentityErrorDescriber>();
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("tr"),
new CultureInfo("en")
};
options.DefaultRequestCulture = new RequestCulture(culture: "tr", uiCulture: "tr");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
{
// My custom request culture logic
return new ProviderCultureResult("en");
}));
});
services.AddMvc(o =>
{
var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
var localizer = factory.Create(nameof(ModelBindingMessages), typeof(ModelBindingMessages).GetTypeInfo().Assembly.GetName().Name);
o.ModelBindingMessageProvider
.SetAttemptedValueIsInvalidAccessor((x, y) => localizer[ModelBindingMessages.ModelState_AttemptedValueIsInvalid, x, y]);
o.ModelBindingMessageProvider
.SetMissingBindRequiredValueAccessor((x) => localizer[ModelBindingMessages.ModelBinding_MissingBindRequiredMember, x]);
o.ModelBindingMessageProvider
.SetMissingKeyOrValueAccessor(() => localizer[ModelBindingMessages.KeyValuePair_BothKeyAndValueMustBePresent]);
o.ModelBindingMessageProvider
.SetMissingRequestBodyRequiredValueAccessor(() => localizer[ModelBindingMessages.ModelBinding_MissingRequestBodyRequiredMember]);
o.ModelBindingMessageProvider
.SetNonPropertyAttemptedValueIsInvalidAccessor((x) => localizer[ModelBindingMessages.ModelState_NonPropertyAttemptedValueIsInvalid, x]);
o.ModelBindingMessageProvider
.SetNonPropertyUnknownValueIsInvalidAccessor(() => localizer[ModelBindingMessages.ModelState_NonPropertyUnknownValueIsInvalid]);
o.ModelBindingMessageProvider
.SetNonPropertyValueMustBeANumberAccessor(() => localizer[ModelBindingMessages.HtmlGeneration_NonPropertyValueMustBeNumber]);
o.ModelBindingMessageProvider
.SetUnknownValueIsInvalidAccessor((x) => localizer[ModelBindingMessages.ModelState_UnknownValueIsInvalid, x]);
o.ModelBindingMessageProvider
.SetValueIsInvalidAccessor((x) => localizer[ModelBindingMessages.HtmlGeneration_ValueIsInvalid, x]);
o.ModelBindingMessageProvider
.SetValueMustBeANumberAccessor((x) => localizer[ModelBindingMessages.HtmlGeneration_ValueMustBeNumber, x]);
o.ModelBindingMessageProvider
.SetValueMustNotBeNullAccessor((x) => localizer[ModelBindingMessages.ModelBinding_NullValueNotValid, x]);
})
.AddViewLocalization(LanguageViewLocationExpanderFormat.SubFolder)
.AddDataAnnotationsLocalization(options => {
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(DataAnnotationMessages));
}).AddXaki(new XakiOptions
{
RequiredLanguages = new[] { "tr"},
OptionalLanguages = new[] { "en" }
});
services.AddSingleton<EFStringLocalizerFactory>(option =>
{
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
return new EFStringLocalizerFactory(new ApplicationDbContext(optionsBuilder.Options));
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseXaki();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
var supportedCultures = new[] { "tr", "en" };
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
Sağlıcakla kalın.