Spiria logo.
Anthony Giretti
dans «  Applications web  »,
 
05 octobre 2015.

Comment gérer facilement un site multilingue: un tutoriel pour ASP .NET MVC

Les technologies de l’information sont une partie intégrante de nos vies et conséquemment, l’accès au contenu depuis n’importe où dans le monde est devenu chose courante. Les développeurs doivent donc créer de plus en plus des sites multilingues afin de rejoindre une audience plus large. Voici un tutoriel afin de gérer facilement un site multilingue avec ASP .NET MVC. 

J’utilise des fichiers XML comme fichier source afin d’éviter d’être attaché à une base de données. En conséquence, je crée un service qui implémente une interface afin d’être capable d’effectuer des changements rapidement.

Avec ce service, je suis capable de :

retrouver des ressources afin de créer des contrôles HTML dynamiques;

créer un HtmlHelpers qui donnent accès à toutes ces ressources;

créer des attributs sur des modèles afin de traduire les étiquettes de champs.

Étape 1 : définir le fichier source XML

Je recommande de créer un répertoire spécifique dans «App_GlobalResources», par exemple : «XmlResources».

Afin de traduire la page d’accueil de notre site Web, nous pouvons créer le fichier Home.xml qui différenciera les ressources sur cette page des ressources des autres pages. Disons que cela contient deux ressources :

<?xml version="1.0" encoding="utf-8" ?>
 <Resources>
  <Resource key="HelloWordKey">
   <Language key="EN">Hello World!</Language>
   <Language key="FR">Bonjour le monde!</Language>
  </Resource>
  <Resource key="MyNameKey">
   <Language key="EN">My name is Anthony</Language>
   <Language key="FR">Mon prénom est Anthony</Language>
  </Resource>
  <Resource key="EnterYourNationalityKey">
   <Language key="EN">What's your nationality</Language>
   <Language key="FR">De quelle nationalité êtes-vous?</Language>
  </Resource>
</Resources>

Étape 2: créer un lecteur spécifique à ces fichiers

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using MyApp.Tools;

namespace MyApp.Services
{
   public static class ResourceXmlReader
   {
       //static property, public readable only
       public static readonly Dictionary<string, Dictionary<string, Dictionary<string, string>>> Resources = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>();

       //static constructor
       static ResourceXmlReader()
       {
           try {
           string path = System.Web.Hosting.HostingEnvironment.MapPath("~/App_GlobalResources/XmlResources/");
           FolderContentBrowser content = new FolderContentBrowser(path);
           LoopOnResources(content.FileNameList);
           }
           catch { }
       }

       //Browse each xml resource file on the current directory
       private static void LoopOnResources(List<string> fileList)
       {
          fileList.Where(o => o.EndsWith(".xml")).ToList().ForEach(o => OpenAndStoreResource(o));
       }

       //Open, read and store into the static property xml file
       private static void OpenAndStoreResource(string resourcePath)
       {
          try {
          string fileName = Path.GetFileName(resourcePath).Split('.')[0];
          XDocument doc = XDocument.Load(resourcePath);
          if (null != doc) {
             Dictionary<string, Dictionary<string, string>> currentResource = new Dictionary<string, Dictionary<string, string>>();
             var resources = doc.Descendants("Resource").ToList();
             resources.ForEach(o => currentResource.Add(o.Attribute("key").Value, getEachLanguage(o.Elements("Language"))));

             //attachement des resources à une ressource nommée
             Resources.Add(fileName, currentResource);
          }
          }
          catch { }
        }

        //Loop on each language into the file
        private static Dictionary<string, string> getEachLanguage(IEnumerable<XElement> elements)
        {
           Dictionary<string, string> langList = new Dictionary<string, string>();
           elements.ToList().ForEach(o => langList.Add(o.Attribute("key").Value, o.Value));
           return langList;
        }
     }
}

J’utilise un constructeur statique car il sera exécuté seulement une fois. Les fichiers XML ne seront lus qu’une seule fois et les archives le seront par la suite dans mes propriétés statiques. Cette étape est la clé d’un système multilingue efficace. Il ne faut pas lire les fichiers XML à chaque chargement de page.

Notez que le lecteur développe un string de dictionnaires. Les données sont arrangées de cette façon : nom du fichier XML dictionnaire (un pour chaque page), qui à son tour contient une langue qui est elle-même un dictionnaire ressource (ressource clé, valeur texte de la ressource).

