Set Culture in an ASP.Net MVC app
Asked Answered
S

8

92

What is the best place to set the Culture/UI Culture in an ASP.net MVC app

Currently I have a CultureController class which looks like this:

public class CultureController : Controller
{
    public ActionResult SetSpanishCulture()
    {
        HttpContext.Session["culture"] = "es-ES";
        return RedirectToAction("Index", "Home");
    }

    public ActionResult SetFrenchCulture()
    {
        HttpContext.Session["culture"] = "fr-FR";
        return RedirectToAction("Index", "Home");
    }
}

and a hyperlink for each language on the homepage with a link such as this:

<li><%= Html.ActionLink("French", "SetFrenchCulture", "Culture")%></li>
<li><%= Html.ActionLink("Spanish", "SetSpanishCulture", "Culture")%></li>

which works fine but I am thinking there is a more appropriate way to do this.

I am reading the Culture using the following ActionFilter http://www.iansuttle.com/blog/post/ASPNET-MVC-Action-Filter-for-Localized-Sites.aspx. I am a bit of an MVC noob so am not confident I am setting this in the correct place. I don't want to do it at the web.config level, it has to be based on a user's choice. I also don't want to check their http-headers to get the culture from their browser settings.

Edit:

Just to be clear - I am not trying to decide whether to use session or not. I am happy with that bit. What I am trying to work out is if it is best to do this in a Culture controller that has an action method for each Culture to be set, or is there is a better place in the MVC pipeline to do this?

Sport answered 13/10, 2009 at 15:0 Comment(1)
Using session state to select user culture is not a good choice. The best way is to include the culture as part of the URL, which makes it easy to "swap" the current page with another culture.Cadel
K
123

I'm using this localization method and added a route parameter that sets the culture and language whenever a user visits example.com/xx-xx/

Example:

routes.MapRoute("DefaultLocalized",
            "{language}-{culture}/{controller}/{action}/{id}",
            new
            {
                controller = "Home",
                action = "Index",
                id = "",
                language = "nl",
                culture = "NL"
            });

I have a filter that does the actual culture/language setting:

using System.Globalization;
using System.Threading;
using System.Web.Mvc;

public class InternationalizationAttribute : ActionFilterAttribute {

    public override void OnActionExecuting(ActionExecutingContext filterContext) {

        string language = (string)filterContext.RouteData.Values["language"] ?? "nl";
        string culture = (string)filterContext.RouteData.Values["culture"] ?? "NL";

        Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(string.Format("{0}-{1}", language, culture));
        Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(string.Format("{0}-{1}", language, culture));

    }
}

To activate the Internationalization attribute, simply add it to your class:

