Localisation

La localisation d’une application est un sujet à prendre en considération le plus tôt possible. La problématique principale est de permettre un mécanisme simple pour localiser toutes les ressources (chaînes, images, etc.). Cet article se limite à la localisation des chaînes. Il s’agit d’une approche paresseuse que j’ai pu expérimenter sur de véritables projets, et qui peut être utilisée en complément d’autres mécanismes, comme les classiques ressources localisées d’ASP.NET.


L’approche la plus courante consiste à utiliser une forme de dictionnaire de clés/valeurs pour obtenir la version localisée d’une chaîne en fonction d’un identifiant. C’est ainsi que fonctionnent les fichiers de ressources d’ASP.NET.

Le mécanisme proposé ici est de masquer l’utilisation du dictionnaire en se basant sur la chaîne elle-même. Il y a bien toujours un dictionnaire, la clé est simplement représentée par la chaîne. De plus, lorsque la chaîne localisée n’existe pas, son entité est créée et la chaîne native est retournée. L’avantage est un code plus lisible et plus rapide à développer car les traductions peuvent être renseignées dans un second temps.

Je me suis largement inspiré d’un autre projet open-source (Griffin.MvcContrib). Celui-ci ne répondait pas entièrement à mes besoins et contient des fonctionnalités hors périmètre de la localisation.

Exemple d’utilisation

À partir d’une vue Razor :

@* // Require namespace Localization.MvcProviders.Html *@
<p>@Html.Translate("Texte à traduire...")</p>

À partir d’une classe :

 private readonly StringProvider<OwnerClass> _localizer;
 public void Method()
 {
     string localizedString = _localizer.Translate("Texte à traduire...");
 }

Implémentations proposées

Cette API contient des fournisseurs de chaînes localisées à partir d’une vue MVC (via HtmlHelper), d’une classe simple ou d’un modèle de vue (MVC). Les fournisseurs s’appuient tous sur la même interface ILocalizedStringProvider, il est donc très facile d’en implémenter de nouveaux.

API

Update: Cet article décrit la version initiale de cette API. Plusieurs améliorations ont été apportées depuis (notamment le support d’une langue par défaut si une traduction n’existe pas dans la langue cible). Pour connaître l’état courant de l’API, reportez-vous sur GitHub.

L’API est constituée de deux assemblages, chacun publié sur Nuget:

Comme leur nom l’indique, le premier contient l’implémentation de base tandis que le second contient des extensions pour ASP.NET MVC 4.

Une chaîne localisée contient les métadonnées suivantes:

  • La chaîne native (codée en dur)
  • Une source (identifie l’origine de la chaîne native)
  • Une clé (identifie toutes les traductions d’une même chaîne)
  • Une version traduite
  • La langue de la version traduite

Les principaux services sont les suivants:

  • ILocalizedRepository: couche la plus basse de l’API, juste au dessus de ou équivalente à la couche d’accès aux données (typiquement SQL pour les projets d’entreprise).
  • ILocalizedStringProvider: contient la logique principale (couche intermédiaire).
  • StringProvider<T>: fournisseur à utiliser à partir d’une classe simple (couche la plus haute)
  • ModelMetaDataProvider: fournisseur pour les modèles de vue (couche la plus haute)
  • HtmlHelper.Translate: méthode d’extension pour les vues (couche la plus haute)

Dans le diagramme ci-dessous, les principaux services sont en italique. Les autres sont essentiellement des points d’extension. Les briques vertes sont implémentées dans le projet Localization.Core. Les briques violet sont implémentées dans Localization.MvcProviders. Les briques en bleu représentent des composants de l’application cible (hors périmètre de cette API).

Stockage des chaînes

La couche d’accès n’est pas incluse dans ce projet et doit donc être implémentée en fonction de vos besoins. J’ai pour habitude d’utiliser une unique base SQL pour l’ensemble des sites hébergés sur un même serveur. Mon datalayer ajoute notamment une colonne « applicationName » qui identifie le site hôte (constante au niveau du site) et « updatedOn » qui permet d’auditer la création et la mise à jour de chaînes. J’ai également une table d’audit qui permet d’enregistrer le dernier accès à chaque chaîne. La mise à jour de cette table est activable/désactivable par une simple modification dans une procédure stockée. L’audit des accès permet d’identifier les chaînes probablement obsolètes.

Mise en oeuvre

La mise en oeuvre dépend de votre application. L’exemple ci-dessous présente la configuration d’un site ASP.NET MVC avec le conteneur IoC SimpleInjector:


    // Pre-requisites:
    container.RegisterSingle<ILogger, EmptyLogger>();
    container.RegisterSingle<ITextKeyFactory, DefaultTextKeyFactory>();
    container.RegisterSingle<ITypeNameFactory, DefaultTypeNameFactory>();

    // ToDo: repository (replace this by your repository)
    //
    //    var translationsPath = HttpContext.Current.Server.MapPath("~/App_Data/translations.xml");
    //    container.RegisterSingle<ILocalizedRepository>(() => new XmlFileRepository(translationsPath));
    //

    // View localization:
    container.RegisterSingle<IViewNameFactory, DefaultViewNameFactory>();
    container.RegisterSingle<ILocalizedStringProvider>(
 () =>
 new DefaultLocalizedStringProvider( 
     // provider used for views and legacy classes localization
     container.GetInstance<ILocalizedRepository>(), 
     container.GetInstance<ITextKeyFactory>(),
     container.GetInstance<ILogger>(), 
     CultureInfo.GetCultureInfo("fr-FR") // native text is in french...
     ));

    // Legacy class localization:
    container.RegisterManyForOpenGeneric(typeof(StringProvider<>), typeof(StringProvider<>).Assembly);

    // Model metadata localization:
    container.RegisterSingle<Localization.MvcProviders.ModelMetadataProvider>(() => 
        new Localization.MvcProviders.ModelMetadataProvider(
           new DefaultLocalizedStringProvider(
               container.GetInstance<ILocalizedRepository>(),
               container.GetInstance<ITextKeyFactory>(),
               container.GetInstance<ILogger>(),
               CultureInfo.GetCultureInfo("en-US")), // Names of model properties are in english...
            container.GetInstance<ITypeNameFactory>(),
            container.GetInstance<ILogger>()));

Il faut également remplacer l’instance par défaut de ModelMetadataProvider, dans le global.asax:

ModelMetadataProviders.Current = DependencyResolver.Current.GetService<Localization.MvcProviders.ModelMetadataProvider>();

Cette implémentation prend en charge la localisation des propriétés d’un modèle de vue (basé sur le nom de la propriété ou sur son attribut DisplayName.
Enfin, il faut ajouter l’espace de nom Localization.MvcProviders.Html aux vues, typiquement dans le fichier web.config placé dans le dossier /Views:

 <system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <!-- [...] -->
        <add namespace="Localization.MvcProviders.Html" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

Ceci permet d’accéder à la méthode d’extension Html.Translate(string) à partir des vues.

Code source

Le projet est sur GitHub.

Il contient par ailleurs un exemple (application ASP.NET MVC 4) qui illustre l’utilisation des trois fournisseurs (classe simple, vue et modèle).