Étape 3: créer un service pour l’interface de gestion des accès aux ressources

using System;
using System.Collections.Generic;
namespace MyApp.Globalization
{
   public interface IResourceService
   {
      string GetResource(string resourceName, string resourceKey);
      Dictionary<string, Dictionary<string, string>> GetRessourcesByName(string resourceName);
   }
}

using MyApp.Services;
using System.Collections.Generic;
using System.Globalization;

namespace MyApp.Globalization
{
   public class ResourceService : IResourceService
   {
      public string GetResource(string resourceName, string resourceKey)
      {
         try {
            string language = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.ToUpper();
            if (ResourceXmlReader.Resources.ContainsKey(resourceName)) {
               if (ResourceXmlReader.Resources[resourceName].ContainsKey(resourceKey)) {
                  if (ResourceXmlReader.Resources[resourceName][resourceKey].ContainsKey(language))
                     return ResourceXmlReader.Resources[resourceName][resourceKey][language];
                  else
                     return ResourceXmlReader.Resources[resourceName][resourceKey]["EN"];
               }
               else
                  return string.Empty;
            }
            else return string.Empty;
            }
        catch { return string.Empty; }
      }

      public Dictionary<string, Dictionary<string, string>> GetRessourcesByName(string resourceName)
      {
         try {
            return ResourceXmlReader.Resources[resourceName];
         }
         catch { return null; }
      }
   }
}

Nous avons accès à la bonne ressource en utilisant la propriété "TwoLetterISOLanguageName", mais nous devons la définir à l'étape 4.

Étape 4 : créer un attribut action filter qui définit la langue dans le texte actuel

using System.Globalization;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;

namespace MVC.Globalization
{
   public class GlobalizeFilterAttribute : ActionFilterAttribute
   {
      /// <summary>
      /// Define language in current context
      /// </summary>
      /// <param name="filterContext"></param>
      public override void OnActionExecuting(ActionExecutingContext filterContext)
      {
         //Get current Http HttpContextBase context = filterContext.HttpContext;
         //if sent by Url
         string cultureName = context.Request.QueryString["lang"];
         //Cookie test
         if (string.IsNullOrEmpty(cultureName))
         {
            cultureName = (null != context.Request.Cookies["lang"]) ? context.Request.Cookies["lang"].Value : string.Empty;
            if (string.IsNullOrEmpty(cultureName))
            {
               try {
                  //sinon langue du navigateur
                  cultureName = context.Request.UserLanguages.FirstOrDefault();
                  if (string.IsNullOrEmpty(cultureName)) cultureName = "EN";
               }
               catch { cultureName = "EN"; }
            }
         }
         else
         {
            var langCookie = new HttpCookie("lang");
            langCookie.Value = cultureName;
            context.Response.Cookies.Add(langCookie);
         }

         // Change culture on current thread
         CultureInfo culture = CultureInfo.CreateSpecificCulture(cultureName);
         Thread.CurrentThread.CurrentCulture = culture;
         Thread.CurrentThread.CurrentUICulture = culture;

         //action continuation
         base.OnActionExecuting(filterContext);
      }
   }
}

Cet attribut permet d’intercepter la langue définie depuis un formulaire (un URL dans cet exemple) et le mémoriser dans un cookie.

Si aucune langue n’est définie par un formulaire ou un cookie, la première langue définie dans le navigateur sera utilisée. Si aucune langue n’est définie dans le navigateur, l’anglais sera utilisé par défaut.

Cet attribut fonctionne si vous définissez la langue vous-même ou si vous définissez une langue par défaut.

Pour utiliser cet attribut pour chaque page de votre site, il faut définir un filtre global dans la classe FilterConfig comme suit :

using MVC.Globalization;
using System.Web;
using System.Web.Mvc;

namespace MVC
{
   public class FilterConfig
   {
      public static void RegisterGlobalFilters(GlobalFilterCollection filters)
      {
         filters.Add(new GlobalizeFilterAttribute());
         filters.Add(new HandleErrorAttribute());
      }
   }
}

Maintenant, il faut implémenter chaque cas possible d’utilisation de la fonction de traduction depuis IResourceService.

Étape 5 : implanter les cas d’utilisation de cette fonction

  • HtmlHelper :
using MyApp.Globalization;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web.Mvc;

