5

I'm trying to implement localization with routes

I have the following:

routes.MapRoute( "DefaultLocalized", 
                 "{lang}/{controller}/{action}/{id}",
                 new { controller = "Home",
                       action = "Index",
                       id = "",
                       lang = "en" }
               );

routes.MapRoute( "Default",
                 "{controller}/{action}/{id}",
                 new { controller = "Home",
                       action = "Index",
                       id = "" }
               );

When I call my page domain/en/home/index, it works fine but when i call domain/home/index I get error 404: resource cannot be found.

Also when I'm at domain/en/home/index and I click a secured page I get redirected to domain/Account/login how can I be redirected to domain/en/Account/login?

Also when I get an application error how can I be redirected to domain/en/home/error?

The real question is how can I implement localization with language as a route parameter?

Rex M
  • 142,167
  • 33
  • 283
  • 313
freddoo
  • 6,770
  • 2
  • 29
  • 39
  • Although this is an old question, I just went through the pain of providing a complete solution for language selection in Razor, so have added a new answer below as a guide http://stackoverflow.com/questions/660872/asp-net-mvc-localization/19398879#19398879 Hope it helps :) – iCollect.it Ltd Oct 17 '13 at 17:11

4 Answers4

10

The routes will match, by default, left-to-right, so "domain/home/index" will match first to lang=domain, controller=index, action (default to index), id (default to 0/null).

To fix this, I believe you can specify a regex on the MapRoute (matching, for example, languages with exactly 2 characters) - it has changed at some point, though... (sorry, no IDE at the moment, so I can't check exactly).

From memory, it might be:

routes.MapRoute( "DefaultLocalized", 
             "{lang}/{controller}/{action}/{id}",
             new { controller = "Home",
                   action = "Index",
                   id = "",},
             new { lang = "[a-z]{2}" }
           );

Note that you probably aren't going to want every action to take a "string lang", so you should handle the "lang" part of the route either in a base-controller, or an action-filter (in either case, presumably add the info to the ViewData).

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    Just to avoid same confusion as happened to me. You don't have add lang parameter to each controller action or mess with RouteData. It is just nice to handle language in global manner. – Jakub Šturc May 19 '09 at 09:59
7

I know this is a very old question, but having just had to solve the complete set of related issues, I thought I would share my solution.

Below is a complete solution, including a few extra tricks to allow easy changing of language. It allows for specific cultures, not just specific languages (but only the language part is retained in this example).

Features include:

  • Fallback to browser locale in determining language
  • Uses cookies to retain language across visits
  • Override language with url
  • Supports changing language via link (e.g. simple menu options)

Step 1: Modify RegisterRoutes in RouteConfig

This new routing includes a constraint (as others also suggest) to ensure the language route does not grab certain standard paths. There is no need for a default language value as that is all handled by the LocalisationAttribute (see step 2).

    public static void RegisterRoutes(RouteCollection routes)
    {
        ...

        // Special localisation route mapping - expects specific language/culture code as first param
        routes.MapRoute(
            name: "Localisation",
            url: "{lang}/{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
            constraints: new { lang = @"[a-z]{2}|[a-z]{2}-[a-zA-Z]{2}" }
        );

        // Default routing
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );

    }

Step 2: Create a Localisation attribute

This will look at controller requests, before they are handled, and change the current culture based on the URL, a cookie, or the default browser culture.

// Based on: http://geekswithblogs.net/shaunxu/archive/2010/05/06/localization-in-asp.net-mvc-ndash-3-days-investigation-1-day.aspx
public class LocalisationAttribute : ActionFilterAttribute
{
    public const string LangParam = "lang";
    public const string CookieName = "mydomain.CurrentUICulture";

    // List of allowed languages in this app (to speed up check)
    private const string Cultures = "en-GB en-US de-DE fr-FR es-ES ro-RO ";

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // Try getting culture from URL first
        var culture = (string)filterContext.RouteData.Values[LangParam];

        // If not provided, or the culture does not match the list of known cultures, try cookie or browser setting
        if (string.IsNullOrEmpty(culture) || !Cultures.Contains(culture))
        {
            // load the culture info from the cookie
            var cookie = filterContext.HttpContext.Request.Cookies[CookieName];
            var langHeader = string.Empty;
            if (cookie != null)
            {
                // set the culture by the cookie content
                culture = cookie.Value;
            }
            else
            {
                // set the culture by the location if not specified - default to English for bots
                culture = filterContext.HttpContext.Request.UserLanguages == null ? "en-EN" : filterContext.HttpContext.Request.UserLanguages[0];
            }
            // set the lang value into route data
            filterContext.RouteData.Values[LangParam] = langHeader;
        }

