Da es häufiger vorkommt, dass eine Website sowohl auf englisch als auch auf deutsch verfügbar sein muss (und mehr!), wollte ich mit diesen Blogpost die wichtigsten Best-Practices versuchen wiedergeben. Ich hatte bereits zu dem Thema einen älteren Blogpost geschrieben, dieser ist aber nicht mehr ganz aktuell.
Was kann man alles mit “Standardmitteln” lokalisieren?
Das augenscheinlichste sind natürlich einfache Texte, welche über Resource Files (ich gehe mal davon aus, dass der Umgang damit bekannt ist. Ansonsten nach .resx suchen) ausgelesen werden. Ein weiteres großes Kapitel ist die Ausgabe von Beschreibungstexten für das Model bzw. Validierungsfehler.
Sample App
Angehangen an den Blogpost ist auch eine Sample App, welche allerdings noch keine Resourcen für Modelbeschreibungen bzw. für die Validierung nutzt – allerdings ist das schon mal ein guter Ausgangspunkt.
Die Gelb markierten Sachen sind "besonders wichtig”. Der andere Source Code kommt entweder mit dem Template mit oder er hat sich in größeren Projekten als ganz nützlich herausgestellt – auch wenn es etwas nach Raketenwissenschaft aussieht.
Wichtigster Teil: Der CurrentLanguageStore. Diese Komponente kann ich aufrufen um herauszufinden welche Sprache der User hat (diese Information schickt der Browser z.B. mit) oder ob der User explizit eine andere Sprache gewählt hat – dann kommt diese in meinem Beispiel aus dem Cookie.
Eine WebApp kann nicht alle Sprachen unterstützen, daher muss irgendwo festgelegt werden, welche Sprachen definitiv unterstützt werden. Dies habe ich über ein Enum namens “LanguageKey” realisiert.
CurrentLanguageStore
Der CurrentLanguageStore besitzt zwei Methoden:
- GetPreferredLanguage: Gib die bevorzugte Sprache des Nutzers zurück.
- SetPreferredLanguage: Setze die bevorzugte Sprache explizit.
Der Code ist etwas größer, weil er aus einem anderen Projekt ist und ich dort via Dependency Injection die benötigten Komponenten mit reinlade. Die “GetPreferredLanguage” Methode sucht nach einem Cookie und ob in diesem Cookie eine valide Sprache abgespeichert ist. Das Cookie kommt nur zum Tragen wenn ein User explizit (z.B. in einem Internet-Cafe) eine andere Sprache haben möchte als der Browser. Dieses Setzen erfolgt bei der “SetPreferredLanguage” Methode. Da ich an der Stelle viel mit Cookies arbeite, habe ich mir einen kleinen Helper gebaut. Den braucht man allerdings nicht unbedingt – man kann diese Speicherung auch pur vornehmen.
public class CurrentLanguageStore { /// <summary> /// Cookie Repository for getting and setting cookie values. /// </summary> private ICookieRepository _cookieRep; /// <summary> /// HttpContext for accessing the HttpRequests UserLanguages /// </summary> private HttpContextBase _context; public CurrentLanguageStore() { this._cookieRep = new HttpCookieRepository(); this._context = new HttpContextWrapper(HttpContext.Current); } /// <summary> /// Default ctor for a new instance. /// </summary> /// <param name="baseContext">HttpBaseContext for accessing the HttpRequest.</param> /// <param name="cookieRepository">Implementation of ICookieRepository.</param> public CurrentLanguageStore(HttpContextBase baseContext, ICookieRepository cookieRepository) { _cookieRep = cookieRepository; _context = baseContext; } /// <summary> /// Gets the preferred language. /// </summary> /// <returns></returns> public LanguageKey GetPreferredLanguage() { string[] browserLanguages = this._context.Request.UserLanguages; if (this._cookieRep.HasElement(CookieKey.UserLanguage)) { string cookieResult = this._cookieRep.GetElement(CookieKey.UserLanguage); if (string.IsNullOrWhiteSpace(cookieResult)) return LanguageKey.En; if (cookieResult.ToLower() == LanguageKey.De.ToString().ToLower()) return LanguageKey.De; return LanguageKey.En; } if (browserLanguages == null) return LanguageKey.En; foreach (var language in browserLanguages) { if (language.StartsWith(LanguageKey.De.ToString().ToLower())) return LanguageKey.De; else if (language.StartsWith(LanguageKey.En.ToString().ToLower())) return LanguageKey.En; } return LanguageKey.En; } public void SetPreferredLanguage(LanguageKey key) { if (this._cookieRep.HasElement(CookieKey.UserLanguage)) { this._cookieRep.UpdateElement(CookieKey.UserLanguage, key.ToString()); } else { this._cookieRep.AddElement(CookieKey.UserLanguage, key.ToString()); } } }
Meine Anwendung unterstützt nur Deutsch und Englisch – auch der regional Code z.B. für de-AT ist mir an der Stelle egal, wäre aber auch möglich.
public enum LanguageKey { De = 1031, En = 1033 }
Das ist im Grunde die Infrastruktur (+ die Hilfsklassen für den Zugriff auf das Cookie) um die Lokalisierung zu machen.
Der LanguageController
Der LanguageController hat nur die Aufgabe dem Benutzer ein UI-Element anzuzeigen um die Sprache zu wechseln und den Aufruf auch an den CurrentLanguageStore weiterzugeben.
public class LanguageController : Controller { private CurrentLanguageStore _languageStore; public LanguageController() { this._languageStore = new CurrentLanguageStore(); } public RedirectToRouteResult SwitchLanguage(LanguageKey key) { this._languageStore.SetPreferredLanguage(key); return RedirectToAction("Index", "Home"); } public ActionResult LanguageBox() { LanguageKey languageNow = this._languageStore.GetPreferredLanguage(); if (languageNow == LanguageKey.De) ViewBag.AvailableLanguage = LanguageKey.En; else ViewBag.AvailableLanguage = LanguageKey.De; return View(); }
Der View zur LanguageBox:
Der Code hier ist nicht besonders schön (und geht schon schief wenn ich nur eine weitere Sprache unterstützen möchte), aber an der Stelle gibt es für euch noch Potenzial zum Verbessern
@using MvcLocalization.WebApp.Infrastructure @{ Layout = ""; var availableLanguageText = ""; var availableLanguage = LanguageKey.De; if(ViewBag.AvailableLanguage == LanguageKey.De) { availableLanguageText = "Deutsch"; availableLanguage = LanguageKey.De; } else { availableLanguageText = "English"; availableLanguage = LanguageKey.En; } } @Html.ActionLink(availableLanguageText, "SwitchLanguage", "Language", new { key = availableLanguage.ToString() }, null)
Ganz wichtig: Anwendung des CurrentLanguageStore über einen BaseController
In dem alten Blogpost habe ich geschrieben, dass man nun den CurrentLanguageStore in einem ActionFilter einsetzen kann und dort die CurrentCulture des Threads zu manipulieren. Allerdings ist dieser Weg nicht richtig!
Grund: Das Modelbinding + die Validierung wird ausgeführt bevor die ActionFilter zum Tragen kommen, daher muss die Lokalisierung vorher passieren!
Abhilfe schafft das Überschreiben der ExecuteCore Methode in einem Basis-Controller.
public class BaseController : Controller { protected override void ExecuteCore() { CurrentLanguageStore store = new CurrentLanguageStore(); LanguageKey key = store.GetPreferredLanguage(); CultureInfo language = new CultureInfo(key.ToString()); Thread.CurrentThread.CurrentCulture = language; Thread.CurrentThread.CurrentUICulture = language; base.ExecuteCore(); } }
Die Anwendung
Damit sollte ich nun in der Lage sein, einfach über Resourcen Dateien meine Texte sowohl im Model als auch im View zu lokalisieren. Eine größere Beschreibung zum Thema findet sich auch auf diesem Blog – von der Herangehensweise ist es ähnlich wie meine Variante. Allerdings dort bereits mit Model-Lokalisierung ausgestattet – ein Blick lohnt!
<p> @TestResource.Title </p>