namespace MVC.Helpers
{
   public static class ResourceHelper
   {
      private static IResourceService _resources;
      public static string GetResource(this HtmlHelper helper, string resourceName, string resourceKey)
      {
         CheckProvider();
         return _resources.GetResource(resourceName, resourceKey);
      }

      public static MvcHtmlString GetJSONResources(this HtmlHelper helper, string[] resourcesName)
      {
         CheckProvider();
         string lang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.ToUpper();
         TagBuilder builder = new TagBuilder("script");
         builder.MergeAttribute("type", "text/javascript");
         StringBuilder strBuilder = new StringBuilder();
         strBuilder.AppendLine();
         strBuilder.AppendLine("var MyApp = MyApp || {};");
         strBuilder.AppendLine("MyApp.Resources = MyApp.Resources || {};");
         strBuilder.AppendLine("MyApp.Resources =");
         strBuilder.AppendLine("{");
         resourcesName.ToList().ForEach(resourceName => {
            var ressourceCollection = _resources.GetRessourcesByName(resourceName);
            if (null != ressourceCollection && ressourceCollection.Count > 0)
            {
               int nbElements = ressourceCollection.Count;
               int i = 1;
               foreach (KeyValuePair<string, Dictionary<string, string>> item in ressourceCollection) {
                  string value = string.Empty;
                  try {
                     value = item.Value[lang];
                  }
                  catch {
                     try {
                        value = item.Value["EN"];
                     }
                     catch { }
                  }
                  strBuilder.AppendFormat(@"""{0}"" : ""{1}""", item.Key, value);
                  strBuilder.Append(",");
                  strBuilder.AppendLine();
                  i++;
               }
            }
         });
         strBuilder.Remove(strBuilder.Length - 3, 1);
         strBuilder.AppendLine("}");
         builder.InnerHtml = strBuilder.ToString();
         return new MvcHtmlString(builder.ToString());
      }

      public static void RegisterProvider(IResourceService provider)
      {
         _resources = provider;
      }

      private void CheckProvider()
      {
         if (null == _resources)
            throw new Exception("Resource provider is not set");
      }
   }
}

J’ai créé deux manières de faire cela. La première « GetResource », permet de contacter la ressource que vous désirez afficher en HTML. La deuxième « getJSONResources », permet de faire une série pour compléter la ressource dans un objet JSON afin d’utiliser la ressource avec Javascript. Cela prend une suite de strings parce que vous pouvez faire une série de plusieurs ressources (définie comme « dictionnaire » tel que stipulé dans le début de l’article).

Comme cette fonctionnalité requiert une instance IResourceService, vous devez enregistrer une instance là où l’application commence, comme ceci :

using MVC.Helpers;
using MyApp.Globalization;
using System.Web.Mvc;
using System.Web.Routing;

namespace MVC
{
   public class MvcApplication : System.Web.HttpApplication
   {
      protected void Application_Start()
      {
         AreaRegistration.RegisterAllAreas();
         RouteConfig.RegisterRoutes(RouteTable.Routes);
         FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

         IResourceService r = new ResourceService();
         ResourceHelper.RegisterProvider(r);
         CustomDisplayNameAttribute.RegisterProvider(r);
      }
   }
}
  • Attribut des modèles (pour gérer les étiquettes des modèles dans un formulaire HTML) :
using MyApp.Globalization;
using System.ComponentModel;
namespace MVC
{
   public class CustomDisplayNameAttribute : DisplayNameAttribute
   {
      private static IResourceService _resourceService;
      private string _resourceName;
      private string _resourceKey;

      public CustomDisplayNameAttribute(string resourceName, string resourceKey)
      {
         _resourceName = resourceName;
         _resourceKey = resourceKey;
      }

      public override string DisplayName
      {
         get
         {
            CheckProvider();
            return _resourceService.GetResource(_resourceName, _resourceKey);
         }
      }

      public static void RegisterProvider(IResourceService provider)
      {
         _resources = provider;
      }

      private void CheckProvider()
      {
         if (null == _resourceService)
            throw new Exception("Resource provider is not set");
      }
}

namespace MVC.Models
{
   public class TestModel
   {
      [CustomDisplayName("Home", "EnterYourNationalityKey")]
      public string Name { get; set; }
   }
}

Comme pour le dernier HtmlHelper, vous devez enregistrer une instance IResourceService. Une manière de le faire est d’utiliser directement le IResourceService à l’intérieur d’un contrôleur MVC :

using MyApp.Globalization;
using System.Web.Mvc;

namespace MVC.Controllers
{
   public class HomeController : Controller
   {
      private IResourceService _resourceService;