[Internationalization]
public class HomeController : Controller {
...

Now whenever a visitor goes to http://example.com/de-DE/Home/Index the German site is displayed.

I hope this answers points you in the right direction.

I also made a small MVC 5 example project which you can find here

Just go to http://{yourhost}:{port}/en-us/home/index to see the current date in English (US), or change it to http://{yourhost}:{port}/de-de/home/index for German etcetera.

Kuster answered 13/10, 2009 at 17:2 Comment(19)
In my opinion, using the URL to specify the language is the worst thing you can do. It totally violates REST and you're getting yourself in trouble if you ever want to add AJAX.Stalinism
I also likes to put the lang in the URL, because it became crawlable by search engines in different languages and allow the user to save or send a URL with a specific lang.Unisexual
Adding the language to the url does not violate REST. It fact it adheres to it by making the web resource not dependent on a hidden session state.Bethany
The web resource is not dependent on a hidden state, the way it's rendered is. If you want to access the resource as a web service, you'll need to choose a language to do it in.Stalinism
@Dave: If the web resource is actually localizable, why shouldn't it be mandatory to choose a supported language for the resource? You can just have a route without culture for non-localizable resources (but since this question is about setting the localization, that seems a bit irrelevant here)?Alyssaalyssum
This post is a couple of years old, but I am interested in implementing @jao's solution. I am using MVC5. I've included the code he provided, but the InternationalizationAttribute routine doesn't seem to ever get called. Can someone provide me a hint on how to activate this filter? I visited the link provided, but that solution is dated 2008 and besides, I don't understand what is being required. :SRheumatoid
@Rheumatoid I created a test project in MVC 5 which you can find here: app.box.com/s/ls0j5zns3vlzp3yo0zey I'll update my answer with some more infoKuster
Very appreciated. Thx. Going off of the test project I integrated to my project. Set a breakpoint in the InternationalizationAttribute class and see it works. /pt-BR/ is detected and CurrentCulture/CurrentUICulture are set correctly. In my _Layout.cshtml I am outputting CurrentCulture and CurrentUICulture, but they both say en-US no matter what. Would you know and reason why pt-BR doesn't take hold?Rheumatoid
@jao, I do see why it didn't take hold. I didn't set the [Internationalization] attribute on the particular controller I was looking at. Works great! One question, if it's simple enough to advise, I have a dropdown list that allows selection of the preferred language. What would be the best way to give that dropdown selection control of the language? Do I need to set my action to redirect to /pt-BR/ et al? I see that once /pt-BR/ is removed from the URL it goes back to en-US. Is there no way of eliminating the requirement in the URL and still keeping the selected language/culture?Rheumatoid
I think you could always set a default culture like you always did (web.config, see: msdn.microsoft.com/en-us/library/vstudio/…)Kuster
Your solution works, plain and simple, so long as the language is specified in the URL. Hate to beat this to death, but I have some specific needs in order to get this to work with my project requirements. I created a new question here on SO. Do you have any suggestions?Rheumatoid
REST issues aside, this method of setting the culture happens too late in the pipeline for various things to use the culture. A key one for example would be ModelBinders, you will find something like 10,00 for a decimal would fail to bind to your model because it cannot parse it. I've found setting it in the Initialisation method of your controller works the best to cover most instances.Chesna
I had some issues with this type of solution. The validation error messages were not being translated. To solve the problem I set the culture in the global.asax.cs file's Application_AcquireRequestState function.Hegelianism
Putting this in a filter is NOT a good idea. Model binding makes use of the CurrentCulture, but the ActionFilter occurs after model binding. It's better to do this in Global.asax, Application_PreRequestHandlerExecute.Mccarthyism
I used this method and getting error when the route data is not specified. Error : HTTP Error 403.14 - Forbidden The Web server is configured to not list the contents of this directory. Solution is to change route config as "language}/{culture}/{controller}/{action}"Georgiana
As @Mccarthyism wrote, I've down voted this because this solution resulted in errors in my project when model was not bound correctly (invalid date format)Penney
How to create @Html.ActionLink for this ?Rail
@Muflix: See this answer: https://mcmap.net/q/108924/-html-actionlink-method. In case you would create an actionlink to a English/US page, you pass the following in the Route Arguments part: Language="en", Culture = "US"Kuster
This is just brilliantShaylynn
A
39

I know this is an old question, but if you really would like to have this working with your ModelBinder (in respect to DefaultModelBinder.ResourceClassKey = "MyResource"; as well as the resources indicated in the data annotations of the viewmodel classes), the controller or even an ActionFilter is too late to set the culture.

The culture could be set in Application_AcquireRequestState, for example:

protected void Application_AcquireRequestState(object sender, EventArgs e)
    {
        // For example a cookie, but better extract it from the url
        string culture = HttpContext.Current.Request.Cookies["culture"].Value;

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

EDIT

Actually there is a better way using a custom routehandler which sets the culture according to the url, perfectly described by Alex Adamyan on his blog.

All there is to do is to override the GetHttpHandler method and set the culture there.

public class MultiCultureMvcRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        // get culture from route data
        var culture = requestContext.RouteData.Values["culture"].ToString();
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}
Add answered 22/7, 2011 at 10:29 Comment(6)
Unfortunatelly RouteData etc. are not available in "Application_AcquireRequestState" method but they are in Controller.CreateActionInvoker(). So i suggest to "protected override IActionInvoker CreateActionInvoker()" and set CultureInfo right there.Impeller
I read that blog. Is there any issue if I go ahead with cookie ? Since I don't have permission to change it. Kindly please inform me. is there any issue with this approach ?Adulation
@VeeKeyBee If your site is public, all languages won't get indexed properly when using cookies, for protected sites you're probably fine.Add
not its not public. Can you please give a hint about the word "indexed" ?Adulation
You should ask your own question and read up on SEO, this has nothing to do anymore with the original question. webmasters.stackexchange.com/questions/3786/…Add
@SkorunkaFrantišek Even better is using a custom routehandler as described in the linked article - RouteData is available.Add
B
25

I would do it in the Initialize event of the controller like this...

