Ustaw kulturę w aplikacji ASP.Net MVC

85

Jakie jest najlepsze miejsce do ustawienia kultury / kultury interfejsu użytkownika w aplikacji ASP.net MVC

Obecnie mam klasę CultureController, która wygląda następująco:

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

oraz hiperłącze dla każdego języka na stronie głównej z takim linkiem:

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

co działa dobrze, ale myślę, że istnieje bardziej odpowiedni sposób, aby to zrobić.

Czytam kulturę za pomocą następującego filtra ActionFilter http://www.iansuttle.com/blog/post/ASPNET-MVC-Action-Filter-for-Localized-Sites.aspx . Jestem trochę noobem MVC, więc nie jestem pewien, czy ustawiam to we właściwym miejscu. Nie chcę tego robić na poziomie web.config, musi to być oparte na wyborze użytkownika. Nie chcę też sprawdzać ich nagłówków http, aby pobrać kulturę z ustawień przeglądarki.

Edytować:

Dla jasności - nie próbuję decydować, czy użyć sesji, czy nie. Jestem zadowolony z tego kawałka. Próbuję ustalić, czy najlepiej jest to zrobić w kontrolerze kultury, który ma ustawioną metodę akcji dla każdej kultury, czy też jest lepsze miejsce w potoku MVC, aby to zrobić?

ChrisCa
źródło
Używanie stanu sesji do wybierania kultury użytkownika nie jest dobrym wyborem. Najlepszym sposobem jest uwzględnienie kultury jako części adresu URL , co ułatwia „zamianę” bieżącej strony na inną kulturę.
NightOwl888

Odpowiedzi:

114

Używam tej metody lokalizacji i dodałem parametr trasy, który ustawia kulturę i język za każdym razem, gdy użytkownik odwiedza example.com/xx-xx/

Przykład:

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

Mam filtr, który wykonuje rzeczywiste ustawienie kultury / języka:

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

    }
}

Aby aktywować atrybut Internationalization, po prostu dodaj go do swojej klasy:

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

Teraz za każdym razem, gdy odwiedzający przechodzi do http://example.com/de-DE/Home/Index, wyświetlana jest niemiecka witryna.

Mam nadzieję, że ta odpowiedź wskazuje właściwy kierunek.

Zrobiłem również mały przykładowy projekt MVC 5, który możesz znaleźć tutaj

Po prostu przejdź do http: // {yourhost}: {port} / en-us / home / index, aby zobaczyć aktualną datę w języku angielskim (USA) lub zmień ją na http: // {yourhost}: {port} / de -de / home / index dla niemieckiego etcetera.

jao
źródło
15
Lubię też umieszczać język w adresie URL, ponieważ stał się on dostępny dla wyszukiwarek w różnych językach i pozwala użytkownikowi zapisać lub wysłać adres URL z określonym językiem.
Eduardo Molteni
50
Dodanie języka do adresu URL nie narusza REST. Faktem jest, że przylega do niego, czyniąc zasób sieciowy niezależnym od stanu sesji ukrytej.
Jace Rhea
4
Zasób sieciowy nie jest zależny od stanu ukrytego, ani sposobu, w jaki jest renderowany. Jeśli chcesz uzyskać dostęp do zasobu jako usługi internetowej, musisz wybrać język, w którym chcesz to zrobić.
Dave Van den Eynde
4
Miałem pewne problemy z tego typu rozwiązaniem. Komunikaty o błędach weryfikacji nie były tłumaczone. Aby rozwiązać problem, ustawiłem kulturę w funkcji Application_AcquireRequestState pliku global.asax.cs.
ADH
4
Umieszczenie tego w filtrze NIE jest dobrym pomysłem. Powiązanie modelu korzysta z CurrentCulture, ale ActionFilter występuje po powiązaniu modelu. Lepiej jest to zrobić w Global.asax, Application_PreRequestHandlerExecute.
Stefan
38

Wiem, że to stare pytanie, ale jeśli naprawdę chciałbyś, aby to działało z Twoim ModelBinder (w odniesieniu do DefaultModelBinder.ResourceClassKey = "MyResource";zasobów wskazanych w adnotacjach danych klas viewmodel), kontroler lub nawet ActionFilterjest za późno na ustawić kulturę .

