Establecer cultura en una aplicación ASP.Net MVC

85

¿Cuál es el mejor lugar para configurar la cultura / UI Culture en una aplicación ASP.net MVC?

Actualmente tengo una clase CultureController que se ve así:

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");
    }
}

y un hipervínculo para cada idioma en la página de inicio con un enlace como este:

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

que funciona bien, pero creo que hay una forma más apropiada de hacerlo.

Estoy leyendo la Cultura usando el siguiente ActionFilter http://www.iansuttle.com/blog/post/ASPNET-MVC-Action-Filter-for-Localized-Sites.aspx . Soy un poco novato de MVC, así que no estoy seguro de estar configurando esto en el lugar correcto. No quiero hacerlo a nivel web.config, tiene que basarse en la elección del usuario. Tampoco quiero verificar sus encabezados http para obtener la cultura de la configuración de su navegador.

Editar:

Para que quede claro, no estoy tratando de decidir si usar sesión o no. Estoy feliz con esa parte. Lo que estoy tratando de resolver es si es mejor hacer esto en un controlador de Cultura que tiene un método de acción para cada Cultura que se va a configurar, o ¿hay un lugar mejor en la canalización de MVC para hacer esto?

ChrisCa
fuente
Usar el estado de la sesión para seleccionar la cultura del usuario no es una buena opción. La mejor manera es incluir la cultura como parte de la URL , lo que facilita "intercambiar" la página actual con otra cultura.
NightOwl888

Respuestas:

114

Estoy usando este método de localización y agregué un parámetro de ruta que establece la cultura y el idioma cada vez que un usuario visita example.com/xx-xx/

Ejemplo:

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

Tengo un filtro que hace la configuración real de cultura / idioma:

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));

    }
}

Para activar el atributo de internacionalización, simplemente agréguelo a su clase:

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

Ahora, cada vez que un visitante accede a http://example.com/de-DE/Home/Index , se muestra el sitio en alemán.

Espero que esta respuesta le indique la dirección correcta.

También hice un pequeño proyecto de ejemplo MVC 5 que puedes encontrar aquí

Simplemente vaya a http: // {yourhost}: {port} / en-us / home / index para ver la fecha actual en inglés (EE. UU.), O cámbiela a http: // {yourhost}: {port} / de -de / home / index para etcétera alemana.

jao
fuente
15
También me gusta poner el idioma en la URL, porque los motores de búsqueda lo pueden rastrear en diferentes idiomas y permite al usuario guardar o enviar una URL con un idioma específico.
Eduardo Molteni
50
Agregar el idioma a la URL no viola REST. De hecho, se adhiere a él al hacer que el recurso web no dependa de un estado de sesión oculto.
Jace Rhea
4
El recurso web no depende de un estado oculto, la forma en que se representa. Si desea acceder al recurso como un servicio web, deberá elegir un idioma para hacerlo.
Dave Van den Eynde
4
Tuve algunos problemas con este tipo de solución. Los mensajes de error de validación no se estaban traduciendo. Para resolver el problema, configuré la cultura en la función Application_AcquireRequestState del archivo global.asax.cs.
ADH
4
Poner esto en un filtro NO es una buena idea. El enlace de modelo utiliza CurrentCulture, pero ActionFilter se produce después del enlace de modelo. Es mejor hacer esto en Global.asax, Application_PreRequestHandlerExecute.
Stefan
38

Sé que esta es una pregunta antigua, pero si realmente desea que esto funcione con su ModelBinder (con respecto a DefaultModelBinder.ResourceClassKey = "MyResource";los recursos indicados en las anotaciones de datos de las clases de viewmodel), el controlador o incluso un ActionFilteres demasiado tarde para establecer la cultura .

La cultura se podría establecer Application_AcquireRequestState, por ejemplo:

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);
    }

EDITAR

En realidad, hay una mejor manera de usar un controlador de ruta personalizado que establece la cultura de acuerdo con la URL, perfectamente descrita por Alex Adamyan en su blog .

Todo lo que hay que hacer es anular el GetHttpHandlermétodo y establecer la cultura allí.

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);
    }
}
maratón
fuente
Desafortunadamente, RouteData, etc. no están disponibles en el método "Application_AcquireRequestState" pero están en Controller.CreateActionInvoker (). Así que sugiero "anular protegido IActionInvoker CreateActionInvoker ()" y configurar CultureInfo allí mismo.
Skorunka František
Leí ese blog. ¿Hay algún problema si sigo adelante con las cookies? Ya que no tengo permiso para cambiarlo. Amablemente por favor infórmeme. ¿Hay algún problema con este enfoque?
kbvishnu
@VeeKeyBee Si su sitio es público, no todos los idiomas se indexarán correctamente al usar cookies, para los sitios protegidos probablemente esté bien.
marapet
no es público. ¿Puede darnos una pista sobre la palabra "indexado"?
kbvishnu
1
Deberías hacer tu propia pregunta y leer sobre SEO, esto ya no tiene nada que ver con la pregunta original. webmasters.stackexchange.com/questions/3786/…
marapet
25

Lo haría en el evento Initialize del controlador como este ...

    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;
    }