    protected override void Initialize(System.Web.Routing.RequestContext requestContext)
    {
        base.Initialize(requestContext);

        const string culture = "en-US";
        CultureInfo ci = CultureInfo.GetCultureInfo(culture);

        Thread.CurrentThread.CurrentCulture = ci;
        Thread.CurrentThread.CurrentUICulture = ci;
    }
Bethany answered 13/10, 2009 at 16:44 Comment(4)
the culture string can't be a const, as the user needs to be able to specify the culture they would like to use on the site.Ordinance
I understand that, but the question was where it was best to set the culture not how to set it.Bethany
Instead of a const, you can use something like: var newCulture = new CultureInfo(RouteData.Values["lang"].ToString());Aubade
AuthorizeCore is called before OnActionExecuting, so you will not have any culture details in your AuthorizeCore overridden method. Using the controller's initialize method may work better, especially if you are implementing a custom AuthorizeAttribute, since the Initialize method is called before AuthorizeCore (you will have culture details within AuthorizeCore).Frontier
O
7

Being as it is a setting that is stored per-user, the session is an appropriate place to store the informtion.

I would change your controller to take the culture string as a parameter, rather than having a different action method for each potential culture. Adding a link to the page is easy, and you shouldn't need to write the same code repeatedly any time a new culture is required.

public class CultureController : Controller    
{
        public ActionResult SetCulture(string culture)
        {
            HttpContext.Session["culture"] = culture
            return RedirectToAction("Index", "Home");
        }        
}

<li><%= Html.ActionLink("French", "SetCulture", new {controller = "Culture", culture = "fr-FR"})%></li>
<li><%= Html.ActionLink("Spanish", "SetCulture", new {controller = "Culture", culture = "es-ES"})%></li>
Ordinance answered 13/10, 2009 at 15:5 Comment(6)
thanks for the answer, I am not trying to deciede whether to use session or not. I am happy with that bit. What I am trying to work out is if it is best to do this in a Culture controller that has an action method for each Culture to be set Or is there is a better place in the MVC pipeline to do thisSport
I have provided an edited answer that better fits the question.Ordinance
Yes, that is certainly cleaner, but what I really want to know is if this should be done in a Controller at all. Or if there is a better place in the MVC pipeline to set Culture. Or if it is better in an ActionFilters, Handlers, Modules etcSport
A handler and module don't make sense because the user hasn't had a chance to make a selection. You need a way for the user to make a selection, and then process the users selection, that will be done in a controller.Ordinance
agreed, handlers and modules are to early to allow user interaction. However, I am quite new to MVC so am unsure if this is the best place in the pipeline to set it. If I don't hear otherwise after a while I'll accept your answer. p.s. that syntax you have used to pass a parameter to an Action method doesnt seem to work. It doesnt have a controller defined so just uses the default one (which is not the correct one in this case). And there doesnt appear to be another overload suitableSport
I made the change above regarding the ActionLink. You can specify the correct controller in the anonymous class.Ordinance
S
6

What is the best place is your question. The best place is inside the Controller.Initialize method. MSDN writes that it is called after the constructor and before the action method. In contrary of overriding OnActionExecuting, placing your code in the Initialize method allow you to benefit of having all custom data annotation and attribute on your classes and on your properties to be localized.

For example, my localization logic come from an class that is injected to my custom controller. I have access to this object since Initialize is called after the constructor. I can do the Thread's culture assignation and not having every error message displayed correctly.

 public BaseController(IRunningContext runningContext){/*...*/}

