ASP.NET Core: Simple localization and language based URL-s

ASP.NET Core comes with new support for localization and globalization. I had to work with one specific prototyping scenario at work and as I was able to solve some problems that also other people may face I decided to share my knowledge and experience with my readers. This blog post is short overview of simple localization that uses some interesting tweaks and framework level dependency injection.

Source alert! Full sample solution built on ASP.NET Core 2.0 for this blog post is available at GitHub repository gpeipman/AspNetCoreLocalization.

My scenario was simple:

  1. We have limited number of supported languages and the number of languages doesn’t change often
  2. Coming of new language means changes in organization and it will probably be high level decision
  3. Although et-ee is official notation for localization here people are used with ee because it is our country domain
  4. Application has small amount of translations that are held in resource files (one per language)

As “ee” is not supported culture and “et” is not very familiar to regular users here I needed a way how to hide mapping from “ee” to “et” the way that I don’t have to inject this logic to views where translations are needed.

NB! To find out more about localization and globalization in ASP.NET Core please read the official documentation about it at https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization.

Setting up localization

Localization is different compared to previous versions of ASP.NET. We need some modifications to startup class. Let’s take ConfigureServices() method first.

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization();
    services.AddMvc();


    services.Configure<
RouteOptions
>(options =>
    {
        options.ConstraintMap.Add(
"lang", typeof(LanguageRouteConstraint
));
    });

   
// ...


    services.AddSingleton<
IHttpContextAccessor, HttpContextAccessor>();
}

You don’t have LanguageRouteConstraint class yet in your code. It’s coming later. Notice how supported cultures are configured and route based culture provider is added to request culture providers collection. These are important steps to make our site to support these cultures.

Now let’s modify Configure() method of startup class.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection(
"Logging"
));
    loggerFactory.AddDebug();
         

   
if
(env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
   
else
    {
        app.UseExceptionHandler(
"/Home/Error"
);
    }

    app.UseStaticFiles();

   
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions
>>();
    app.UseRequestLocalization(options.Value);

    app.UseMvc(routes =>
    {


        routes.MapRoute(
            name:
"LocalizedDefault"
,
            template:
"{lang:lang}/{controller=Home}/{action=Index}/{id?}"

        );

        routes.MapRoute(
            name:
"default"
,
            template:
"{*catchall}"
,
            defaults:
new { controller = "Home", action = "RedirectToDefaultLanguage", lang = "et" });
    });
}

Notice how localized route is defined. lang:lang means that there is request parameter lang that is validated by element with index “lang” from contraints map. Default route calles RedirectToDefaultLanguage() method of Home controller. We will take a look at this method later.

Now let’s add language route constraint to our web application project.

public class LanguageRouteConstraint : IRouteConstraint
{
   
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection
routeDirection)
    {
       
if(!values.ContainsKey("lang"
))
        {
           
return false
;
        }

       
var lang = values["lang"
].ToString();

       
return lang == "ee" || lang == "en" || lang == "ru";
    }
}

This constraint checks if language route value is given and if it is then check is made if it has valid value. Note how I use here “ee” instead of “et”: it’s the route value from URL where I have to use “ee” instead of “et”.

Request localization pipeline

There’s one issue. Routes are defined when MVC is configured. When localization is configured there are no routes. If we configure localization later then MVC has no idea about it. To solve this puzzle we will use special pipeline class with MiddlewareFilterAttribute.

public class LocalizationPipeline
{
   
public void Configure(IApplicationBuilder
app)
    {

       
var supportedCultures = new List<CultureInfo
>
                                {
                                   
new CultureInfo("et"
),
                                   
new CultureInfo("en"
),
                                   
new CultureInfo("ru"
),
                                };

       
var options = new RequestLocalizationOptions
()
        {

            DefaultRequestCulture =
new RequestCulture(culture: "et", uiCulture: "et"
),
            SupportedCultures = supportedCultures,
            SupportedUICultures = supportedCultures
        };

        options.RequestCultureProviders =
new[] { new RouteDataRequestCultureProvider() { Options = options, RouteDataStringKey = "lang", UIRouteDataStringKey = "lang" } };

        app.UseRequestLocalization(options);
    }
}

