This article shows how ASP.NET Core MVC razor views and view models can use localized strings from a shared resource. This saves you creating many different files and duplicating translations for the different views and models. This makes it much easier to manage your translations, and also reduces the effort required to export, import the translations.
Code: https://github.com/damienbod/AspNetCoreMvcSharedLocalization
A default ASP.NET Core MVC application with Individual user accounts authentication is used to create the application.
A LocService class is used, which takes the IStringLocalizerFactory interface as a dependency using construction injection. The factory is then used, to create an IStringLocalizer instance using the type from the SharedResource class.
using Microsoft.Extensions.Localization; using System.Reflection; namespace AspNetCoreMvcSharedLocalization.Resources { public class LocService { private readonly IStringLocalizer _localizer; public LocService(IStringLocalizerFactory factory) { var type = typeof(SharedResource); var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName); _localizer = factory.Create("SharedResource", assemblyName.Name); } public LocalizedString GetLocalizedHtmlString(string key) { return _localizer[key]; } } }
The dummy SharedResource is required to create the IStringLocalizer instance using the type from the class.
namespace AspNetCoreMvcSharedLocalization.Resources { /// <summary> /// Dummy class to group shared resources /// </summary> public class SharedResource { } }
The resx resource files are added with the name, which matches the IStringLocalizer definition. This example uses SharedResource.de-CH.resx and the other localizations as required. One of the biggest problems with ASP.NET Core localization, if the name of the resx does not match the name/type of the class, view using the resource, it will not be found and so not localized. It will then use the default string, which is the name of the resource. This is also a problem as we programme in english, but the default language is german or french. Some programmers don’t understand german. It is bad to have german strings throughout the english code base.
The localization setup is then added to the startup class. This application uses de-CH, it-CH, fr-CH and en-US. The QueryStringRequestCultureProvider is used to set the request localization.
public void ConfigureServices(IServiceCollection services) { ... services.AddSingleton<LocService>(); services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddMvc() .AddViewLocalization() .AddDataAnnotationsLocalization(options => { options.DataAnnotationLocalizerProvider = (type, factory) => { var assemblyName = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName); return factory.Create("SharedResource", assemblyName.Name); }; }); services.Configure<RequestLocalizationOptions>( options => { var supportedCultures = new List<CultureInfo> { new CultureInfo("en-US"), new CultureInfo("de-CH"), new CultureInfo("fr-CH"), new CultureInfo("it-CH") }; options.DefaultRequestCulture = new RequestCulture(culture: "de-CH", uiCulture: "de-CH"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; options.RequestCultureProviders.Insert(0, new QueryStringRequestCultureProvider()); }); services.AddMvc(); }
The localization is then added as a middleware.
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>(); app.UseRequestLocalization(locOptions.Value); app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
Razor Views
The razor views use the shared resource localization by injecting the LocService. This was registered in the IoC in the startup class. The localized strings can then be used as required.
@model RegisterViewModel @using AspNetCoreMvcSharedLocalization.Resources @inject LocService SharedLocalizer @{ ViewData["Title"] = @SharedLocalizer.GetLocalizedHtmlString("register"); } <h2>@ViewData["Title"]</h2> <form asp-controller="Account" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal"> <h4>@SharedLocalizer.GetLocalizedHtmlString("createNewAccount")</h4> <hr /> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label class="col-md-2 control-label">@SharedLocalizer.GetLocalizedHtmlString("email")</label> <div class="col-md-10"> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> </div> </div> <div class="form-group"> <label class="col-md-2 control-label">@SharedLocalizer.GetLocalizedHtmlString("password")</label> <div class="col-md-10"> <input asp-for="Password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> </div> <div class="form-group"> <label class="col-md-2 control-label">@SharedLocalizer.GetLocalizedHtmlString("confirmPassword")</label> <div class="col-md-10"> <input asp-for="ConfirmPassword" class="form-control" /> <span asp-validation-for="ConfirmPassword" class="text-danger"></span> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <button type="submit" class="btn btn-default">@SharedLocalizer.GetLocalizedHtmlString("register")</button> </div> </div> </form> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
View Model
The models validation messages are also localized. The ErrorMessage of the attributes are used to get the localized strings.
using System.ComponentModel.DataAnnotations; namespace AspNetCoreMvcSharedLocalization.Models.AccountViewModels { public class RegisterViewModel { [Required(ErrorMessage = "emailRequired")] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } [Required(ErrorMessage = "passwordRequired")] [StringLength(100, ErrorMessage = "passwordStringLength", MinimumLength = 8)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "confirmPasswordNotMatching")] public string ConfirmPassword { get; set; } } }
The AddDataAnnotationsLocalization DataAnnotationLocalizerProvider is setup to always use the SharedResource resx files for all of the models. This prevents duplicating the localizations for each of the different models.
.AddDataAnnotationsLocalization(options => { options.DataAnnotationLocalizerProvider = (type, factory) => { var assemblyName = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName); return factory.Create("SharedResource", assemblyName.Name); }; });
The localization can be tested using the following requests:
https://localhost:44371/Account/Register?culure=de-CH&ui-culture=de-CH
https://localhost:44371/Account/Register?culure=it-CH&ui-culture=it-CH
https://localhost:44371/Account/Register?culure=fr-CH&ui-culture=fr-CH
https://localhost:44371/Account/Register?culure=en-US&ui-culture=en-US
The QueryStringRequestCultureProvider reads the culture and the ui-culture from the parameters. You could also use headers or cookies to send the required localization in the request, but this needs to be configured in the Startup class.
Links:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization
