Best Practice Localization: Lokalisierung einer ASP.NET MVC WebApp

5012422171_ee81beb92f_z.jpg

(Featured Foto Quelle)

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

image

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 Zwinkerndes Smiley

@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>

 

[ Download Democode ]


Kick It auf dotnet-kicks.de
Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by Robert Mühsig

Robert Mühsig (@robert0muehsig) ist Webentwickler und beschäftigt sich mit Web-Frameworks (vor allem dem ASP.NET MVC Framework) und scheut sich auch nicht vor Javascript. Ansonsten bloggt er über all jene Probleme, die ihm über den Weg laufen. Seit 2008 ist er Microsoft MVP für ASP.NET und er arbeitet bei der T-Systems Multimedia Solutions GmbH in Dresden. Treffen kann man ihn online via Twitter (@robert0muehsig) oder dieser Seite oder bei der .NET User Group Dresden.

4 Responses

  1. Hallo Robert,
    ich hatte letztens auch mit dem Thema zu tun und fand den Artikel sehr hilfreich.
    http://adamyan.blogspot.com/2010/02/aspnet-mvc-2-localization-complete.html
    Der verlinkte Ansatz arbeitet sehr ähnlich und nutzt die preferred Language aus dem HTTP-Header und das Setzen der Sprache erfolgt hier in der Global.asax. Des Weiteren fand ich die Aufteilung der Ressourcen nach View sehr gut gelungen.
    Hat die von dir beschriebene Vorgehensweise (über den Cookie) Vorteile gegenüber der Auswertung des HTTP-Headers, oder ist es gar besser beide zu nutzen?
    Bruß, Sebastian

    Reply
    • Ich les die Sprache auch aus den Http-Headern. Im Request schickt der Browser ja die UserLanguage mit. Diese lese ich auch aus und schau ob ich eine dieser Sprachen anbieten kann. Der Cookie kommt nur zum Tragen wenn der User explizit eine andere Sprache wählt – hier ist das Szenario z.B. in einem Internet-Cafe. Ich speicher mir das im Cookie, es würde aber theoretisch auch die Session gehen oder gar die URL. Je nachdem was man erreichen will.
      Ich hab das setzen der Sprache in einem Base-Controller gemacht, weil ich meine Global.asax möglichst schlank halten wollte. Aber vom Prinzip her funktioniert beides ähnlich :)

      Reply
  2. Hi Robert,

    wie es der Zufall will brauch ich sowas die Tage. Hat mir die Suche schon mal erleichtert :) Besten Dank dafür!

    Gruß
    Björn

    Reply
  3. Hio,
    sehr nice beschrieben, ich hatte z.B. auch so meine Probleme beim Einbinden von Globalen Lokalisierungsdateien, was ich hier mal niedergeschrieben habe, evtl. hilfts wem weiter.

    http://squadwuschel.wordpress.com/2011/06/22/asp-net-mvc-3-lokalisierung/

    mfg squadwuschel

    Reply

Comment on this post

Letzte Posts

  • image.png
    RavenHQ–RavenDB in der Cloud

    Ayende Rahien hat es heute verkündet – RavenHQ, der RavenDB Cloud Hoster (natürlich von und mit Ayende) ist ab heute raus aus der Beta und man kann es von überall aus nutzen. In der Betaphase waren nur Nutzer von AppHarbor zugelassen. Was ist RavenHQ? RavenHQ ist im Grunde ein gehostes RavenDB in den Rechenzentren von ...

  • image.png
    GitHub for Windows–erste Eindrücke

    Git ist schon eine tolle Sachen und eröffnet viele neue Möglichkeiten – allerdings ist der Einstieg recht hart und selbst wenn man die guten Hilfsanleitungen auf GitHub befolgt, kommt man am Anfang nur langsam vorwärt. Insbesondere ist das Tooling für Windows / .NET Entwickler auch nicht gerade “bekanntes Terrain”. GitHub to the rescue! Die GitHub ...

  • image.png
    Chocolatey–apt-get für Windows

    Durch Zufall bin ich auf das Tool “Chocolatey” gestoßen. Wer die Website sich anschaut, wird evtl. eine Verwandschaft mit NuGet ausmachen. Was macht Chocolatey? Chocolatey ist ein “Maschine Package Manager”, das bedeutet, dass man für seine Maschine einfach Tools runterladen und Updaten kann – direkt über die Konsole. Was ist der Unterschied zu NuGet? NuGet ...

  • image.png
    SASS, LESS & Coffeescript in Visual Studio mit der Web Workbench

    CSS und Javascript sind die “kleinste” Schnittmenge von allen Browsern für die Erstellung von Web-Applikationen. Leider geht dabei etwas komfort verloren, daher lieben alle Webentwickler jQuery! SASS und LESS sind zwei Varianten, wie man “schöner” CSS schreiben kann und Coffeescript versucht Javascript Entwicklung zu vereinfachen. Aber immer der Reihe nach… Was ist SASS? SASS steht ...

  • image.png
    Code-Inside Sample nun auf GitHub: Google Code zu GitHub Migration

    Seit einiger Zeit habe ich Beispielcode auf Google Code bereitgestellt. Einfach nur noch weg von Google Code O-Ton damals war: Ich hatte mich für Google Code entschieden, weil ich hoffe dass früher oder später die Google Code Suche nutzbar ist und es dadurch wenigstens ein kleiner Mehrwert entsteht. Allerdings wirft es momentan noch ein Fehler. ...

Auf Amazon einkaufen & unterstützen

Facebook