Quantcast
Channel: damienbod – Software Engineering
Viewing all articles
Browse latest Browse all 357

Adding Localization to the ASP.NET Core Identity Pages

$
0
0

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://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.1#querystringrequestcultureprovider

https://github.com/aspnet/Identity

https://andrewlock.net/adding-localisation-to-an-asp-net-core-application/

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio%2Caspnetcore2x


Viewing all articles
Browse latest Browse all 357

Trending Articles