How to Easily Manage Different Languages on a Website: a Complete Tutorial With ASP.NET MVC
As information technology becomes an ever-larger part of our lives, it is becoming ever easier to access Internet content from all over the world, and developers are increasingly creating multilingual sites to reach a wide audience. The following is a tutorial to easily and efficiently manage ASP.NET MVC web sites in multiple languages.
I use XML files as source files in order to avoid being beholden to a database. Then, I create a service that implements an interface to be able to easily change the implementation at need.
With this service, I am able to:
- Retrieve resources for building dynamic HTML controls (code behind side).
- Create HtmlHelpers providing access to these resources.
- Create attributes on models to translate label form fields.
Finally, I create an action filter to access the desired resource.
Step 1: Define XML source file(s)
I recommend creating a specific directory in "App_GlobalResources", for example "XmlResources".
For the purposes of translating the homepage of our website, let’s create the Home.xml file to differentiate the resources on this page from others. Let’s say it contains two resources:
<?xml version="1.0" encoding="utf-8" ?>
<Resources>
<Resource key="HelloWordKey">
<Language key="EN">Hello World!</Language>
<Language key="FR">Bonjour le monde!</Language>
</Resource>
<Resource key="MyNameKey">
<Language key="EN">My name is Anthony</Language>
<Language key="FR">Mon prénom est Anthony</Language>
</Resource>
<Resource key="EnterYourNationalityKey">
<Language key="EN">What's your nationality</Language>
<Language key="FR">De quelle nationalité êtes-vous?</Language>
</Resource>
</Resources>
Step 2 : Create a Specific Reader for This(ese) File(s)
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using MyApp.Tools;
namespace MyApp.Services
{
public static class ResourceXmlReader
{
//static property, public readable only
public static readonly Dictionary<string, Dictionary<string, Dictionary<string, string>>> Resources = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>();
//static constructor
static ResourceXmlReader()
{
try {
string path = System.Web.Hosting.HostingEnvironment.MapPath("~/App_GlobalResources/XmlResources/");
FolderContentBrowser content = new FolderContentBrowser(path);
LoopOnResources(content.FileNameList);
}
catch { }
}
//Browse each xml resource file on the current directory
private static void LoopOnResources(List<string> fileList)
{
fileList.Where(o => o.EndsWith(".xml")).ToList().ForEach(o => OpenAndStoreResource(o));
}
//Open, read and store into the static property xml file
private static void OpenAndStoreResource(string resourcePath)
{
try {
string fileName = Path.GetFileName(resourcePath).Split('.')[0];
XDocument doc = XDocument.Load(resourcePath);
if (null != doc) {
Dictionary<string, Dictionary<string, string>> currentResource = new Dictionary<string, Dictionary<string, string>>();
var resources = doc.Descendants("Resource").ToList();
resources.ForEach(o => currentResource.Add(o.Attribute("key").Value, getEachLanguage(o.Elements("Language"))));
//attachement des resources à une ressource nommée
Resources.Add(fileName, currentResource);
}
}
catch { }
}
//Loop on each language into the file
private static Dictionary<string, string> getEachLanguage(IEnumerable<XElement> elements)
{
Dictionary<string, string> langList = new Dictionary<string, string>();
elements.ToList().ForEach(o => langList.Add(o.Attribute("key").Value, o.Value));
return langList;
}
}
}
I use a static constructor because it will be executed just one time. I read xml files just once and store them in my static property. This is the secret of performance management system languages. I do not read the xml files for each page load.
Note that the reader developed a string of dictionaries. The data is arranged as follows: Name of dictionary xml file (one for each page), containing a dictionary, in turn containing language, itself a resource dictionary (key resources, textual value of the resource).
Step 3: Create a Service That Implements an Interface For Managing Access to Resources
using System;
using System.Collections.Generic;
namespace MyApp.Globalization
{
public interface IResourceService
{
string GetResource(string resourceName, string resourceKey);
Dictionary<string, Dictionary<string, string>> GetRessourcesByName(string resourceName);
}
}
using MyApp.Services;
using System.Collections.Generic;
using System.Globalization;
namespace MyApp.Globalization
{
public class ResourceService : IResourceService
{
public string GetResource(string resourceName, string resourceKey)
{
try {
string language = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.ToUpper();
if (ResourceXmlReader.Resources.ContainsKey(resourceName)) {
if (ResourceXmlReader.Resources[resourceName].ContainsKey(resourceKey)) {
if (ResourceXmlReader.Resources[resourceName][resourceKey].ContainsKey(language))
return ResourceXmlReader.Resources[resourceName][resourceKey][language];
else
return ResourceXmlReader.Resources[resourceName][resourceKey]["EN"];
}
else
return string.Empty;
}
else return string.Empty;
}
catch { return string.Empty; }
}
public Dictionary<string, Dictionary<string, string>> GetRessourcesByName(string resourceName)
{
try {
return ResourceXmlReader.Resources[resourceName];
}
catch { return null; }
}
}
}
We access to the right resource by using the "TwoLetterISOLanguageName" Property, but we need to define it! so here step 4 !
Step 4: Create an action filter attribute which defines the language in the current context
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace MVC.Globalization
{
public class GlobalizeFilterAttribute : ActionFilterAttribute
{
/// <summary>
/// Define language in current context
/// </summary>
/// <param name="filterContext"></param>
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Get current Http HttpContextBase context = filterContext.HttpContext;
//if sent by Url
string cultureName = context.Request.QueryString["lang"];
//Cookie test
if (string.IsNullOrEmpty(cultureName))
{
cultureName = (null != context.Request.Cookies["lang"]) ? context.Request.Cookies["lang"].Value : string.Empty;
if (string.IsNullOrEmpty(cultureName))
{
try {
//sinon langue du navigateur
cultureName = context.Request.UserLanguages.FirstOrDefault();
if (string.IsNullOrEmpty(cultureName)) cultureName = "EN";
}
catch { cultureName = "EN"; }
}
}
else
{
var langCookie = new HttpCookie("lang");
langCookie.Value = cultureName;
context.Response.Cookies.Add(langCookie);
}
// Change culture on current thread
CultureInfo culture = CultureInfo.CreateSpecificCulture(cultureName);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
//action continuation
base.OnActionExecuting(filterContext);
}
}
}
This attribute allows you to intercept a language set from a form (by Url in this example) and memorize it into a cookie.
If no language is set by a form or by a cookie, the first language set in your browser will be used. If no language is set in your browser, English will be the default language.
This attribute works both if you set the language yourself and if you use the browser language by default.
To use this attribute for each page of your website, define it as a global filter in your FilterConfig class, as follows:
using MVC.Globalization;
using System.Web;
using System.Web.Mvc;
namespace MVC
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new GlobalizeFilterAttribute());
filters.Add(new HandleErrorAttribute());
}
}
}
Now it's time to implement each use case translation functionality from service resources (IResourceService).
Step 5 : Implement usage, translation functionality cases
- HtmlHelper :
using MyApp.Globalization;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web.Mvc;
namespace MVC.Helpers
{
public static class ResourceHelper
{
private static IResourceService _resources;
public static string GetResource(this HtmlHelper helper, string resourceName, string resourceKey)
{
CheckProvider();
return _resources.GetResource(resourceName, resourceKey);
}
public static MvcHtmlString GetJSONResources(this HtmlHelper helper, string[] resourcesName)
{
CheckProvider();
string lang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.ToUpper();
TagBuilder builder = new TagBuilder("script");
builder.MergeAttribute("type", "text/javascript");
StringBuilder strBuilder = new StringBuilder();
strBuilder.AppendLine();
strBuilder.AppendLine("var MyApp = MyApp || {};");
strBuilder.AppendLine("MyApp.Resources = MyApp.Resources || {};");
strBuilder.AppendLine("MyApp.Resources =");
strBuilder.AppendLine("{");
resourcesName.ToList().ForEach(resourceName => {
var ressourceCollection = _resources.GetRessourcesByName(resourceName);
if (null != ressourceCollection && ressourceCollection.Count > 0)
{
int nbElements = ressourceCollection.Count;
int i = 1;
foreach (KeyValuePair<string, Dictionary<string, string>> item in ressourceCollection) {
string value = string.Empty;
try {
value = item.Value[lang];
}
catch {
try {
value = item.Value["EN"];
}
catch { }
}
strBuilder.AppendFormat(@"""{0}"" : ""{1}""", item.Key, value);
strBuilder.Append(",");
strBuilder.AppendLine();
i++;
}
}
});
strBuilder.Remove(strBuilder.Length - 3, 1);
strBuilder.AppendLine("}");
builder.InnerHtml = strBuilder.ToString();
return new MvcHtmlString(builder.ToString());
}
public static void RegisterProvider(IResourceService provider)
{
_resources = provider;
}
private void CheckProvider()
{
if (null == _resources)
throw new Exception("Resource provider is not set");
}
}
}
I have created two ways to do this. The first, "GetResource", allows you to call the resource you want to display in html. The second, "GetJSONResources", allows you to serialize the complete resource into a Json object in order to use resources with Javascript. It takes an array of string parameters because you can serialize several resources (defined as “Dictionary” in the resource container described at the beginning of this article).
As this helper requires an IResourceService instance, you must register an instance when the application starts, as follows:
using MVC.Helpers;
using MyApp.Globalization;
using System.Web.Mvc;
using System.Web.Routing;
namespace MVC
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
IResourceService r = new ResourceService();
ResourceHelper.RegisterProvider(r);
CustomDisplayNameAttribute.RegisterProvider(r);
}
}
}
- Attribute on Models (for managing Model labels into a html form) :
using MyApp.Globalization;
using System.ComponentModel;
namespace MVC
{
public class CustomDisplayNameAttribute : DisplayNameAttribute
{
private static IResourceService _resourceService;
private string _resourceName;
private string _resourceKey;
public CustomDisplayNameAttribute(string resourceName, string resourceKey)
{
_resourceName = resourceName;
_resourceKey = resourceKey;
}
public override string DisplayName
{
get
{
CheckProvider();
return _resourceService.GetResource(_resourceName, _resourceKey);
}
}
public static void RegisterProvider(IResourceService provider)
{
_resources = provider;
}
private void CheckProvider()
{
if (null == _resourceService)
throw new Exception("Resource provider is not set");
}
}
namespace MVC.Models
{
public class TestModel
{
[CustomDisplayName("Home", "EnterYourNationalityKey")]
public string Name { get; set; }
}
}
Like the previous HtmlHelper you need to register also an IResourceService instance
- Using directly the IResourceService into a MVC Controller :
using MyApp.Globalization;
using System.Web.Mvc;
namespace MVC.Controllers
{
public class HomeController : Controller
{
private IResourceService _resourceService;
public HomeController() : this(new ResourceService()) { }
public HomeController(IResourceService resourceService)
{
_resourceService = resourceService;
}
// GET: /Index/
public ActionResult Index()
{
ViewData["HelloWorld"] = _resourceService.GetResource("Home", "HelloWordKey");
return View();
}
}
}
I recommend you use Injection dependency. I will not describe it in this article, but I have "prepared" this controller in order to use this pattern with this constructor "public HomeController(IResourceService resourceService)"
Step 6: Test Tools in an HTML Page
@using MVC.Helpers
@model MVC.Models.TestModel
@{ ViewBag.Title = "Index"; }
<h2>@Html.Raw(ViewData["HelloWorld"])</h2>
<h3>@Html.GetResource("Home", "MyNameKey")</h3>
<br /> @Html.LabelFor(m=> m.Name)
<br /> @Html.TextBoxFor(m=> m.Name)
@Html.GetJSONResources(new string[] { "Home" })
<script type="text/javascript"> alert(MyApp.Resources.HelloWordKey + "\n" + MyApp.Resources.MyNameKey); </script>
As you can see, there is a sample of each implemented tool:
- @Html.GetResource("Home", "MyNameKey") as simple HtmlHelper in order to access to a specific asked resource
- @Html.Raw(ViewData["HelloWorld"]) as ViewData setted into the MVC Controller by accessing directly to IResourceService (_resourceService.GetResource("Home", "HelloWordKey");)
- @Html.GetJSONResources(new string[] { "Home" }) as HtmlHelper wich serialize a resource into a JSON object
- @Html.LabelFor(m=> m.Name) as a translated Model label
And now for the result:
Sample 1: French language as default language on browser
Source code :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyApp</title>
</head>
<body>
<h2>Bonjour le monde!</h2>
<h3>Mon prénom est Anthony</h3>
<br />
<label for="Name">De quelle nationalité êtes-vous?</label>
<br />
<input id="Name" name="Name" type="text" value="" />
<script type="text/javascript">
var MyApp = MyApp || {};
MyApp.Resources = MyApp.Resources || {};
MyApp.Resources = { "HelloWordKey" : "Bonjour le monde!",
"MyNameKey" : "Mon prénom est Anthony",
"EnterYourNationalityKey" : "De quelle nationalité êtes-vous?"
}
</script>
<script type="text/javascript">
alert(MyApp.Resources.HelloWordKey + "\n" + MyApp.Resources.MyNameKey);
</script> </body> </html>
Sample 2: German language as default language on browser (as German is not managed, it will be managed in English by default)
Source code :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyApp</title>
</head>
<body>
<h2>Hello World!</h2>
<h3>My name is Anthony</h3>
<br />
<label for="Name">What's your nationality</label>
<br />
<input id="Name" name="Name" type="text" value="" />
<script type="text/javascript">
var MyApp = MyApp || {};
MyApp.Resources = MyApp.Resources || {};
MyApp.Resources = { "HelloWordKey" : "Hello World!",
"MyNameKey" : "My name is Anthony",
"EnterYourNationalityKey" : "What's your nationality"
}
</script>
<script type="text/javascript">
alert(MyApp.Resources.HelloWordKey + "\n" + MyApp.Resources.MyNameKey);
</script>
</body>
</html>
Sample 3 : French language as default browser language and select into a form english language (stored in cookie after selection)
Reselect french by form action :
I hope this article has helped you to easily translate your ASP.NET application ;)