Definir cultura em um aplicativo ASP.Net MVC

85

Qual é o melhor lugar para definir a cultura / cultura da interface do usuário em um aplicativo ASP.net MVC

Atualmente, tenho uma classe CultureController que se parece com isto:

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

e um hiperlink para cada idioma na página inicial com um link como este:

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

que funciona bem, mas estou pensando que existe uma maneira mais apropriada de fazer isso.

Estou lendo a Cultura usando o seguinte ActionFilter http://www.iansuttle.com/blog/post/ASPNET-MVC-Action-Filter-for-Localized-Sites.aspx . Eu sou um pouco um novato MVC, então não tenho certeza se estou definindo isso no lugar correto. Eu não quero fazer isso no nível do web.config, tem que ser baseado na escolha do usuário. Eu também não quero verificar seus cabeçalhos http para obter a cultura de suas configurações de navegador.

Editar:

Só para deixar claro - não estou tentando decidir se devo usar a sessão ou não. Estou feliz com essa parte. O que estou tentando descobrir é se é melhor fazer isso em um controlador de cultura que tem um método de ação para cada cultura a ser definida, ou se há um lugar melhor no pipeline MVC para fazer isso?

ChrisCa
fonte
Usar o estado da sessão para selecionar a cultura do usuário não é uma boa escolha. A melhor maneira é incluir a cultura como parte da URL , o que torna fácil "trocar" a página atual por outra cultura.
NightOwl888

Respostas:

114

Estou usando este método de localização e adicionei um parâmetro de rota que define a cultura e o idioma sempre que um usuário visita example.com/xx-xx/

Exemplo:

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

Eu tenho um filtro que faz a configuração 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 ativar o atributo Internacionalização, basta adicioná-lo à sua classe:

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

Agora, sempre que um visitante vai para http://example.com/de-DE/Home/Index, o site alemão é exibido.

Espero que essas respostas apontem na direção certa.

Eu também fiz um pequeno projeto de exemplo MVC 5 que você pode encontrar aqui

Basta ir para http: // {yourhost}: {port} / en-us / home / index para ver a data atual em inglês (EUA) ou alterá-la para http: // {yourhost}: {port} / de -de / home / index para alemão etc.

jao
fonte
15
Também gosto de colocar o idioma no URL, porque ele se tornou rastreável por mecanismos de pesquisa em diferentes idiomas e permite ao usuário salvar ou enviar um URL com um idioma específico.
Eduardo Molteni
50
Adicionar o idioma ao url não viola o REST. De fato, adere a ele, tornando o recurso da web independente de um estado de sessão oculto.
Jace Rhea
4
O recurso da web não depende de um estado oculto, da maneira como é processado. Se você deseja acessar o recurso como um serviço da web, você precisará escolher um idioma para fazê-lo.
Dave Van den Eynde
4
Tive alguns problemas com este tipo de solução. As mensagens de erro de validação não estavam sendo traduzidas. Para resolver o problema, defini a cultura na função Application_AcquireRequestState do arquivo global.asax.cs.
ADH
4
Colocar isso em um filtro NÃO é uma boa ideia. A vinculação do modelo usa CurrentCulture, mas o ActionFilter ocorre após a vinculação do modelo. É melhor fazer isso em Global.asax, Application_PreRequestHandlerExecute.
Stefan
38

Eu sei que esta é uma questão antiga, mas se você realmente gostaria de ter isso funcionando com o seu ModelBinder (em relação a DefaultModelBinder.ResourceClassKey = "MyResource";, bem como os recursos indicados nas anotações de dados das classes de viewmodel), o controlador ou mesmo um ActionFilteré tarde demais para definir a cultura .

A cultura pode ser definida Application_AcquireRequestState, por exemplo:

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

Na verdade, existe uma maneira melhor de usar um gerenciador de rotas personalizado que define a cultura de acordo com a url, perfeitamente descrito por Alex Adamyan em seu blog .

Tudo o que há a fazer é substituir o GetHttpHandlermétodo e definir a cultura lá.

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);
    }
}
marapet
fonte
Infelizmente, RouteData etc. não estão disponíveis no método "Application_AcquireRequestState", mas estão em Controller.CreateActionInvoker (). Portanto, sugiro "sobrescrever protegido IActionInvoker CreateActionInvoker ()" e definir CultureInfo ali.
Skorunka František
Eu li esse blog. Há algum problema se eu prosseguir com o cookie? Uma vez que não tenho permissão para alterá-lo. Por favor, me informe. há algum problema com essa abordagem?
kbvishnu
@VeeKeyBee Se o seu site for público, todos os idiomas não serão indexados corretamente ao usar cookies. Para sites protegidos, provavelmente você está bem.
marapet
não é público. Você pode dar uma dica sobre a palavra "indexado"?
kbvishnu
1
Você deve fazer sua própria pergunta e ler sobre SEO, isso não tem mais nada a ver com a pergunta original. webmasters.stackexchange.com/questions/3786/…
marapet
25

Eu faria isso no evento Initialize do controlador assim ...

    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
