Setup a multilingual site using ASP.NET MVC5

A multilingual site should translate the following:

  • Date and time formatting
  • Currency
  • Text resources: lables, buttons, validation messages, tooltips

It must be easy to switch languages.
It should be relatively easy to add more languages.

An example ASP.NET MVC 5 project can be downloaded here https://github.com/mchudinov/AspMvc5Multilingual

languages_feature_v3_tcm4-833994



multilang

1. Routing

Add lang parameter to routes in RegisterRoutes method of RouteConfig class. Set constraints to enabled languages. Set default language in default route.

public class RouteConfig
{
	public static void RegisterRoutes(RouteCollection routes)
	{
		routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

		routes.MapRoute(
			name: "Language",
			url: "{lang}/{controller}/{action}/{id}",
			defaults: new { controller = "Default", action = "Index", id = UrlParameter.Optional },
			constraints: new { lang = @"ru|en" }
		);

		routes.MapRoute(
			name: "Default",
			url: "{controller}/{action}/{id}",
			defaults: new { controller = "Default", action = "Index", id = UrlParameter.Optional, lang = "en" }
		);
	}
}

2. Activate culture

Culture can be activated by two different ways: in controller’s Initialize method or by using filter attribute.

2.1 Initialize method

Every controller that needs language data must Initialize culture in it’s Initialize method. Let’s create a base controller class and override Initialize method in it:

public abstract class BaseController : Controller
{
	private string CurrentLanguageCode { get; set; }

	protected override void Initialize(RequestContext requestContext)
	{
		if (requestContext.RouteData.Values["lang"] != null && requestContext.RouteData.Values["lang"] as string != "null")
		{
			CurrentLanguageCode = (string)requestContext.RouteData.Values["lang"];
			if (CurrentLanguageCode != null)
			{
				try
				{
					Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo(CurrentLanguageCode);
				}
				catch (Exception)
				{
					throw new NotSupportedException($"Invalid language code '{CurrentLanguageCode}'.");
				}
			}
		}
		base.Initialize(requestContext);
	}
}

All the other language-dependent controllers must be inherited from this base controller class.

2.2 Language localization filter attribute

Create a LocalizationAttribute class inherited from ActionFilterAttribute:

public class LocalizationAttribute : ActionFilterAttribute
{
	private readonly string _defaultLanguage;

	public LocalizationAttribute(string defaultLanguage)
	{
		_defaultLanguage = defaultLanguage;
	}

	public override void OnActionExecuting(ActionExecutingContext filterContext)
	{
		string lang = (string)filterContext.RouteData.Values["lang"] ?? _defaultLanguage;
		if (lang != null)
		{
			try
			{
				Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo(lang);
			}
			catch (Exception)
			{
				throw new NotSupportedException($"Invalid language code '{lang}'.");
			}
		}
	}
}

 

Add FilterConfig class to App_Start folder and add created LocalizationAttribute in it.

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new LocalizationAttribute("en"), 0);
        }
    }

 

Use [Localization] attribute on every controller that needs language information.

[Localization("en")]
public class DefaultController : Controller
{
	public ActionResult Index()
	{
		return View();
	}
}

3. Create translation resources

3.1 Add App_LocalResources folder

Add Asp_Net folder App_LocalResources to project. Resource files will be placed in it.

asplocalresfolder

3.2 Add resource files

Add language resource files GlobalRes.resx for the default language (English in my case) and files for other languages like GlobalRes.ru.resx to App_LocalResources folder. Two letters in the file name must be region info ISO (country code) as defined in RegionInfo.TwoLetterISORegionName Property. The complete list of region codes can be found at the Wikipedia page ISO 3166-1 alpha-2.

addresfile

3.3 Set resource files properties

  • Resource type: String
  • Build Action: Embedded Resource
  • Custom Tool: PublicResXFileCodeGenerator
  • Access Modifier: Public

resfile

3.4 Add translations for all string resources

3.5 Add reference to resource namespace to Razor in web.config

Make sure you have added this namespace to the ~/Views/web.config file and not to the standard ~/Web.config file:

  <system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