 protected override void Initialize(RequestContext requestContext)
 {
     base.Initialize(requestContext);
     var culture = runningContext.GetCulture();
     Thread.CurrentThread.CurrentUICulture = culture;
     Thread.CurrentThread.CurrentCulture = culture;
 }

Even if your logic is not inside a class like the example I provided, you have access to the RequestContext which allow you to have the URL and HttpContext and the RouteData which you can do basically any parsing possible.

Sapwood answered 9/4, 2015 at 1:36 Comment(1)
This works for my HTML5 Telerik ReportLocalization!. Thanks @Patrick DesjardinsStott
I
4

If using Subdomains, for example like "pt.mydomain.com" to set portuguese for example, using Application_AcquireRequestState won't work, because it's not called on subsequent cache requests.

To solve this, I suggest an implementation like this:

  1. Add the VaryByCustom parameter to the OutPutCache like this:

    [OutputCache(Duration = 10000, VaryByCustom = "lang")]
    public ActionResult Contact()
    {
        return View("Contact");
    }
    
  2. In global.asax.cs, get the culture from the host using a function call:

    protected void Application_AcquireRequestState(object sender, EventArgs e)
    {
        System.Threading.Thread.CurrentThread.CurrentUICulture = GetCultureFromHost();
    }
    
  3. Add the GetCultureFromHost function to global.asax.cs:

    private CultureInfo GetCultureFromHost()
    {
        CultureInfo ci = new CultureInfo("en-US"); // en-US
        string host = Request.Url.Host.ToLower();
        if (host.Equals("mydomain.com"))
        {
            ci = new CultureInfo("en-US");
        }
        else if (host.StartsWith("pt."))
        {
            ci = new CultureInfo("pt");
        }
        else if (host.StartsWith("de."))
        {
            ci = new CultureInfo("de");
        }
        else if (host.StartsWith("da."))
        {
            ci = new CultureInfo("da");
        }
    
        return ci;
    }
    
  4. And finally override the GetVaryByCustomString(...) to also use this function:

    public override string GetVaryByCustomString(HttpContext context, string value)
    {
        if (value.ToLower() == "lang")
        {
            CultureInfo ci = GetCultureFromHost();
            return ci.Name;
        }
        return base.GetVaryByCustomString(context, value);
    }
    

The function Application_AcquireRequestState is called on non-cached calls, which allows the content to get generated and cached. GetVaryByCustomString is called on cached calls to check if the content is available in cache, and in this case we examine the incoming host domain value, again, instead of relying on just the current culture info, which could have changed for the new request (because we are using subdomains).

Irradiate answered 23/1, 2013 at 4:31 Comment(0)
A
4

1: Create a custom attribute and override method like this:

public class CultureAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
    // Retreive culture from GET
    string currentCulture = filterContext.HttpContext.Request.QueryString["culture"];

    // Also, you can retreive culture from Cookie like this :
    //string currentCulture = filterContext.HttpContext.Request.Cookies["cookie"].Value;

    // Set culture
    Thread.CurrentThread.CurrentCulture = new CultureInfo(currentCulture);
    Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(currentCulture);
    }
}

2: In App_Start, find FilterConfig.cs, add this attribute. (this works for WHOLE application)

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
    // Add custom attribute here
    filters.Add(new CultureAttribute());
    }
}    

That's it !

If you want to define culture for each controller/action in stead of whole application, you can use this attribute like this:

[Culture]
public class StudentsController : Controller
{
}

Or:

[Culture]
public ActionResult Index()
{
    return View();
}
Azotic answered 22/11, 2016 at 19:36 Comment(0)
E
0
protected void Application_AcquireRequestState(object sender, EventArgs e)
        {
            if(Context.Session!= null)
            Thread.CurrentThread.CurrentCulture =
                    Thread.CurrentThread.CurrentUICulture = (Context.Session["culture"] ?? (Context.Session["culture"] = new CultureInfo("pt-BR"))) as CultureInfo;
        }
Evolve answered 9/10, 2013 at 6:56 Comment(1)
Please explain why this is supposed to be the best way.Marc

© 2022 - 2024 — McMap. All rights reserved.