fonte
1
a string de cultura não pode ser uma const, pois o usuário precisa ser capaz de especificar a cultura que gostaria de usar no site.
NerdFury
2
Eu entendo isso, mas a questão era onde seria melhor definir a cultura e não como fazê-lo.
Jace Rhea
Em vez de const, você pode usar algo como: var newCulture = new CultureInfo (RouteData.Values ​​["lang"]. ToString ());
Nordes de
AuthorizeCore é chamado antes de OnActionExecuting, portanto, você não terá nenhum detalhe de cultura em seu método substituído AuthorizeCore. Usar o método de inicialização do controlador pode funcionar melhor, especialmente se você estiver implementando um AuthorizeAttribute customizado, já que o método Initialize é chamado antes de AuthorizeCore (você terá detalhes da cultura em AuthorizeCore).
Nathan R
7

Por se tratar de uma configuração que é armazenada por usuário, a sessão é um local adequado para armazenar as informações.

Eu mudaria seu controlador para usar a string de cultura como parâmetro, em vez de ter um método de ação diferente para cada cultura potencial. Adicionar um link à página é fácil e você não deve precisar escrever o mesmo código repetidamente sempre que uma nova cultura for necessária.

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
fonte
obrigado pela resposta, não estou tentando decidir se devo usar a sessão ou não. Estou feliz com essa parte. O que estou tentando descobrir é se é melhor fazer isso em um controlador de cultura que tem um método de ação para cada cultura a ser definida ou se há um lugar melhor no pipeline MVC para fazer isso
ChrisCa
Eu forneci uma resposta editada que se encaixa melhor à pergunta.
NerdFury
Sim, isso é certamente mais limpo, mas o que realmente quero saber é se isso deve ser feito em um controlador. Ou se há um lugar melhor no pipeline MVC para definir a cultura. Ou se for melhor em ActionFilters, Handlers, Modules etc
ChrisCa
Um manipulador e um módulo não fazem sentido porque o usuário não teve a chance de fazer uma seleção. Você precisa de uma maneira para o usuário fazer uma seleção e, em seguida, processar a seleção de usuários, que será feita em um controlador.
NerdFury
acordado, os manipuladores e os módulos são muito cedo para permitir a interação do usuário. No entanto, sou muito novo no MVC, então não tenho certeza se este é o melhor lugar no pipeline para configurá-lo. Se eu não ouvir de outra forma depois de um tempo, aceitarei sua resposta. ps essa sintaxe que você usou para passar um parâmetro para um método Action não parece funcionar. Não tem um controlador definido, então apenas usa o padrão (que não é o correto neste caso). E não parece haver outra sobrecarga adequada
ChrisCa
6

Qual é o melhor lugar é a sua pergunta. O melhor lugar é dentro do método Controller.Initialize . O MSDN escreve que ele é chamado após o construtor e antes do método de ação. Ao contrário de substituir OnActionExecuting, colocar seu código no método Initialize permite que você se beneficie de ter todas as anotações e atributos de dados personalizados em suas classes e propriedades a serem localizados.

Por exemplo, minha lógica de localização vem de uma classe que é injetada em meu controlador personalizado. Tenho acesso a este objeto porque Initialize é chamado após o construtor. Eu posso fazer a atribuição de cultura do Thread e não ter todas as mensagens de erro exibidas corretamente.

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

Mesmo que sua lógica não esteja dentro de uma classe como o exemplo que forneci, você tem acesso ao RequestContext que permite que você tenha a URL e HttpContext e o RouteData, que você pode fazer basicamente qualquer análise possível.

Patrick Desjardins
fonte
Isso funciona para meu HTML5 Telerik ReportLocalization !. Obrigado @Patrick Desjardins
CoderRoller
4

Se estiver usando Subdomínios, por exemplo, como "pt.mydomain.com" para definir o português, por exemplo, usar Application_AcquireRequestState não funcionará, porque não é chamado em solicitações de cache subsequentes.

Para resolver isso, sugiro uma implementação como esta:

  1. Adicione o parâmetro VaryByCustom ao OutPutCache assim:

    [OutputCache(Duration = 10000, VaryByCustom = "lang")]
    public ActionResult Contact()
    {
        return View("Contact");
    }
    
  2. Em global.asax.cs, obtenha a cultura do host usando uma chamada de função:

    protected void Application_AcquireRequestState(object sender, EventArgs e)
    {
        System.Threading.Thread.CurrentThread.CurrentUICulture = GetCultureFromHost();
    }
    
  3. Adicione a função 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. E, finalmente, substitua GetVaryByCustomString (...) para usar também esta função:

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

A função Application_AcquireRequestState é chamada em chamadas não armazenadas em cache, o que permite que o conteúdo seja gerado e armazenado em cache. GetVaryByCustomString é chamado em chamadas em cache para verificar se o conteúdo está disponível no cache e, neste caso, examinamos o valor do domínio do host de entrada, novamente, em vez de confiar apenas nas informações da cultura atual, que poderiam ter mudado para a nova solicitação (porque estamos usando subdomínios).

Andy
fonte
4

1: Crie um atributo personalizado e um método de substituição 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: Em App_Start, encontre FilterConfig.cs, adicione este atributo. (funciona para TODO o aplicativo)

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

É isso aí !

Se você deseja definir a cultura para cada controlador / ação em vez de todo o aplicativo, você pode usar este atributo assim:

[Culture]
public class StudentsController : Controller
{
}

Ou:

[Culture]
public ActionResult Index()
{
    return View();
}
Meng Xue
fonte
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;
        }
user2861593
fonte
3
Explique por que essa é considerada a melhor maneira.
Max Leske,