      public HomeController() : this(new ResourceService()) { }

      public HomeController(IResourceService resourceService)
      {
         _resourceService = resourceService;
      }

      // GET: /Index/
      public ActionResult Index()
      {
         ViewData["HelloWorld"] = _resourceService.GetResource("Home", "HelloWordKey");
         return View();
      }
   }
}

Je vous recommande d’injecter la dépendance. Je ne décrirai pas comment le faire dans cet article, mais j’ai préparé le contrôleur afin qu’il ait ce comportement avec ce constructeur public HomeController (IResourceService resourceService)

Étape 6 : tester l’outil sur une page HTML

@using MVC.Helpers
@model MVC.Models.TestModel

@{ ViewBag.Title = "Index"; }
<h2>@Html.Raw(ViewData["HelloWorld"])</h2>
<h3>@Html.GetResource("Home", "MyNameKey")</h3>
<br /> @Html.LabelFor(m=> m.Name)
<br /> @Html.TextBoxFor(m=> m.Name)
@Html.GetJSONResources(new string[] { "Home" })

<script type="text/javascript"> alert(MyApp.Resources.HelloWordKey + "\n" + MyApp.Resources.MyNameKey); </script>

 

Comme vous pouvez voir, il y a un exemple pour chaque outil d’implantation :

  • @Html.GetResource("Home", "MyNameKey") comme HtmlHelper afin d’accéder à une ressource spécifique demandée.
  • @Html.Raw(ViewData["HelloWorld"]) comme ViewData définie dans le contrôleur MVC en accédant directement IResourceService (_resourceService.GetResource("Home", "HelloWordKey");)
  • @Html.GetJSONResources(new string[] { "Home" }) comme HtmlHelper avec une série de ressource dans des objets JSON.
  • @Html.LabelFor(m=> m.Name) comme une traduction des étiquettes de modèles.

Et maintenant, les résultats :

Exemple 1: le français comme langue par défaut dans un navigateur

decorative

decorative

Code source:

<!DOCTYPE html>
   <html>
      <head>
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>MyApp</title>
      </head>
      <body>
         <h2>Bonjour le monde!</h2>
         <h3>Mon pr&#233;nom est Anthony</h3>
         <br />
         <label for="Name">De quelle nationalit&#233; &#234;tes-vous?</label>
         <br />
         <input id="Name" name="Name" type="text" value="" />
         <script type="text/javascript">
            var MyApp = MyApp || {};
            MyApp.Resources = MyApp.Resources || {};
            MyApp.Resources = { "HelloWordKey" : "Bonjour le monde!",
                                "MyNameKey" : "Mon prénom est Anthony",
                                "EnterYourNationalityKey" : "De quelle nationalité êtes-vous?"
                              }
         </script>
         <script type="text/javascript">
            alert(MyApp.Resources.HelloWordKey + "\n" + MyApp.Resources.MyNameKey);
         </script> </body> </html>

 

Exemple 2: l’allemand comme langue par défaut sur le navigateur (comme l’allemand n’est pas géré, l’anglais sera utilisé comme langue par défaut) 

decorative

decorative

Code source:

<!DOCTYPE html>
   <html>
      <head>
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>MyApp</title>
      </head>
      <body>
         <h2>Hello World!</h2>
         <h3>My name is Anthony</h3>
         <br />
         <label for="Name">What&#39;s your nationality</label>
         <br />
         <input id="Name" name="Name" type="text" value="" />
         <script type="text/javascript">
            var MyApp = MyApp || {};
            MyApp.Resources = MyApp.Resources || {};
            MyApp.Resources = { "HelloWordKey" : "Hello World!",
                                "MyNameKey" : "My name is Anthony",
                                "EnterYourNationalityKey" : "What's your nationality"
                              }
         </script>
         <script type="text/javascript">
            alert(MyApp.Resources.HelloWordKey + "\n" + MyApp.Resources.MyNameKey);
         </script>
      </body>
</html>

Exemple 3 : le français par défaut dans le navigateur et sélectionné comme langue par défaut et l’utilisateur sélectionne l’anglais par défaut dans un formulaire (stocké dans un cookie après la sélection)

decorative

Resélectionner le français par action dans le formulaire:

decorative

 

J’espère que cet article vous a aidé à faciliter la gestion de la traduction de votre application .NET. ;)

Partager l’article :