Jace Rhea
fuente
1
la cadena de cultura no puede ser una constante, ya que el usuario debe poder especificar la cultura que le gustaría usar en el sitio.
NerdFury
2
Lo entiendo, pero la pregunta era dónde era mejor establecer la cultura, no cómo establecerla.
Jace Rhea
En lugar de una constante, puede usar algo como: var newCulture = new CultureInfo (RouteData.Values ​​["lang"]. ToString ());
Nordes
AuthorizeCore se llama antes de OnActionExecuting, por lo que no tendrá ningún detalle cultural en el método anulado de AuthorizeCore. Usar el método de inicialización del controlador puede funcionar mejor, especialmente si está implementando un AuthorizeAttribute personalizado, ya que el método Initialize se llama antes que AuthorizeCore (tendrá detalles de la cultura dentro de AuthorizeCore).
Nathan R
7

Siendo una configuración que se almacena por usuario, la sesión es un lugar apropiado para almacenar la información.

Cambiaría su controlador para tomar la cadena de cultura como parámetro, en lugar de tener un método de acción diferente para cada cultura potencial. Agregar un enlace a la página es fácil y no debería necesitar escribir el mismo código repetidamente cada vez que se requiera una nueva cultura.

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>
NerdFury
fuente
gracias por la respuesta, no estoy tratando de decidir si usar sesión o no. Estoy feliz con esa parte. Lo que estoy tratando de resolver es si es mejor hacer esto en un controlador de Cultura que tiene un método de acción para cada Cultura que se va a configurar o si hay un lugar mejor en la canalización de MVC para hacer esto
ChrisCa
Proporcioné una respuesta editada que se ajusta mejor a la pregunta.
NerdFury
Sí, eso es ciertamente más limpio, pero lo que realmente quiero saber es si esto debería hacerse en un controlador. O si hay un lugar mejor en la canalización de MVC para configurar Culture. O si es mejor en ActionFilters, Handlers, Modules, etc.
ChrisCa
Un controlador y un módulo no tienen sentido porque el usuario no ha tenido la oportunidad de hacer una selección. Necesita una forma para que el usuario haga una selección y luego procese la selección de los usuarios, que se hará en un controlador.
NerdFury
acordado, los controladores y los módulos son demasiado tempranos para permitir la interacción del usuario. Sin embargo, soy bastante nuevo en MVC, así que no estoy seguro de si este es el mejor lugar en la tubería para configurarlo. Si no escucho lo contrario después de un tiempo, aceptaré tu respuesta. ps, esa sintaxis que ha utilizado para pasar un parámetro a un método Action no parece funcionar. No tiene un controlador definido, por lo que solo usa el predeterminado (que no es el correcto en este caso). Y no parece haber otra sobrecarga adecuada
ChrisCa
6

Cuál es el mejor lugar es tu pregunta. El mejor lugar es dentro del método Controller.Initialize . MSDN escribe que se llama después del constructor y antes del método de acción. A diferencia de anular OnActionExecuting, colocar su código en el método Initialize le permite beneficiarse de tener todas las anotaciones y atributos de datos personalizados en sus clases y en sus propiedades para ser localizadas.

Por ejemplo, mi lógica de localización proviene de una clase que se inyecta en mi controlador personalizado. Tengo acceso a este objeto ya que se llama a Initialize después del constructor. Puedo hacer la asignación de cultura del hilo y no tener todos los mensajes de error mostrados correctamente.

 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;
 }

Incluso si su lógica no está dentro de una clase como el ejemplo que proporcioné, tiene acceso al RequestContext que le permite tener la URL y HttpContext y RouteData, que puede hacer básicamente cualquier análisis posible.

Patrick Desjardins
fuente
¡Esto funciona para mi HTML5 Telerik ReportLocalization !. Gracias @Patrick Desjardins
CoderRoller
4

Si usa subdominios, por ejemplo, como "pt.mydomain.com" para establecer portugués, por ejemplo, el uso de Application_AcquireRequestState no funcionará, porque no se invoca en solicitudes de caché posteriores.

Para resolver esto, sugiero una implementación como esta:

  1. Agregue el parámetro VaryByCustom al OutPutCache de esta manera:

    [OutputCache(Duration = 10000, VaryByCustom = "lang")]
    public ActionResult Contact()
    {
        return View("Contact");
    }
    
  2. En global.asax.cs, obtenga la cultura del host mediante una llamada a la función:

    protected void Application_AcquireRequestState(object sender, EventArgs e)
    {
        System.Threading.Thread.CurrentThread.CurrentUICulture = GetCultureFromHost();
    }
    
  3. Agregue la función GetCultureFromHost a 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. Y finalmente anule GetVaryByCustomString (...) para usar también esta función:

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

La función Application_AcquireRequestState se llama en llamadas no almacenadas en caché, lo que permite que el contenido se genere y se almacene en caché. GetVaryByCustomString se llama en las llamadas almacenadas en caché para verificar si el contenido está disponible en la caché, y en este caso examinamos el valor del dominio del host entrante, nuevamente, en lugar de confiar solo en la información de la cultura actual, que podría haber cambiado para la nueva solicitud (porque estamos usando subdominios).

Andy
fuente
4

1: Cree un atributo personalizado y anule un método como este:

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: En App_Start, busque FilterConfig.cs, agregue este atributo. (esto funciona para TODA la aplicación)

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

Eso es !

Si desea definir la cultura para cada controlador / acción en lugar de toda la aplicación, puede usar este atributo de esta manera:

[Culture]
public class StudentsController : Controller
{
}

O:

[Culture]
public ActionResult Index()
{
    return View();
}
Meng Xue
fuente
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;
        }
usuario2861593
fuente
3
Explique por qué se supone que esta es la mejor manera.
Max Leske