To use pipeline class by middleware attribute we apply this attribute to controllers and view components where localization is needed.

[MiddlewareFilter(typeof(LocalizationPipeline))]
public class HomeController : Controller
{
   
// ...
}

Redirecting to language route

By default all requests to MVC that doesn’t have valid language in URL are handled by RedirectToDefaultLanguage() method of Home controller.

public ActionResult RedirectToDefaultLanguage()
{
   
var
lang = CurrentLanguage;
   
if(lang == "et"
)
    {
        lang =
"ee"
;
    }

   
return RedirectToAction("Index", new
{ lang = lang });
}

private string CurrentLanguage
{
   
get
    {
       
if(!string
.IsNullOrEmpty(_currentLanguage))
        {
           
return
_currentLanguage;
        }



       
if (string
.IsNullOrEmpty(_currentLanguage))
        {
           
var feature = HttpContext.Features.Get<IRequestCultureFeature
>();
            _currentLanguage = feature.RequestCulture.Culture.TwoLetterISOLanguageName.ToLower();
        }

       
return _currentLanguage;
    }
}

Here we have to replace “et” with “ee” to have a valid default URL. When on language route the CurrentLanguage property gives us current language from route. If it is not language route then language by culture is returned.

Building custom string localizer

As we have one resource file per language and as views are using in big part same translation strings we don’t go with resource file per view strategy. It would introduce many duplications and here we can avoid it by using just one StringLocalizer<T>. There reason why we need custom string localizer is the “ee” and “et” issue: “ee” is not known culture in .NET and we have to translate it to “et” to ask for resources.

public class CustomLocalizer : StringLocalizer<Strings>
{
   
private readonly IStringLocalizer
_internalLocalizer;

   
public CustomLocalizer(IStringLocalizerFactory factory, IHttpContextAccessor httpContextAccessor) : base
(factory)
    {
        CurrentLanguage = httpContextAccessor.HttpContext.GetRouteValue(
"lang") as string
;
       
if(string.IsNullOrEmpty(CurrentLanguage) || CurrentLanguage == "ee"
)
        {
            CurrentLanguage =
"et"
;
        }

        _internalLocalizer = WithCulture(
new CultureInfo
(CurrentLanguage));
    }

   
public override LocalizedString this[string name, params object
[] arguments]
    {
       
get
        {
           
return
_internalLocalizer[name, arguments];
        }
    }

   
public override LocalizedString this[string
name]
    {
       
get
        {
           
return
_internalLocalizer[name];
        }
    }

   
public string CurrentLanguage { get; set; }
}

Our custom localizer is actually wrapper that translates “ee” and empty language to “et”. This way we have one localizer class to injeect to views that need localization. Base class StringLocalizer<T> gets Strings as type and this is the name of resource files.

Example of localized view

Now let’s take a look at view that uses custom localizer. It’s a simple view that outputs list of articles and below articles there is link to all news list, Link text is read from resource string called “AllNews”.

@model CategoryModel
@inject CustomLocalizer
localizer

<section class="newsSection">
    <header class="sectionHeader">
        <h1>@Model.CategoryTitle</h1>
    </header>
    @Html.DisplayFor(m => m.CategoryContent, "ContentList"
)
   
<div class="sectionFooter">
        <a href="@Url.Action("Category", new { id = Model.CategoryId })" class="readMoreLink">@localizer["AllNews"]</a>
    </div
>
</
section
>

Wrapping up

ASP.NET Core comes with new localization support and it is different from the one used in previous ASP.NET applications. It was easy to create language based URL-s and also handle the special case where local people are used with “ee” as language code instead of official code “et”. We were able to achieve decent language support for application where new languages are not added often. Also we were able to keep things easy and compact. We wrote custom string localizer class to handle mapping between “et” and “ee” and we wrote just some lines of code for it. And as it turned out we also got away with simple language route constraint. Our solution is good example how flexible ASP.NET Core is on supporting both small and big scenarios of lozalization.

