Translating Angular 2 with ASP.NET MVC

Angular 2 uses templates for the html. I would like to write my templates in Razor and have them processed on the server before they get to the client side. This way I can use resource files for translations.

Note: See my other blog post ‘Translating ASP.NET MVC with resource files’ for an explanation on how to use resource files and how to set the language on the server.

The core concept

The concept is simple, on the server I create a controller which has an action for each template. And each action has the template in its corresponding View. In Angular we simply specify the URL for the action as the URL for the template.

namespace AngularApplication.Controllers
{
    public class TemplateController : Controller
    {
        public ActionResult Welcome()
        {
            return View();
        }

        public ActionResult Locale()
        {
            return View();
        }
    }
}
@using AngularApplication.Resources
<h2>@WelcomeTexts.Welcome</h2>

<p>
    @WelcomeTexts.Introduction
</p>
import { Component } from '@angular/core';

@Component({
    selector: 'angular-application',
    templateUrl: '/Template/Welcome'
})
export class AppComponent {}

Switching languages

To switch language I create an Angular component and an Angular service. The component handles the interface for the user and the service sends the chosen language back to the server. After the user selects a new language the Angular application is reloaded.

import {Component} from '@angular/core';
import {LocaleService} from './locale.service';

@Component({
    selector: 'locale-switcher',
    templateUrl: 'Template/Locale'
})

export class LocaleComponent {
    constructor(private _localeService: LocaleService) {}

    switch(language: string) {
        this._localeService.switchLanguage(language);
    }
}
@using AngularApplication.Resources
<h2>@LocaleTexts.SwitchLanguage</h2>

<ul>
	<li><a (click)='switch("en")'>English</a></li>
	<li><a (click)='switch("sv")'>Svenska</a></li>
</ul>
import {Injectable, ApplicationRef} from '@angular/core';
import {Http, Response} from '@angular/http';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class LocaleService {
    constructor(private _http: Http) {}

    switchLanguage(language: string) {
        this._http
            .post('Locale/Switch', { language: language })
            .subscribe();

        // Must reload to change language in templates
        window.location.reload();
    }
}

But wait, there’s a cache!

While the above sounds good on paper it won’t work terribly well in practice. The content of the templates depend on the language, but this isn’t reflected in the URLs. Hence any caching going on in the browser will interfere when the user changes the language. And any caching on the server will interfere when there are users with different languages.

To fix this we must add the language to the template URLs.

namespace AngularApplication.Controllers
{
    [OutputCache(Duration = 604800, VaryByParam = "language")]
    public class TemplateController : Controller
    {
        public ActionResult Welcome(string language)
        {
            return View();
        }

        public ActionResult Locale(string language)
        {
            return View();
        }
    }
}

The browser then has to use these new URLs when getting the templates. Therefore, we need to send the language to the browser. We do this by adding it to the DOM and using a bit of JavaScript to get it into a global JavaScript variable.

<div style="display:none">
    <div id="language" data-language="@ViewBag.Language"></div>
</div>
window.onload = function() {
    document.language = document
        .getElementById('language')
        .dataset['language'];
};

Lastly, we must add the language to the template URL in the component. We also have to add a type definition for document, otherwise TypeScript will complain about it being an unknown type.

import { Component } from '@angular/core';

declare var document: any;

@Component({
    selector: 'angular-application',
    templateUrl: `/Template/Welcome?language=${document.language}`
})
export class AppComponent {}
Advertisements

Translating ASP.NET MVC with resource files

Resource files are the easiest way to translate ASP.NET MVC applications into multiple languages. They are XML files containing name-value pairs for a specific language.

The resource file editor in Visual Studio

WelcomeTexts.resx
WelcomeTexts.de.resx
WelcomeTexts.sv-SE.resx
WelcomeTexts.uz-Cyrl-UZ.resx

Above are examples of resource file names. The language of the resource file is denoted by a BCP 47 language tag. When .NET looks up a resource by name it will choose the most appropriate file available (e.g. using en if en-US doesn’t exist). The last resort is to use the resource file without a language identifier. This file must exist and it must contain all resource names you want to use or .NET will not find them.

Using the resource files