Kulturę można osadzić Application_AcquireRequestStatena przykład w:

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

EDYTOWAĆ

Właściwie jest lepszy sposób na użycie niestandardowego routera, który ustawia kulturę zgodnie z adresem URL, doskonale opisanym przez Alexa Adamyana na swoim blogu .

Wszystko, co trzeba zrobić, to przesłonić GetHttpHandlermetodę i ustawić tam kulturę.

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
źródło
Niestety RouteData itp. Nie są dostępne w metodzie „Application_AcquireRequestState”, ale znajdują się w Controller.CreateActionInvoker (). Dlatego proponuję „chronione zastąpienie IActionInvoker CreateActionInvoker ()” i ustawienie tutaj CultureInfo.
Skorunka František
Czytałem tego bloga. Czy jest jakiś problem, jeśli zdecyduję się na plik cookie? Ponieważ nie mam uprawnień, aby to zmienić. Uprzejmie proszę o poinformowanie mnie. czy jest jakiś problem z tym podejściem?
kbvishnu
@VeeKeyBee Jeśli Twoja witryna jest publiczna, wszystkie języki nie zostaną poprawnie zaindeksowane podczas korzystania z plików cookie, w przypadku witryn chronionych prawdopodobnie wszystko w porządku.
marapet
nie jest to publiczne. Czy możesz podać wskazówkę dotyczącą słowa „indeksowane”?
kbvishnu
1
Powinieneś zadać własne pytanie i poczytać o SEO, nie ma to już nic wspólnego z pierwotnym pytaniem. webmasters.stackexchange.com/questions/3786/ ...
marapet
25

Zrobiłbym to w przypadku inicjalizacji kontrolera w ten sposób ...

    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
źródło
1
ciąg kultury nie może być stałą, ponieważ użytkownik musi mieć możliwość określenia kultury, której chciałby używać w witrynie.
NerdFury
2
Rozumiem to, ale pytanie dotyczyło tego, gdzie najlepiej ustawić kulturę, a nie jak ją ustawić.
Jace Rhea
Zamiast stałej możesz użyć czegoś takiego jak: var newCulture = new CultureInfo (RouteData.Values ​​["lang"]. ToString ());
Nordes
AuthorizeCore jest wywoływana przed OnActionExecuting, więc nie będziesz mieć żadnych szczegółów kultury w swojej zastąpionej metodzie AuthorizeCore. Użycie metody inicjowania kontrolera może działać lepiej, zwłaszcza jeśli implementujesz niestandardowy AuthorizeAttribute, ponieważ metoda Initialize jest wywoływana przed AuthorizeCore (będziesz mieć szczegóły kultury w AuthorizeCore).
Nathan R
7

Ponieważ jest to ustawienie przechowywane dla każdego użytkownika, sesja jest odpowiednim miejscem do przechowywania informacji.

Zmieniłbym kontroler tak, aby przyjmował ciąg kultury jako parametr, zamiast mieć inną metodę akcji dla każdej potencjalnej kultury. Dodanie linku do strony jest łatwe i nie powinno być konieczne wielokrotne pisanie tego samego kodu za każdym razem, gdy wymagana jest nowa kultura.

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
źródło
dzięki za odpowiedź, nie próbuję się zastanawiać, czy użyć sesji, czy nie. Jestem zadowolony z tego kawałka. Próbuję ustalić, czy najlepiej jest to zrobić w kontrolerze kultury, który ma metodę akcji dla każdej kultury do ustawienia, czy też jest lepsze miejsce w potoku MVC, aby to zrobić
ChrisCa
Podałem zredagowaną odpowiedź, która lepiej pasuje do pytania.
NerdFury
Tak, to z pewnością jest czystsze, ale naprawdę chcę wiedzieć, czy w ogóle należy to zrobić w kontrolerze. Lub jeśli istnieje lepsze miejsce w potoku MVC do ustawienia Culture. Lub jeśli jest lepiej w ActionFilters, Handlers, Modules itp.
ChrisCa
Program obsługi i moduł nie mają sensu, ponieważ użytkownik nie miał okazji dokonać wyboru. Potrzebujesz sposobu, aby użytkownik dokonał wyboru, a następnie przetworzył wybór użytkowników, który zostanie wykonany w kontrolerze.
NerdFury
uzgodniono, programy obsługi i moduły są za wcześnie, aby umożliwić interakcję z użytkownikiem. Jednak jestem całkiem nowy w MVC, więc nie jestem pewien, czy jest to najlepsze miejsce w potoku, aby to ustawić. Jeśli po chwili nie usłyszę inaczej, przyjmuję twoją odpowiedź. ps wydaje się, że składnia, której użyłeś do przekazania parametru do metody Action, nie działa. Nie ma zdefiniowanego kontrolera, więc używa tylko domyślnego (który w tym przypadku nie jest prawidłowy). I wydaje się, że nie ma innego odpowiedniego przeciążenia
ChrisCa
6

