Localization

mccalltd edited this page Jun 2, 2012 · 8 revisions

Note: Localization is a new feature (as of Feb, 2012), and might need additional implementaion. So if you need something not currently provided by AR regarding localization, please add an issue!

Translation Provider

To support localization, AR uses the the abstract TranslationProviderBase class. There is a default implementation — the FluentTranslationProvider, which stores translations of route url components in a dictionary. Usage is as follows (taken from the specs project):

var translations = new FluentTranslationProvider();

// Can add translations in a strongly-typed manner
translations.AddTranslations().ForController<TranslationController>()
    .AreaUrl(new Dictionary<string, string>
    {
        { "es", "es-Area" }
    })
    .RoutePrefixUrl(new Dictionary<string, string>
    {
        { "es", "es-Prefix" }
    })
    .RouteUrl(c => c.Index(), new Dictionary<string, string>
    {
        { "es", "es-Index" }
    });

// Or can add translations by refencing the keys specified by the
// TranslationKey properties on the RouteArea, RoutePrefix, and GET/POST/PUT/DELETE attributes.
translations.AddTranslations()
    .ForKey("CustomAreaKey", new Dictionary<string, string>
    {
        { "es", "es-CustomArea" }
    })
    .ForKey("CustomPrefixKey", new Dictionary<string, string>
    {
        { "es", "es-CustomPrefix" }
    })
    .ForKey("CustomRouteKey", new Dictionary<string, string>
    {
        { "es", "es-CustomIndex" }
    });

routes.MapAttributeRoutes(config =>
{
    config.AddRoutesFromController<TranslationController>();
    config.AddRoutesFromController<TranslationWithCustomKeysController>();
    config.AddTranslationProvider(translations);
});

If you don’t like the default provider, just implement your own version of TranslationProviderBase. It’s simple (here’s what the base class looks like):

public abstract class TranslationProviderBase
{
    public abstract IEnumerable<string> CultureNames { get; }

    public abstract string Translate(string key, string cultureName);
}

CultureNames returns a list of all the cultures represented in translation. Translate(key, cultureName) will return a translation for the given key and culture.

Translation Keys

AR uses a default convention for generating keys used by the translation provider, which takes into account the area name, controller name, and action name related to route component (area, prefix, or route url) being translated.

If you use the default FluentTranslationProvider, with the strongly-typed method of registering translations, then you don’t have to care very much about what these conventional keys look like. However, if you want to make the keys used by the translation provider static and decouple them from the names of your areas, controllers, and action methods, then you can set the keys manually in the attributes themselves. For example:

[RouteArea("Area", TranslationKey = "CustomAreaKey")]
[RoutePrefix("Prefix", TranslationKey = "CustomPrefixKey")]
public class TranslationWithCustomKeysController : Controller
{
    [GET("Index", TranslationKey = "CustomRouteKey")]
    public ActionResult CustomIndex()
    {
        return Content("content");
    }
}

So you have both a conventional method for adding translations and associating them with route url components, and a custom method based on overriding translation keys and/or implementing your own translation provider to pull from a database, resx, etc.

Translations and Inbound Requests

AR adds a route to the route table for each translation you add. So if you have 10 routes and translate the urls for two cultures, you will have 30 routes in your route table.

By default, the inbound request handling doesn’t care what culture the current user is associated with, so if you are browsing in Spanish, requesting the English or French urls for an action will also work. However, you can change this via the config:

routes.MapAttributeRoutes(config =>
{
    config.AddRoutesFromController<TranslateActionsController>();
    config.AddTranslationProvider(provider);
    config.ConstrainTranslatedRoutesByCurrentUICulture = true;
});

When you choose to constrain inbound requests this way, a route is considered when:

  1. no translations exist for the route;
  2. the route is translated for the current thread’s current UI culture; or,
  3. the route is translated for the current thread’s neutral culture when no translation exists for the specific culture (eg: you have translation for fr and the current UI culture is fr-FR).

If you want to use url parameters for specifying the culture (/en/home, /pt/inicio, etc), then use the config setting for CurrentUICultureResolver. Given the current HTTP context and route data, this delegate returns the culture name. By default, it returns the name of the current UI culture for the current thread.

...
config.CurrentUICultureResolver = (httpContext, routeData) =>
{
    return (string)routeData.Values["culture"]
           ?? Thread.CurrentThread.CurrentUICulture.Name;
};
...

Translations and Outbound Routes

AR will translate urls generated by the MVC framework via UrlHelper.Action() and Html.ActionLink() if a translation is available for the route. This is done by inspecting the UI culture of the current thread. In order to support this, you must set the thread’s culture yourself, using whatever means you prefer. A simple solution involves detecting the user’s preferences passed through the request context. For example:

// In global.asax
public MvcApplication()
{
    BeginRequest += OnBeginRequest;
}

protected void OnBeginRequest(object sender, System.EventArgs e)
{
    if (Request.UserLanguages != null && Request.UserLanguages.Any())
    {
        var cultureInfo = new CultureInfo(Request.UserLanguages[0]);
        Thread.CurrentThread.CurrentUICulture = cultureInfo;
    }
}