When you create a new resource file in Visual Studio it will also generate a .NET class with members for each name. The class is regenerated when you rebuild the project to keep up to date with the resource file.

Note: By default Visual Studio will set the access modifier of the class to Internal. To be able to use them in your Razor files you want to change this to Public. This can be changed in the GUI, just above the editor. Or you can change the Custom Tool property from ResXFileCodeGenerator to PublicResXFileCodeGenerator manually in the Properties.

I prefer to use the postfix Texts on all my resource files to keep the generated class names from clashing with my other classes.

@using TranslatedApplication.Resources
<!DOCTYPE html>
<html>
<head>
    <title>@WelcomeTexts.Welcome</title>
</head>
<body>
<h1>@WelcomeTexts.Welcome</h1>

<p>
    @WelcomeTexts.Introduction
</p>
</body>
</html>

Setting the language

.NET chooses which resource file to use based on the language of the current thread. Specifically, it looks at the CurrentUICulture of the thread. We must set it at the start of every HTTP request.

The user should be given the option to change the language. But I also like to start with an educated guess based on the user’s language settings, which are sent to us by the browser.

To set the language I recommend creating an action filter attribute that performs the necessary steps before each controller action executes.

namespace TranslatedApplication.Infrastructure.Attributes
{
    public class LocalizeAttribute : ActionFilterAttribute, IActionFilter
    {
        private readonly string[] SupportedLanguages = {"en", "sv"};
        private const string DefaultLanguge = "en";

        void IActionFilter.OnActionExecuting(ActionExecutingContext filterContext)
        {
            string language = DefaultLanguge;

            HttpRequestBase request = filterContext.HttpContext.Request;

            HttpCookie cookie = request.Cookies["Language"];
            if(cookie != null && SupportedLanguages.Contains(cookie.Value))
            {
                language = cookie.Value;
            }
            else if(request.UserLanguages != null)
            {
                foreach(string userLang in request.UserLanguages)
                {
                    string lang = userLang;
                    if(lang.Length < 2) continue;
                    if(lang.Contains('-'))
                        lang = lang.Substring(0, lang.IndexOf('-'));
                    if(SupportedLanguages.Contains(lang))
                    {
                        language = lang;
                        break;
                    }
                }
            }

            CultureInfo culture = new CultureInfo(language);
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = culture;
        }
    }
}

To make the attribute apply to all controller actions, add it to the global filters in FilterConfig.cs. This file should be in the App_Start directory, create it if it doesn’t exist.

namespace TranslatedApplication
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new LocalizeAttribute());
        }
    }
}

Finally, make sure the following line is in the Application_Start method in Global.asax.

FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

To let the user change the language themselves, add a controller action for it. The page has to be reloaded to reflect the change.

Note: Keep in mind that since we use a cookie to remember the user’s choice we must inform the user about it under EU law.

public class LocaleController : Controller
{
    public ActionResult Switch(string language)
    {
        Response.Cookies["Language"].Value = language;
        return Redirect("/");
    }
}

My ASP.NET is full of eels

When creating a multilingual ASP.NET application there are two sane options. You can either create different pages for each language. This can be done without duplicating the server code by using views in MVC or by using the same code behind file for different pages in web forms. However, in both cases you will have to duplicate your markup.

The second option is to use resources. Commonly in the form of resource files, though there are other options. Resource files are XML files, each file contains resources for one language and each resource is identified with a name. Resource values can contain binary data such as images but for localization purposes they will typically be strings.

Resource.resx
Resource.de.resx
Resource.sv-SE.resx
Resource.uz-Cyrl-UZ.resx

Above are examples of resource file names. The language of a resource file is denoted by an RFC 1766 language identifier before the files .resx extension. When .NET looks up a resource by name it will choose the most appropriate file available (e.g. using en if en-US doesn’t exist). The last resort is to use the resource file without a language identifier. This file must exist and it must contain all resource names you want to use or .NET will not find them.

Visual Studio has good support for editing resource files but for translations you might want to use a third party program. For example Zeta Resource Editor which is free. It allows you to edit several resource files at once.

Local and global resource files