...
        <add namespace="AspMvc5Multilingual.App_LocalResources" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

3.6 Use translated resources in code

DisplayAttribute to define translation for a model’s class name:
[Display(Name = "Name", ResourceType = typeof(GlobalRes))]

Use ErrorMessageResourceType and ErrorMessageResourceName for validation messages:
ErrorMessageResourceType = typeof(GlobalRes), ErrorMessageResourceName = "This_field_is_required")

public class Widget
{
	[Required(ErrorMessageResourceType = typeof(GlobalRes), ErrorMessageResourceName = "This_field_is_required")]
	[StringLength(50, MinimumLength = 5, ErrorMessageResourceType = typeof(GlobalRes), ErrorMessageResourceName = "Must_be_at_least_5_charachters")]
	[RegularExpression(@"^[A-Za-z0-9_]+$", ErrorMessageResourceType = typeof(GlobalRes), ErrorMessageResourceName = "Must_contain_only_letters")]
        [Display(Name = "Name", ResourceType = typeof(GlobalRes))]
	public string Name { get; set; }
}

Use GlobalRes.resname references in Razor files:

@model AspMvc5Multilingual.Models.Widget

@Html.ActionLink(GlobalRes.MainMenu, "Index", "Default", null, new { title = GlobalRes.Tooltip_help })

<br/>

    @GlobalRes.Money: @($"{Model.Money:c0}") <br/>
    @GlobalRes.DateAndTime: @Model.DateTime.ToString("F") <br/>

<br/>

@using (Html.BeginForm())
{
    @Html.ValidationSummary(false, "")
    <div>
        @Html.LabelFor(model => model.Name, GlobalRes.Label_text)
        @Html.EditorFor(model => model.Name)
        @Html.ValidationMessageFor(model => model.Name, "")
    </div>
    <input type="submit" value="@GlobalRes.Submit_and_test_error_messages" />
}

 

4. Switch between languages

Use the following helper class that produces language links based on routing. It creates UrlHelper extension method.

public static class LanguageHelper
{
	public static MvcHtmlString LangSwitcher(this UrlHelper url, string Name, RouteData routeData, string lang)
	{
		var liTagBuilder = new TagBuilder("li");
		var aTagBuilder = new TagBuilder("a");
		var routeValueDictionary = new RouteValueDictionary(routeData.Values);
		if (routeValueDictionary.ContainsKey("lang"))
		{
			if (routeData.Values["lang"] as string == lang)
			{
				liTagBuilder.AddCssClass("active");
			}
			else
			{
				routeValueDictionary["lang"] = lang;
			}
		}
		aTagBuilder.MergeAttribute("href", url.RouteUrl(routeValueDictionary));
		aTagBuilder.SetInnerText(Name);
		liTagBuilder.InnerHtml = aTagBuilder.ToString();
		return new MvcHtmlString(liTagBuilder.ToString());
	}
}

Use language links in for instance _Layout.cshtml

@using AspMvc5Multilingual.Helper
<!DOCTYPE html>
<html>
    <head>
...
    </head>
    <body>
        <ul>
            @Url.LangSwitcher("English", ViewContext.RouteData, "en")
            @Url.LangSwitcher("Russian", ViewContext.RouteData, "ru")
        </ul>
        <div>
            @RenderBody()
        </div>
    </body>
</html>

5. Format some numbers as another currency then current culture

System.Globalization.NumberFormatInfo class provides culture-specific information for formatting and parsing numeric values. It’s quite easy to use :

NumberFormatInfo nfi = new NumberFormatInfo {CurrencySymbol = "€"};
string.Format(nfi,"{0:c0}",12345);

6. Format all numbers as another currency then current culture

NumberFormatInfo nfi = new NumberFormatInfo { CurrencySymbol = "£" };
Thread.CurrentThread.CurrentUICulture.NumberFormat = Thread.CurrentThread.CurrentCulture.NumberFormat = nfi;

7. Add new language

  • Add new language code in routing map
  • Add new resource file and translate all strings in it
  • Add new language switch-link to views