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 Reply