ASP.NET web forms has excellent support for resource files. It uses two types of files, local resource files are bound to a specific page while global resource files are used everywhere. The resource files are put in two folders named App_LocalResources and App_GlobalResources respectively.

<asp:Literal runat="server" meta:resourcekey="Concept" />
<asp:Literal runat="server" Text="<%$ Resources: Concept.Text %>" />
<asp:Literal runat="server" Text="<%$ Resources: Company, Slogan %>" />

You can include text from your local resource files in an ASPX page implicitly. The first line in the code above, which is taken from a page called About.aspx, will look for resources named Concept.X in the local resource file called About.aspx.resx, where X is a valid attribute for the tag. In this case I have a resource named Concept.Text which will set the Text attribute of the literal tag.

The second line does the same thing as the first but looks up the resource name explicitly. The third line gets a resource named Slogan from the global resource file named Company.resx. Global resources cannot be used implicitly.

GetLocalResourceObject("Concept.Text") as string;
GetGlobalResourceObject("Company", "Slogan") as string;
HttpContext.GetLocalResourceObject("~/About.aspx", "Concept.Text") as string;
HttpContext.GetGlobalResourceObject("Company", "Slogan") as string;

You can also use the resources in your C# code. If you are in the code behind for a page you can use either of the first two lines while the last two lines can be used from anywhere. In the latter case you have to specify which page you want if you use a local resource file.

Strongly typed resources

Unfortunately, the local/global resource system doesn’t work very well in MVC. Instead you may want to use the following system. When you create new resource files in Visual Studio it will also generate a C# class with members for each resource. The class is regenerated when you rebuild the project to keep up to date with the resource file.

Resources.Support.Contact;

If you are using strongly typed resources there is no reason to use the directories for local and global resource files, although that should be possible as well, instead I suggest using a directory called Resources for all your resource files. In this case I have put a resource file called Support.resx in the Resources directory. In the example above I retrieve the resource named Contact.

@Project.Resources.Support.Help

You use the same code to get resources into the razor views. You have to include the project name. You may want to simplify the syntax by using using or something similar.

While the strongly typed resources look easy to use there are two major drawbacks. You can’t assemble resource names dynamically in the code, which can be useful sometimes. Secondly the generated classes only work with resource files, if you want to store resources some other way it won’t work. It might be worth putting together your own classes for dealing with resources in MVC.

Setting the language

The resources are pretty useless without a way to set the language. This has to be done at the start of each HTTP request. The best way to decide which language to use is to look at what the user’s browser tells us. But we also want the user to be able to override that decision.

protected void Application_BeginRequest(object sender, EventArgs e)
{
    string[] SupportedLanguages = { "en", "sv" };
    string DefaultLanguge = "en";
    string language = DefaultLanguge;

    HttpRequest request = HttpContext.Current.Request;

    HttpCookie cookie = request.Cookies["Language"];
    if (cookie != null && SupportedLanguages.Contains(cookie.Value))
    {
        language = cookie.Value;
    }
    else if (request.UserLanguages != null)
    {
        foreach (string userLang in request.UserLanguages)
        {
            string lang = userLang;
            if (lang.Length < 2) continue;
            if (lang.Contains('-'))
                lang = lang.Substring(0, lang.IndexOf('-'));
            if (SupportedLanguages.Contains(lang))
            {
                language = lang;
                break;
            }
        }
    }

    CultureInfo culture = new CultureInfo(language);
    Thread.CurrentThread.CurrentCulture = culture;
    Thread.CurrentThread.CurrentUICulture = culture;
}

This function in Global.asax.cs is run at the start of each request (this works in both web forms and MVC). We check if we have a language cookie set. Then we check the browser’s languages. If none of those are acceptable we default to English. Finally we can set the language of the thread. The current culture determines how dates, currencies and such things are formatted. The current UI culture determines which resource files to use.

protected void SwitchToSwedish(object sender, EventArgs e)
{
    Response.Cookies["Language"].Value = "sv";
    Response.Redirect(Request.RawUrl);
}

This is the web forms code we use when the user clicks the Swedish button. We set the cookie and then tell the browser to refresh the page. This is the only way to do it since by the time we reach this function it is too late to change the language. We have to get another HTTP request to be able to do that.