The article shows how to localize the new Identity Pages in an ASP.NET Core application. The views, code from the pages, and models require localized strings and are read from global identity resource files. This makes it easy to add translations for further languages, and prevents duplication.
Code: https://github.com/damienbod/AspNetCorePagesWebpack
The application is setup using this blog: Updating ASP.NET Core Identity to use Bootstrap 4
Setting up the localization in the application
To localize the identity views, code, and models, shared identity resource files are used for all the ASP.NET Core Identity pages. This makes it easy to localize this and re-use the resource files, instead of adding a 120 different resources files, for example when translating to 4 languages and duplicating the translations.
The ASP.NET Core application in this example is setup to localization to en-US and de-CH using cookies for the localized requests. The localization configuration is added to the default IoC and also the shared localized service which is used for all the Identity localizations.
The DataAnnotations is configured to use the IdentityResource class which represents the shared Identity resources.
public void ConfigureServices(IServiceCollection services) { ... /**** Localization configuration ****/ services.AddSingleton<IdentityLocalizationService>(); services.AddSingleton<SharedLocalizationService>(); services.AddLocalization(options => options.ResourcesPath = "Resources"); services.Configure<RequestLocalizationOptions>( options => { var supportedCultures = new List<CultureInfo> { new CultureInfo("en-US"), new CultureInfo("de-CH") }; options.DefaultRequestCulture = new RequestCulture(culture: "de-CH", uiCulture: "de-CH"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider()); }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1) .AddViewLocalization() .AddDataAnnotationsLocalization(options => { options.DataAnnotationLocalizerProvider = (type, factory) => { var assemblyName = new AssemblyName(typeof(IdentityResource).GetTypeInfo().Assembly.FullName); return factory.Create("IdentityResource", assemblyName.Name); }; }); }
The localization services which were configured are then used in the Configure method in the Startup class.
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>(); app.UseRequestLocalization(locOptions.Value);
A dummy IdentityResource class is created for the identity localization resources.
namespace AspNetCorePagesIdentity.Resources { /// <summary> /// Dummy class to group shared resources /// </summary> public class IdentityResource { } }
The IdentityLocalizationService is the service which can be used in the Page views to localize the texts.
using Microsoft.Extensions.Localization; using System.Reflection; namespace AspNetCorePagesIdentity.Resources { public class IdentityLocalizationService { private readonly IStringLocalizer _localizer; public IdentityLocalizationService(IStringLocalizerFactory factory) { var type = typeof(IdentityResource); var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName); _localizer = factory.Create("IdentityResource", assemblyName.Name); } public LocalizedString GetLocalizedHtmlString(string key) { return _localizer[key]; } public LocalizedString GetLocalizedHtmlString(string key, string parameter) { return _localizer[key, parameter]; } } }
The language switch was implemented using this blog from Andrew Lock (Thanks):
Adding Localisation to an ASP.NET Core application
The HTML was then re-styled to use bootstrap 4.
Identity Page View Localization
The ASP.NET Core Page texts are localized then using the IdentityLocalizationService. This is injected into the view and then used to get the correct texts. The input labels are also localized using this, and not the display attribute from the model.
@page @model LoginModel @{ ViewData["Title"] = @IdentityLocalizer.GetLocalizedHtmlString("ACCOUNT_LOGIN"); } @inject IdentityLocalizationService IdentityLocalizer <h2>@ViewData["Title"]</h2> <div class="row"> <div class="col-md-4"> <section> <form method="post"> <h4>@IdentityLocalizer.GetLocalizedHtmlString("ACCOUNT_USE_LOCAL_ACCOUNT_TO_LOG_IN")</h4> <hr /> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email">@IdentityLocalizer.GetLocalizedHtmlString("EMAIL")</label> <input asp-for="Input.Email" class="form-control" /> <span asp-validation-for="Input.Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.Password">@IdentityLocalizer.GetLocalizedHtmlString("PASSWORD")</label> <input asp-for="Input.Password" class="form-control" /> <span asp-validation-for="Input.Password" class="text-danger"></span> </div> <div class="form-group"> <div class="checkbox"> <label asp-for="Input.RememberMe"> <input asp-for="Input.RememberMe" /> @IdentityLocalizer.GetLocalizedHtmlString("REMEMBER_ME") </label> </div> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">@IdentityLocalizer.GetLocalizedHtmlString("ACCOUNT_LOGIN")</button> </div> <div class="form-group"> <p> <a asp-page="./ForgotPassword">@IdentityLocalizer.GetLocalizedHtmlString("FORGOT_YOUR_PASSWORD")</a> </p> <p> <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">@IdentityLocalizer.GetLocalizedHtmlString("REGISTER_AS_NEW_USER")</a> </p> </div> </form> </section> </div> <div class="col-md-6 col-md-offset-2"> <section> <h4>@IdentityLocalizer.GetLocalizedHtmlString("ACCOUNT_USE_ANOTHER_SERVICE_LOG_IN")</h4> <hr /> @{ if ((Model.ExternalLogins?.Count ?? 0) == 0) { <div> <p> @IdentityLocalizer.GetLocalizedHtmlString("ACCOUNT_NO_EXTERNAL_LOGINS") </p> </div> } else { <form asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> <div> <p> @foreach (var provider in Model.ExternalLogins) { <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> } </p> </div> </form> } } </section> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Identity Page Code Localization
The Page code uses the IStringLocalizerFactory to localize the status messages, response messages and model errors. This is setup to use the shared IdentityResource resource which is used for all the Identity translations. Then the messages, texts are translated as required. The model items use the ErrorMessage property to defined the resource identifier.
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Localization; using AspNetCorePagesIdentity.Resources; using System.Reflection; namespace AspNetCorePagesIdentity.Areas.Identity.Pages.Account { [AllowAnonymous] public class LoginModel : PageModel { private readonly SignInManager<IdentityUser> _signInManager; private readonly ILogger<LoginModel> _logger; private readonly IStringLocalizer _identityLocalizer; public LoginModel(SignInManager<IdentityUser> signInManager, ILogger<LoginModel> logger, IStringLocalizerFactory factory) { _signInManager = signInManager; _logger = logger; var type = typeof(IdentityResource); var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName); _identityLocalizer = factory.Create("IdentityResource", assemblyName.Name); } [BindProperty] public InputModel Input { get; set; } public IList<AuthenticationScheme> ExternalLogins { get; set; } public string ReturnUrl { get; set; } [TempData] public string ErrorMessage { get; set; } public class InputModel { [Required(ErrorMessage = "EMAIL_REQUIRED")] [EmailAddress(ErrorMessage = "EMAIL_INVALID")] public string Email { get; set; } [Required(ErrorMessage = "PASSWORD_REQUIRED")] [DataType(DataType.Password)] public string Password { get; set; } public bool RememberMe { get; set; } } public async Task OnGetAsync(string returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { ModelState.AddModelError(string.Empty, ErrorMessage); } returnUrl = returnUrl ?? Url.Content("~/"); // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ReturnUrl = returnUrl; } public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); if (ModelState.IsValid) { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { _logger.LogInformation("User logged in."); return LocalRedirect(returnUrl); } if (result.RequiresTwoFactor) { return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); } if (result.IsLockedOut) { _logger.LogWarning("User account locked out."); return RedirectToPage("./Lockout"); } else { ModelState.AddModelError(string.Empty, _identityLocalizer["INVALID_LOGIN_ATTEMPT"]); return Page(); } } // If we got this far, something failed, redisplay form return Page(); } } }
When the app is runs, everything is localized. Here’s an example in German:
Links:
https://github.com/aspnet/Identity
https://andrewlock.net/adding-localisation-to-an-asp-net-core-application/
Hi Damien,
I think you can make this simpler by immediately injecting an IStringLocalizer?
In that case you don’t have to use the factory to create the localizer (and jump through hoops for obtaining the assembly name), and you also don’t need the IdentityLocalizationService, as you can inject that same IStringLocalizer straight into the pageview (or into the _ViewImports.cshtml for that matter).
Hi fretje
thanks. I tried this first, but the type matching did not work, searched for the resources in the wrong folder
Greetings Damien
I think this is due to the fact that your IdentityResource class is not in the root namespace (in your case, that’s “AspNetCorePagesIdentity”). I don’t know exactly why, but I read somewhere that that has to be the case for everything to work properly.
For reference, the relevant part of my app startup looks like this (where my “SharedResources” is equivalent to your “IdentityResource”):
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Latest)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResources));
})
In my previous comment, “IStringLocalizer” should be “IStringLocalizer{lower than}IdentityResource{greater than}”, but apparently tags are being stripped out…
thx for sharing! what would be your take on strongly-typed localization possibilities (even in data annotation cases) using alternative provider? you can read more here: https://blog.tech-fellow.net/2018/01/27/dblocalizationprovider-in-asp-net-core/
[…] Adding Localization to the ASP.NET Core Identity Pages Source: ASP.NET Daily Articles […]
Hi Damien, good article. But for SEO purposes, you best use the culture in the URL, like https://www.mysite.com/en/catalogue or https://www.mysite.com/fr/catalogue or whatever. In MVC pages this can be achieved very easily, but do you have any idea how to localize the Identity razor pages? I’m trying to find a solution for this for a long time! Maybe it would be a good extensions to this article if you know.
Hey,
How do I run this program, what should I add in the URL to get the result in the required language.
Hi Sam
update your repo, and just start it using the IISExpress, https://localhost:44315/
Greetings Damien
Awesome work! I also implemented fretje’s suggestions.
Side note: there are some resources missing in the index page (phone number error message and unexpected error message). Not a big deal since I need to translate all resources to french anyway 🙂
thanks
Hi Damien,
I am using your excellent Localization.SqlLocalizer in a project. Here you refer to a similar solution but use SharedRessource instead of IdentityResource.
This works in many ways. It for instance automatically detect strings in annotations written in models in the program. ASP.NET Core does, however, appear to use strings that are not written to the project – for instance “The {0} field is required” and I cannot figure out how to get them. Does this project solve that?
This works in many ways. It for instance automatically detect strings in annotations written in models in the program. ASP.NET Core does, however, appear to use strings that are not written to the project – for instance “The {0} field is required” and I cannot figure out how to get them. Does this project solve that?
I have some problem with the validation messages when I insert a password that doesn’t meet the requirements (Upper+Lower+Number+Someothersymbol). In that case I still get the error message in english. And breakpoints set where the localizer converts get the strings are not hit. Where are defined those messages like “incorrect password”, “passwords must have at least one non alphanumeric character.”, “Passwords must have at least one uppercase (‘A’-‘Z’).” ?