References

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    43 thoughts on “ASP.NET Core: Simple localization and language based URL-s

    • March 10, 2017 at 12:13 pm
      Permalink

      Thanks for reference, Andrew! Your writing seems to help me to get closer to fully dynamic language support :)

    • Pingback:Things I’ve Learnt This Week (12th March) - Steve Gordon

    • March 13, 2017 at 2:33 pm
      Permalink

      Unless I’m mistaken, this won’t work until you add a middleware to call app.UseRequestLocalization. This needs to be done by applying a MiddlewareFilterAttribute to the target controller(s).

    • March 13, 2017 at 3:05 pm
      Permalink

      The call is there in Configure() method above.

    • March 13, 2017 at 9:25 pm
      Permalink

      Got the point and made fixes. Things work way better on my prototype application now. Thanks, Ricardo! :)

    • April 18, 2017 at 11:23 am
      Permalink

      sorry but where is a _currentLanguage?
      i start copy your code but…

    • April 18, 2017 at 11:30 am
      Permalink

      can you write this article from a to z? thank you

    • April 18, 2017 at 11:41 am
      Permalink

      _currentLanguage is class level variable in controller class.

    • April 18, 2017 at 11:46 am
      Permalink

      please upload project…

    • April 18, 2017 at 11:47 am
      Permalink

      this article is what i looking for, but i can’t handle with it… if you can please upload a working project to look it, thank you

    • April 18, 2017 at 3:58 pm
      Permalink

      I have to create sample project for public space for this. I will do it but it doesn’t happen very soon.

    • September 21, 2017 at 6:41 pm
      Permalink

      Could you share the sample project. Thank you very much.

    • September 24, 2017 at 1:30 pm
      Permalink

      I encounter this exception on CustomLocalizer:
      InvalidOperationException: No service for type ‘{secret-namespace}.CustomLocalizer’ has been registered.
      And I’m not sure what can cause it.
      I’m doubting that:
      public class CustomLocalizer : StringLocalizer
      IntelliSense suggested me 2 option for the Strings class:
      Microsoft.VisualBasic and NuGet.Framework
      I choosed the Microsoft one bacause seemed the most logical reason.

      Do you have any insight? How do I register that service in the exception?
      Can you add a guide portion about configuring the resource language files?

    • October 27, 2017 at 4:27 pm
      Permalink

      poor tutorial, you just post here then left forever.

    • October 29, 2017 at 5:58 am
      Permalink

      Can you share your source code with me?
      I really appreciate it.

    • October 29, 2017 at 9:07 am
      Permalink

      I will build sample solution and publish it to GitHub. Hope to get it done for tomorrow.

    • December 7, 2017 at 6:34 am
      Permalink

      Your sample solution works great. How to use CustomLocalizer in model data annotation. I would like to have a custom error message, for example:

      [Required(ErrorMessage = “…”]
      public string Email { get; set; }.

      Thanks.

    • February 13, 2018 at 3:04 pm
      Permalink

      Thanks for the great post! I have a question – it seems that current implementation allows to localize only small pieces of the view (e.g. @localizer[“AllNews”]), but what if i want to return a completely different view for each language (Index.en-US.cshtml or Index.ru-RU.cshtml)? Do i need to deal view the view engine in this case?

    • February 13, 2018 at 3:18 pm
      Permalink

      It should be possible, yes. Try this in startup class of application:

      public void ConfigureServices(IServiceCollection services)
      {
      // …

      services
      .AddMvc()
      .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
      }

      In views folders structure things like this:

      Views
      — Home
      —- en-us
      —— Index.cshtml
      —— About.cshtml
      —- et-ee
      —— Index.cshtml
      —— About.cshtml

    • March 6, 2018 at 12:13 pm
      Permalink

      Thanks for this post. I just want to convert a MVC5 project to .NET CORE. We had custom culture resource files for each language and we map these files to the client based on URL.

      for eg. http://localhost:50468/clienta/en [en-gb-clienta]
      http://localhost:50468/clientb/en [en-gb-clientb]

      In MVC5 we do mapping in a base Controller and the Application_Start pass ‘ ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new BaseController()));’.

      I think in above example I should do this ‘CustomLocalizer’ class.
      Can you please advise me where is the best place to do mapping in .NET Core.

      Thanks.

    • October 27, 2018 at 6:11 pm
      Permalink

      Hi – great example – what about if you want to have multi-lingual url slugs? So for English you might have ~/en/Home/About but in another language – e.g. Turkish – the url slugs should be the equivalent translation e.g. ~/tr/Anasayfa/Hakkımızda – but this should still route to the Home controller >> About action.

      Thanks, Rich.

    • October 31, 2018 at 7:31 am
      Permalink

      I don’t have any ready-made solution to offer right now. One way to do it is to define route that takes all missed hits and figures out if it should render out something or return 404. I have to play with ASP.NET Core a little bit to find out how to do it.

    • January 14, 2019 at 12:33 pm
      Permalink

      What if i use Area?
      for example in Identity area of ASP.NET Core that uses Pages instead of controllers?
      anchors does not show lang in url (Identity/Account/Register).
      When i open that link without lang TagHelper shows error that currentLanguage is null.

    • February 20, 2019 at 6:14 pm
      Permalink

      I am facing the same issue Identity pages does not show lang in the url (Identity/Account/XXX where xxx could be login, Register, passwordreset etc).
      Is there a solution for this ?

    • February 20, 2019 at 6:16 pm
      Permalink

      Hi!
      I’m trying to work out something for areas and identity. For Identity things are actually sad when it’s used as Razor package. I have found no way to get any translations there. If Identity is scaffold then it’s under our control but it’s PITA to update it. Anyway I will comment here if I make some breakthrough.

    • February 20, 2019 at 9:26 pm
      Permalink

      Thanks Peipman, I appreciate it.

    • March 6, 2019 at 5:06 pm
      Permalink

      Here is a possibly nicer way to write that getter:

      private string CurrentLanguage
      {
      get
      {
      var feature = HttpContext.Features.Get();
      return feature.RequestCulture.Culture.TwoLetterISOLanguageName.ToLower();
      }
      }

    • April 29, 2019 at 8:07 am
      Permalink

      is this works in asp.net2.2 version?
      i tried it but i get this error:
      NullReferenceException: Object reference not set to an instance of an object.
      it seems localizer not finding words i view “@localizer[“myword”]”

    • May 2, 2019 at 11:34 am
      Permalink

      Hey. How to do default language to work without lang route parameter? / – default language, others with param as /ru, /it etc.

    • May 3, 2019 at 10:05 am
      Permalink

      Hi Miri,
      I’m sure it’s possible and it takes just some small tweaks. When I think further then it can be also configuration option to not use language route with default language. I will update my sample solution to .NET Core 2.2 soon and I will comment here when it’s done. Your wish will be also part of next version.

    • May 17, 2019 at 10:35 am
      Permalink

      Great post. Waiting for asp.net Core 2.2 version!

    • June 7, 2019 at 3:56 pm
      Permalink

      Gunnar, thanks a lot for this code, it heped me a lot. Just an advice to add to article:
      when you create a new application, the current version of visual studio 2019 creates a blank asp.net core 2.2 project with this setting

      services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)

      SetCompatibilityVersion completely breaks navigation, must be removed or it will not work at all.

    • June 7, 2019 at 6:15 pm
      Permalink

      Alberto, I have not yet ported this code over to .NET Core 2.2 and I cannot guarantee it works with it 100%. Once I get migration done I will write here.

    • June 9, 2019 at 5:26 pm
      Permalink

      ok thanks for the advice, hoping the my suggestion will help you with the port, then.

    • June 19, 2019 at 12:15 pm
      Permalink

      Another question, maybe it can be answered by anyone here:

      service IOptions is registered as scoped? or singleton?

      Just to know if it can be used to configure supported languages per user.

    • July 11, 2019 at 12:03 pm
      Permalink

      Hi ,

      I Have six radio button on my page : English, Hindi ,Gujarati ,Marathi,Kannad and Urdu.

      I want to when i am click on any radio button then as per click or name on radio button then same pages label is convert to selected language button.

      For Ex : if i click on Hindi then page label is converted to Hindi.

      I Want this solution in ASP.NET CORE.

      Please help me out this problem.

    Leave a Reply

    Your email address will not be published. Required fields are marked *