Jakie jest najlepsze miejsce, to Twoje pytanie. Najlepsze miejsce znajduje się wewnątrz metody Controller.Initialize . MSDN pisze, że jest wywoływana po konstruktorze i przed metodą akcji. W przeciwieństwie do zastępowania OnActionExecuting, umieszczenie kodu w metodzie Initialize umożliwia skorzystanie z posiadania wszystkich niestandardowych adnotacji danych i atrybutów w klasach i we właściwościach, które mają być zlokalizowane.

Na przykład moja logika lokalizacji pochodzi z klasy, która jest wstrzykiwana do mojego kontrolera niestandardowego. Mam dostęp do tego obiektu, ponieważ Initialize jest wywoływana po konstruktorze. Mogę przypisać kulturę wątku i nie wyświetlać poprawnie wszystkich komunikatów o błędach.

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

Nawet jeśli twoja logika nie znajduje się w klasie takiej jak w przykładzie, który podałem , masz dostęp do RequestContext, który pozwala ci mieć adres URL i HttpContext oraz RouteData, które możesz wykonać w zasadzie każdą możliwą analizę.

Patrick Desjardins
źródło
Działa to w przypadku mojej HTML5 Telerik ReportLocalization !. Dzięki @Patrick Desjardins
CoderRoller
4

Jeśli używasz subdomen, na przykład „pt.moja_domena.com”, aby ustawić portugalski, na przykład użycie Application_AcquireRequestState nie zadziała, ponieważ nie jest wywoływane przy kolejnych żądaniach pamięci podręcznej.

Aby rozwiązać ten problem, proponuję taką implementację:

  1. Dodaj parametr VaryByCustom do OutPutCache w następujący sposób:

    [OutputCache(Duration = 10000, VaryByCustom = "lang")]
    public ActionResult Contact()
    {
        return View("Contact");
    }
    
  2. W global.asax.cs pobierz kulturę od hosta za pomocą wywołania funkcji:

    protected void Application_AcquireRequestState(object sender, EventArgs e)
    {
        System.Threading.Thread.CurrentThread.CurrentUICulture = GetCultureFromHost();
    }
    
  3. Dodaj funkcję GetCultureFromHost do 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. I na koniec zastąp GetVaryByCustomString (...), aby również użyć tej funkcji:

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

Funkcja Application_AcquireRequestState jest wywoływana w przypadku wywołań niebuforowanych, co umożliwia generowanie i buforowanie treści. GetVaryByCustomString jest wywoływana w wywołaniach buforowanych w celu sprawdzenia, czy zawartość jest dostępna w pamięci podręcznej, iw tym przypadku ponownie sprawdzamy przychodzącą wartość domeny hosta, zamiast polegać tylko na bieżących informacjach o kulturze, które mogły ulec zmianie dla nowego żądania (ponieważ używamy subdomen).

Andy
źródło
4

1: utwórz atrybut niestandardowy i zastąp metodę w następujący sposób:

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: w App_Start znajdź FilterConfig.cs, dodaj ten atrybut. (to działa dla CAŁEJ aplikacji)

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

Otóż ​​to !

Jeśli chcesz zdefiniować kulturę dla każdego kontrolera / akcji zamiast całej aplikacji, możesz użyć tego atrybutu w następujący sposób:

[Culture]
public class StudentsController : Controller
{
}

Lub:

[Culture]
public ActionResult Index()
{
    return View();
}
Meng Xue
źródło
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
źródło
3
Proszę wyjaśnić, dlaczego ma to być najlepszy sposób.
Max Leske