ASP.NET Core MVC Localization by URL (RouteDataRequestCultureProvider)

ASP.NET Core comes with new localization methods. There are 4 types of methods comes with standard library. But one is not working properly. You can read why it is not working in this post.

In this post I’m going to show a simple implementation of localization by URL.

Source Repo! You can find fully functional sample built on ASP.NET Core 2.0 of this post at GitHub repository irensaltali/AspNetCoreMVCLocalizationByURL

  • I need URL based localization.
  • It has to be in this form: example.com/{twoLetterLanguageName}/...
  • It has to be compatible with standard routing.

Documentation! 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.

Actually ASP.NET Core has a class for this but it is not working. So here my way of fixing that and implementation. I’ll set up for two language, Turkish and English. You may set it up for countless more languages same way.

Let’s start with Startup.cs. Modify ConfigureServices() method first.

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddLocalization(options => options.ResourcesPath = "Resources");services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("tr-TR"),
};
options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[]{ new RouteDataRequestCultureProvider{
IndexOfCulture=1,
IndexofUICulture=1
}};
});

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

Above we set path of resource files for localization, method for recognize matching files (according to suffix), and default and supported culture (languages). You don’t have LanguageRouteConstraint class and overrided RouteDataRequestCultureProvider class yet.

Now modify Configure() method.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();app.UseMvc(routes =>
{
routes.MapRoute(
name: "LocalizedDefault",
template: "{culture:culture}/{controller=Home}/{action=Index}/{id?}"
); routes.MapRoute(
name: "default",
template: "{*catchall}",
defaults: new { controller = "Home", action = "RedirectToDefaultLanguage", culture = "en" });
});
}

Above we set localization option and routing map for URL localization.

LanguageRouteConstraint class is below. You may put this into Startup.cs or anywhere you like.

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

var culture = values["culture"].ToString();
return culture == "en" || culture == "tr";
}
}

Override RouteDataRequestCultureProvider class is below. It is like ASP.NET's implementation but this one is working :)

public class RouteDataRequestCultureProvider : RequestCultureProvider
{
public int IndexOfCulture;
public int IndexofUICulture;
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
throw new ArgumentNullException(nameof(httpContext));
string culture = null;
string uiCulture = null;
var twoLetterCultureName = httpContext.Request.Path.Value.Split('/')[IndexOfCulture]?.ToString();
var twoLetterUICultureName = httpContext.Request.Path.Value.Split('/')[IndexofUICulture]?.ToString();
if (twoLetterCultureName == "tr")
culture = "tr-TR";
else if (twoLetterCultureName == "en")
culture = uiCulture = "en-US";
if (twoLetterUICultureName == "tr")
culture = "tr-TR";
else if (twoLetterUICultureName == "en")
culture = uiCulture = "en-US";
if (culture == null && uiCulture == null)
return NullProviderCultureResult;

if (culture != null && uiCulture == null)
uiCulture = culture;
if (culture == null && uiCulture != null)
culture = uiCulture;
var providerResultCulture = new ProviderCultureResult(culture, uiCulture);return Task.FromResult(providerResultCulture);
}
}

As you see above I used two letter language identifier to provide more beautiful URLs.

Finally, we are setting controller method to redirect URL without culture identifier to default culture. We set MapRoute for this in Startup.cs. This mapping routes to RedirectToDefaultLanguage action in Home controller.

private string _currentLanguage; 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; } } public ActionResult RedirectToDefaultCulture() { var culture = CurrentLanguage; if (culture != "en") culture = "en"; return RedirectToAction("Index", new { culture }); }

After applied this last action method localization is done but there are few things to do.

Add Resource Files

You can think of resource files as translation files. Resource files based Key-Value pairing. You can use any string as key but I strongly recommend to use same string as default language texts. Because these keys will be used as text in case of error in defining current language or referencing resource file.

Image for post
Image for post

You can find detailed explanation for Resourse File Naming. You can use two different types naming methods (Dot and Path). I used both of them as you see above.

Example of Localized View

@using Microsoft.AspNetCore.Mvc.Localization@inject IViewLocalizer Localizer@{
ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"]</h2>
<h3>@ViewData["Message"]</h3>
<p>@Localizer["Use this area to provide additional information."]</p>

Example of Localized Controller

public class HomeController : Controller
{
private readonly IStringLocalizer<HomeController> _localizer;

public HomeController(IStringLocalizer<HomeController> localizer)
{
_localizer = localizer;
}
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = _localizer["Your application description page."];
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = _localizer["Your contact page."];
return View();
}
}

That’s it.

I hope this post has helped you.

Originally published at irensaltali.com on February 11, 2018.

Written by

MSc. Computer Engineer. Founder of @noteducom and @image4io, co-organizer in @serverlesstr, Community Builder of @awscloud

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store