AJ's blog

November 30, 2013

ASP.NET MVC I18n – Part 3: Custom Language Choice

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 5:49 pm

The last post showed how we could honor the language preferences the user declared in his browser. However, it is also good practice to let the user change the region, independent of the browser settings.

Note: This post is part of a series

To achieve this, we need:

  • a way to maintain the user’s culture choice overriding the browser preferences
  • a way to actually change the preferred culture

 

Maintaining the preferred culture… 

As we know, each request comes with the accept-language header, telling us language preferences set in the browser. We need something similar to maintain the overriding culture choice.

One option, that is used very often used, is including the region in the URL. This implies addressing the culture in every route declaration, and every action link needs to provide the culture as additional parameter.  Alex shows how the URLs can be analyzed and the culture applied using a custom MvcRouteHandler; Nadeem does it in a Controller base class with similar intentions. Something in the same line should be possible for generating URLs (e.g. with a custom route class).

This certainly works and there is nothing wrong with it. Still, I don’t like this approach all that much. From a technical perspective, URLs in static content like CSS files are still not addressed properly. Additionally there are also other MVC features affecting URLs as well, e.g. areas, which might cause problems. And from a more “semantic” perspective, URLs are meant to address "resources" – which is a hard enough task already. Language preferences, similarly to skin and other profile data, is kind of "orthogonal" information affecting the rendering, but not addressing the resource. Unfortunately MVC does not really give an answer on how to pass that kind of information, which is why you can find all possible approaches (URL parts, query params, cookies, user session, …), none of which is always good or always bad.

For the problem at hand, the preferred culture, my choice is using a cookie: Adjusting the URL feels too invasive, query params are too error prone, and no need to enforce a session.

 

Setting the culture in a cookie is quite simple:

const string CookieName = "PreferredCulture";

public static void SetPreferredCulture(this HttpResponseBase response, string cultureName)
{
    SetPreferredCulture(response.Cookies, cultureName);
}

static void SetPreferredCulture(HttpCookieCollection cookies, string cultureName)
{
    var cookie = new HttpCookie(CookieName, cultureName);
    cookie.Expires = DateTime.Now.AddDays(30);
    cookies.Set(cookie);
    Debug.WriteLine("SetPreferredCulture: " + cultureName);
}

Reading likewise:

static CultureInfo GetPreferredCulture(HttpCookieCollection cookies)
{
    var cookie = cookies[CookieName];
    if (cookie == null)
        return null;
    var culture = GetCultureInfo((string)cookie.Value);
    if (culture == null)
        return null;
    if (!SupportedCultures.Where(ci => ci.Name == culture.Name).Any())
        return null;
    return culture;
}

I don’t trust the cookie to contain a valid value. Call me paranoid, but somebody could have tampered with the request.

And we need to change our method to determine the current culture based on the cookie value:

public static void ApplyUserCulture(this HttpRequest request)
{
    ApplyUserCulture(request.Headers, request.Cookies);
}

static void ApplyUserCulture(NameValueCollection headers, HttpCookieCollection cookies)
{
    var culture = GetPreferredCulture(cookies)
        ?? GetUserCulture(headers)
        ?? SupportedCultures[0];
    
    var t = Thread.CurrentThread;
    t.CurrentCulture = culture;
    t.CurrentUICulture = culture;
    Debug.WriteLine("Culture: " + culture.Name);
}

 

Changing the preferred culture…

Now that we have the groundwork in place, we need to surface the feature to the user. Starting MVC-like with a controller and action to set the preferred culture:

public class CultureController : Controller
{
    //
    // GET: /SetPreferredCulture/de-DE
    public ActionResult SetPreferredCulture(string culture, string returnUrl)
    {
        this.Response.SetPreferredCulture(culture);
        if (string.IsNullOrEmpty(returnUrl))
            return RedirectToAction("Index", "Home");
        return Redirect(returnUrl);
    }
}

And the necessary route…

var route = routes.MapRoute(
    name: "SetPreferredCulture",
    url: "SetPreferredCulture/{culture}",
    defaults: new { controller = "Culture", action = "SetPreferredCulture", culture = UrlParameter.Optional }
);

For the UI it’s really up to you how you present the choice: some drop down list is quite common. I intend to provide a single action link that simply choses the next supported culture, thus repeatedly clicking it would cycle through all cultures. For this a little helper method makes life simpler:

public static void GetSwitchCultures(out CultureInfo currentCulture, out CultureInfo nextCulture)
{
    currentCulture = Thread.CurrentThread.CurrentUICulture;
    var currentIndex = Array.IndexOf(SupportedCultures.Select(ci => ci.Name).ToArray(), currentCulture.Name);
    int nextIndex = (currentIndex + 1) % SupportedCultures.Length;
    nextCulture = SupportedCultures[nextIndex];
}

Based on the work so far I can implement a _SetPreferredCulture.cshtml partial view:

@{
    // cycle through supported cultures
    System.Globalization.CultureInfo currentCulture;
    System.Globalization.CultureInfo nextCulture; 
    MyStocks.Mvc.Helper.CultureHelper.GetSwitchCultures(out currentCulture, out nextCulture);
    string currentCultureDisplayName = currentCulture.Parent.NativeName;
    string nextCultureDisplayName = nextCulture.Parent.NativeName;
    string linkText = currentCultureDisplayName + " => "+ nextCultureDisplayName; 
    string url= Url.Action("SetPreferredCulture", "Culture", new { culture = nextCulture.Name, returnUrl = Request.RawUrl });
}
<div>
    @Html.ActionLink(linkText, "SetPreferredCulture", "Culture", new { culture = nextCulture.Name, returnUrl = Request.RawUrl }, null)
</div>

Include it in the _Layout.cshtml view and my users can change their culture as they wish:

Done.

 

That’s all for now folks,
AJ.NET

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: