Shared Localization in ASP.NET Core MVC

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

History

2018-11-27 Updated to .NET Core 2.2

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

26 comments

  1. […] Shared Localization in ASP.NET Core MVC – Damien Bowden […]

  2. […] Shared Localization in ASP.NET Core MVC (Damien Bowden) […]

  3. no type safety though 😦

    1. Hi Paul, type safe where? Maybe I could improve this then. Thanks for the feedback.

      Greetings Damien

      1. I mean that using magic strings to retrieve the resources seems like a step backwards. I haven’t done any localization in .net core yet but in mvc5, you could reference a resx and use the type safe auto-generated file. e.g. SharedResources.LoginPageTitle

  4. Hi Damian
    You have done a great job with the localization series, however I have spent some time on them and must admit that I find them hard to follow, especially the sql localization (where angular is included)
    I have taken a look at the source code and I have not been able to reuse your code in my own sample.
    It would be nice with a very simple and short article on how to use sql localization in preferably with the language code visible in the url.

    1. ok, thanks for the feedback

  5. […] Read More (Community content) […]

  6. I tried to follow your guide, but still SharedLocalizer.GetLocalizedHtmlString(“something in SharedResource.en.resx”) return an object with ResourceNotFound=true and SearchedLocation=”MyPlace.Resources.SharedResource”. I’m lost :/

    1. make sure that you have installed “Localization.AspNetCore.TagHelpers” referance (nuget)

  7. […] I’m trying to localize a new project and looking at options determined for this particular use that a shared resource would be the most maintainable solution long term and followed this example: https://damienbod.com/2017/11/01/shared-localization-in-asp-net-core-mvc/ […]

  8. Why do you need the LocService class? When setting breakpoints inside this class they’re not reached.

  9. Krzysztof · · Reply

    Hi Damian

    How to use SharedResource in the controller ? I’ve added:

    private readonly LocService _localizer;

    public TestController( LocService localizer)
    {
    _localizer = localizer;
    }

    and in the method:

    Test = _localizer.GetLocalizedHtmlString(“Test”);

    but it not works…

    1. The method GetLocalizedHtmlString returned LocalizedString, you will do bellow:
      Test = _localizer.GetLocalizedHtmlString(“Test”).value;

  10. William Powell · · Reply

    Thanks for posting this. I was not looking forward to creating all those separates resx files. In addition, this has the added benefit of avoiding all that duplication for common strings across the entirety of the application.

    The only addition I’ve made is the following to the LocService:

    public LocalizedString this[string key] => _localizer[key];

    This allows me to keep the syntax while using this service more like IViewLocalizer.

    In a view, instead of:

    @SharedLocalizer.GetLocalizedHtmlString(“register”);

    We can use:
    @SharedLocalizer[“register”];

    1. thanks good idea
      Greetings Damien

  11. Paulo Sérgio · · Reply

    HI Damien, Can we use this solution also for Razor Pages? Thanks in advance

  12. Paulo Sérgio · · Reply

    Hi, I saw View and DataAnottation examples, any controller localization example?
    Thanks

  13. Kevin A. Alviola · · Reply

    This is very awesome blog, thanks for sharing

  14. Carl Bussema · · Reply

    Can we mix and match this? Use a shared file for very generic things “OK” “Continue” “Error” but also use the file-per-class / file-per-view pattern using native Localization? The shared file gets a little hard to find things in sometimes, because it relies on knowing what the previous developer called the string you think you’re looking for.

  15. Aya Saad · · Reply

    How can change my language from english to arabic ?
    I wrote this function in controller and i pass it culture and returnUrl to change my language to arabic but not work and default language still english not arabic , Can help me?

    public IActionResult SetLanguage(string culture, string returnUrl)
    {
    Response.Cookies.Append(
    CookieRequestCultureProvider.DefaultCookieName,
    CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
    new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return LocalRedirect(returnUrl);
    }

  16. Do you have the sample project for asp net core 2.2?

    Because some Namespac/class are not included in Net core 2.2.

  17. […] have an ASP.NET web api in ASP.NET core 2.1 and I have implemented a shared resource as explained here. This works […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.