ASP.NET MVC I18n – Teil 3: Wechsel der Sprache

15. Januar 2014

Der letzte Beitrag zeigte, wie man die Spracheinstellungen des Browsers auswertet und anwendet. Trotzdem ist es üblich, dem Anwender die Möglichkeit zu geben, die Sprache unabhängig davon selbst zu wählen.

Hinweis: Dieser Beitrag ist Teil einer Serie, die Übersicht findet sich hier.

 

Um das zu erreichen sind zwei Dinge zu tun:

  1. Wir brauchen eine Möglichkeit, seinen Wunsch zu verwalten um damit die Browser-Einstellung zu überschreiben.
  2. Wir müssen ihm die Möglichkeit geben, die Sprache tatsächlich zu wechseln.

Die bevorzugte Sprache verwalten…

Wie wir wissen kommt jeder Request mit dem accept-language Header, der uns die Spracheinstellungen im Browser mitteilt. Etwas vergleichbares brauchen wir für die bevorzugte Sprache des Anwenders.

Eine oft verwendete Möglichkeit ist, die Region in die URL aufzunehmen. Für MVC hat das zur Folge, dass sie in jeder Route und jedem Link berücksichtigen muss. Alex zeigt wie die URLs automatisch über einen MvcRouteHandler ausgewertet können, Nadeem packt die Logik zur Auswertung in eine Controller-Basisklasse. MVC bietet sicher auch irgendwo eine Stelle, an der man das zentral für die Generierung von URLs lösen kann (etwa über eine eigene Route Klasse).

Das funktioniert und man macht mit diesem Ansatz sicher nichts falsch. Trotzdem mag ich ihn nicht besonders. Aus technischer Sicht sind damit URLs in statischen Inhalten nicht berücksichtigt, etwa in CSS Dateien. Dazu kommt, das andere Features ebenfalls Einfluss auf die URL nehmen, etwa Areas, was zu Problemen führen kann. Und aus eher abstrakter Sichtweise sind URLs dazu gedacht "Ressourcen" zu adressieren – komplex genug, auch ohne noch weitere Dinge dazu zu packen.  Sprache, ebenso wie Farbeinstellungen und andere Profil-Einstellungen, sind "orthogonale" Anforderungen, die das Ergebnis in seinem Erscheinungsbild beeinflussen, aber eben nicht adressieren. Dummerweise liefert einem MVC leider keine Antwort auf die Frage, wie solche Informationen transportiert werden sollen, weshalb man alle Arten von Lösungen findet: URL-Bestandteile, Query Parameter, Cookies, Sessions, … . Und keine Variante ist immer gleich gut oder schlecht.

Für den vorliegenden Fall, die Sprache, habe ich mich für einen Cookie entschieden. Die URL anzupassen ist mir zu invasiv, Query Parameter zu fehleranfällig, und eine Session nur für diese Anforderung? No way!

Einen Cookie zu Schreiben ist simpel:

   1: const string CookieName = "PreferredCulture";

   2:  

   3: public static void SetPreferredCulture(this HttpResponseBase response, string cultureName)

   4: {

   5:     SetPreferredCulture(response.Cookies, cultureName);

   6: }

   7:  

   8: static void SetPreferredCulture(HttpCookieCollection cookies, string cultureName)

   9: {

  10:     var cookie = new HttpCookie(CookieName, cultureName);

  11:     cookie.Expires = DateTime.Now.AddDays(30);

  12:     cookies.Set(cookie);

  13:     Debug.WriteLine("SetPreferredCulture: " + cultureName);

  14: }

Ebenso das Auslesen:

   1: static CultureInfo GetPreferredCulture(HttpCookieCollection cookies)

   2: {

   3:     var cookie = cookies[CookieName];

   4:     if (cookie == null)

   5:         return null;

   6:     var culture = GetCultureInfo((string)cookie.Value);

   7:     if (culture == null)

   8:         return null;

   9:     if (!SupportedCultures.Where(ci => ci.Name == culture.Name).Any())

  10:         return null;

  11:     return culture;

  12: }

Etwas mehr Code als erwartet, aber aus Prinzip vertraue ich dem Request erstmal nicht. Also wird die Gültigkeit des Cookie-Wertes rigoros geprüft.

Was noch fehlt ist die Nutzung des Cookies während der Initialisierung des Requests:

   1: static void ApplyUserCulture(NameValueCollection headers, HttpCookieCollection cookies)

   2: {

   3:     var culture = GetPreferredCulture(cookies)

   4:         ?? GetUserCulture(headers)

   5:         ?? SupportedCultures[0];

   6:  

   7:     var t = Thread.CurrentThread;

   8:     t.CurrentCulture = culture;

   9:     t.CurrentUICulture = culture;

  10:     Debug.WriteLine("Culture: " + culture.Name);

  11: }

 

Ändern der bevorzugten Sprache….

Nachdem die Grundlagen gelegt sind braucht es noch MVC-typisch einen entsprechende Controller und Action:

   1: public class CultureController : Controller

   2: {

   3:     //

   4:     // GET: /SetPreferredCulture/de-DE

   5:     public ActionResult SetPreferredCulture(string culture, string returnUrl)

   6:     {

   7:         this.Response.SetPreferredCulture(culture);

   8:         if (string.IsNullOrEmpty(returnUrl))

   9:             return RedirectToAction("Index", "Home");

  10:         return Redirect(returnUrl);

  11:     }

  12: }

Dazu die entsprechende Route:

   1: var route = routes.MapRoute(

   2:     name: "SetPreferredCulture",

   3:     url: "SetPreferredCulture/{culture}",

   4:     defaults: new { controller = "Culture", action = "SetPreferredCulture", culture = UrlParameter.Optional }

   5: );

Wie das UI gestaltet wird ist Geschmackssache. Üblich ist sicher eine Dropdownlist. Ich habe mich für einen einfachen Link entschieden, mit dem die nächste Region aus der Lister der unterstützten Regionen gewählt wird. Dabei unterstützt eine kleine Hilfsmethode:

   1: public static void GetSwitchCultures(out CultureInfo currentCulture, out CultureInfo nextCulture)

   2: {

   3:     currentCulture = Thread.CurrentThread.CurrentUICulture;

   4:     var currentIndex = Array.IndexOf(SupportedCultures.Select(ci => ci.Name).ToArray(), currentCulture.Name);

   5:     int nextIndex = (currentIndex + 1) % SupportedCultures.Length;

   6:     nextCulture = SupportedCultures[nextIndex];

   7: }

Fehlt noch das eigentliche UI in form einer partial view:

   1: @{

   2:     // cycle through supported cultures

   3:     System.Globalization.CultureInfo currentCulture;

   4:     System.Globalization.CultureInfo nextCulture;

   5:     MyStocks.Mvc.Helper.CultureHelper.GetSwitchCultures(out currentCulture, out nextCulture);

   6:     string currentCultureDisplayName = currentCulture.Parent.NativeName;

   7:     string nextCultureDisplayName = nextCulture.Parent.NativeName;

   8:     string linkText = currentCultureDisplayName  + " => "+ nextCultureDisplayName;

   9:     string url= Url.Action("SetPreferredCulture", "Culture", new { culture = nextCulture.Name, returnUrl = Request.RawUrl });

  10: }

  11: <div>

  12:     @Html.ActionLink(linkText, "SetPreferredCulture", "Culture", new { culture = nextCulture.Name, returnUrl = Request.RawUrl }, null)

  13: </div>

Diese in _Layout.cshtml aufgenommen, fertig:

Lokalisierung-4-SetPreferredCulture

Ein Klick oben rechts, und man kann die Sprache wechseln.