        // Keep the part up to the "-" as the primary language
        var language = culture.Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries)[0];
        filterContext.RouteData.Values[LangParam] = language;

        // Set the language - ignore specific culture for now
        Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(language);

        // save the locale into cookie (full locale)
        HttpCookie _cookie = new HttpCookie(CookieName, culture);
        _cookie.Expires = DateTime.Now.AddYears(1);
        filterContext.HttpContext.Response.SetCookie(_cookie);

        // Pass on to normal controller processing
        base.OnActionExecuting(filterContext);
    }
}

Step 3: Apply localisation to all controllers

e.g.

[Localisation]  <<< ADD THIS TO ALL CONTROLLERS (OR A BASE CONTROLLER)
public class AccountController : Controller
{

Step 4: To change language (e.g. from a menu)

This is where it got a little tricky and required some workarounds.

Add a ChangeLanguage method to your account controller. This will strip out any existing language code from the "previous path" to allow the new language to take effect.

    // Regex to find only the language code part of the URL - language (aa) or locale (aa-AA) syntax
    static readonly Regex removeLanguage = new Regex(@"/[a-z]{2}/|/[a-z]{2}-[a-zA-Z]{2}/", RegexOptions.Compiled);

    [AllowAnonymous]
    public ActionResult ChangeLanguage(string id)
    {
        if (!string.IsNullOrEmpty(id))
        {
            // Decode the return URL and remove any language selector from it
            id = Server.UrlDecode(id);
            id = removeLanguage.Replace(id, @"/");
            return Redirect(id);
        }
        return Redirect(@"/");
    }

Step 5: Add language menu links

The menu options consist of a link with the new language specified as a route parameter.

e.g. (Razor example)

<li>@Html.ActionLink("English", "ChangeLanguage", "Account", new { lang = "en", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>
<li>@Html.ActionLink("Spanish", "ChangeLanguage", "Account", new { lang = "es", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>

The return URl is the current page, encoded so that it can become the id parameter of the URL. This means that you need to enable certain escape sequences that are otherwise refused by Razor as a potential security violation.

Note: for non-razor setups you basically want an anchor that has the new language, and the current page relative URL, in a path like: http://website.com/{language}/account/changelanguage/{existingURL}

where {language} is the new culture code and {existingURL} is a URLencoded version of the current relative page address (so that we will return to the same page, with new language selected).

Step 6: Enable certain "unsafe" characters in URLs

The required encoding of the return URL means that you will need to enable certain escape characters, in the web.config, or the existing URL parameter will cause an error.

In your web.config, find the httpRuntime tag (or add it) in <system.web> and add the following to it (basically remove the % that is in the standard version of this attribute):

  requestPathInvalidCharacters="&lt;,&gt;,&amp;,:,\,?"

In your web.config, find the <system.webserver> section and add the following inside it:

<security>
  <requestFiltering allowDoubleEscaping="true"/>
</security>
iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • Hello, very hard to find bug in this code. culture = filterContext.HttpContext.Request.UserLanguages[0]; Raises exception for bots, crawlers, seo tools etc. There should be another else if this is not available that sets language to default - simple string for example: "en-EN". – Stefan Cebulak Oct 25 '15 at 22:10
  • @StefanCebulak: Good point. I have forgotten that bots do not provide the browser language. Presumably `UserLanguages` is null? I will update my code soon. Thanks. – iCollect.it Ltd Oct 25 '15 at 22:47
  • Yes, UserLanguages is null. – Stefan Cebulak Oct 25 '15 at 23:02
  • @StefanCebulak: Added a basic null-check fallback. Let me know if you have a better alternative. Thanks – iCollect.it Ltd Oct 26 '15 at 10:05
  • I wouldn't use ActionFilter for setting the culture because ModelBinding -> Validation happens before OnActionExecuting. – maracuja-juice Oct 25 '16 at 11:31
  • @Marimba: That had had no noticable impact on our projects. Where would model binding impact the above example? Thanks – iCollect.it Ltd Oct 25 '16 at 16:40
4

Add a contraint as new {lang = "[a-z]{2}"}.

Additionally, drop the default lang = "en". If you don't, the routing will grab the language rule when you are browsing it without it. So if you are looking at domain and you select About, it would use domain/en/Home/About instead of the more simple domain/Home/About

eglasius
  • 35,831
  • 5
  • 65
  • 110
3

You could also introduce a constraint even tighter than Marc Gravell and Freddy Rios.

something like "en|de|fr|es". This would mean hardcoding the languages but usually these are few and known.

Andrei Rînea
  • 20,288
  • 17
  • 117
  • 166