Avancé

[ASP.NET Core] Localisation #1

ASP.NET logoDans ce billet, je vous propose de découvrir le modèle de programmation utilisé pour gérer l’internationalisation d’une application ASP.NET Core. Élément important dans le développement d’une application Web moderne, la gestion de la localisation a été totalement modifiée et nécessite une adaptation des développeurs habitués aux précédentes versions d’ASP.NET.

Attention, cet article a été écrit à partir de la beta 8 d’ASP.NET Core. Les APIs décrites ici sont encore sujettes à changement d’ici la release finale de la plate-forme.

Le mécanisme fondamental de localisation

Tout d’abord, analysons un premier paquet : Microsoft.Framework.Localization.Abstractions. Celui-ci contient deux interfaces particulièrement intéressantes : IStringLocalizerFactory et IStringLocalizer. Comme son nom l’indique, le but de la première interface est tout simplement d’obtenir une implémentation de la seconde, à partir d’un type donné. Techniquement, par défaut, la seconde surcharge ne semble pas être utilisée par ASP.NET Core.

public interface IStringLocalizerFactory
{
    IStringLocalizer Create(Type resourceSource);
    IStringLocalizer Create(string baseName, string location);
}

public interface IStringLocalizer
{
    LocalizedString this[string name] { get; }
    LocalizedString this[string name, params object[] arguments] { get; }
    IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures);
    IStringLocalizer WithCulture(CultureInfo culture);
}

La méthode Create de la Factory prend un type en paramètre, dont le nom va être utilisé pour localiser la source des ressources.

Un second paquet, Microsoft.Framework.Localization fournit différentes implémentations correspondant à ces deux interfaces. Ainsi, on y trouve les classes ResourceManagerStringLocalizerFactory et ResourceManagerStringLocalizer. Elles correspondent au mécanisme historique de gestion des ressources dans les applications .NET (basé sur les fichiers resx).

L’enregistrement de ces implémentations standards se fait au travers de la méthode d’extension AddLocalization, qui vient s’ajouter à l’interface IServiceCollection.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseIISPlatformHandler();
    }
}

Le paquet Microsoft.Framework.Localization utilise la librairie OptionsModel. Sa configuration peut se faire au travers d’un objet de type LocalizationOptions. Cette classe expose simplement une propriété ResourcesPath de type string, qui est utilisée par défaut par la classe ResourceManagerStringLocalizerFactory pour rechercher les fichiers ressources demandés.

Notez que l’utilisation d’une factory permet de remplacer facilement la lecture des ressources depuis les fichiers resx par un autre mécanisme maison, une lecture depuis une base de données par exemple. Dans les versions précédentes d’ASP.NET, remplacer le mécanisme de localisation standard par un mécanisme nécessite la réécriture d’une grande partie de l’application. Avec ASP.NET Core, c’est simplement l’enregistrement de cette factory dans le conteneur d’IoC qui change.

L’exemple suivant fait appel au conteneur IoC pour récupérer une implémentation de IStringLocalizer. Puisque l’on va travailler ici avec des fichiers de ressources resx classiques, il est nécessaire de faire la résolution sur le type IStringLocalizer de T, où le nom T doit correspondre au nom du fichier de ressources. Par défaut, IStringLocalizer dans sa version non générique n’est pas enregistré dans le conteneur IoC.

Notez qu’avec la version actuelle d’ASP.NET Core (bêta 8), les outils de développement web n’offrent pas la possibilité d’ajouter un fichier resx au projet. Le plus simple consiste donc à créer une autre application, sans ASP.NET Core, une application console par exemple, et d’y générer le fichier resx. Il suffit ensuite d’aller copier-coller le fichier vers le dossier de l’application ASP.NET Core. Si vous constatez des soucis dans la génération du fichier .cs correspondant au .resx que vous venez d’ajouter à l’application ASP.NET, fermez puis réouvrez la solution et tout devrait rentrer dans l’ordre.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization();
    }

    public void Configure(IApplicationBuilder app, IStringLocalizer<Startup> stringLocalizer)
    {
        app.UseIISPlatformHandler();

        var value = stringLocalizer["String2"];
    }
}

Notez que la clé passé ici, string2, ne doit pas être vu uniquement comme une clé. En fait, avec cette approche de la localisation, si une clé n’existe pas dans la source de ressources correspondant à la culture courante, ou dans une des sources utilisées en fallback, alors c’est la clé elle-même qui est utilisée comme ressource.

Ainsi, l’approche proposée par Microsoft est de pouvoir utiliser directement la valeur de la culture par défaut en tant que clé (sans aucune limite sur la longueur).

L’exemple ci-dessous est une donc variante acceptable.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization();
    }

    public void Configure(IApplicationBuilder app, IStringLocalizer<Startup> stringLocalizer)
    {
        app.UseIISPlatformHandler();

        var value = stringLocalizer["Créer un compte"];
    }
}

Bien sûr, le développeur est libre de continuer à utiliser l’approche historique où le code n’utilise qu’une clé de ressource et la source de la culture par défaut contient la valeur par défaut de la clé.

Il est probable que, à terme, des outils pour générer des fichiers de ressources en analysant le code fasse leur apparition. Ce genre d’outils existe déjà pour Orchard.

Le mécanisme orienté web

Vient ensuite le paquet Microsoft.AspNet.Localization. Ce paquet amène un middleware qui peut être enregistré via la méthode UseRequestLocalization.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization();
    }

    public void Configure(IApplicationBuilder app, IStringLocalizer<Startup> stringLocalizer)
    {
        app.UseRequestLocalization();

        app.UseIISPlatformHandler();

        var value = stringLocalizer["Créer un compte"];
    }
}

Ce middleware repose sur la feature IRequestCultureFeature. La notion de Feature est un concept nouveau apporté par ASP.NET Core. La plupart des features présentes par défaut sont définies par le paquet Microsoft.AspNet.Http.Features. La classe HttpContext, que représente l’ensemble des informations du contexte courant de l’exécution d’une requête (la requête en tant que telle, la réponse, la session utilisateur, etc.), porte une collection de features. Techniquement, les features ne contiennent pas de logique, elles permettent uniquement de placer des éléments en cache pour la requête courante. Les développeurs peuvent considérer la notion de feature comme l’équivalent de la collection HttpContext.Items mais avec une approche typée.

L’implémentation du middleware repose également sur une notion de Provider, représentée par l’interface IRequestCultureProvider. Par défaut, trois implémentations de cette interface sont fournies :

  • CookieRequestCultureProvider. Il recherche un cookie dans le nom par défaut est ASPNET_CULTURE.
  • QueryStringRequestCultureProvider. Il recherche dans l’url courante une querystring dont la clé est nommée culture.
  • AcceptLanguageHeaderRequestCultureProvider. Il se base sur l’en-tête HTTP standard Accept-Language.

La librairie repose sur OptionsModel pour définir ses propres paramètres. Ainsi, il est possible de modifier cette liste de providers par le biais de la classe RequestLocalizationOptions.

Dans son traitement, le middleware va itérer sur les différents providers. Le premier qui retourne une valeur non nulle l’emporte et les autres ne seront même pas appelés. Le résultat est utilisé pour définir la culture du thread de la requête courante. Il est également possible de retrouver la culture sélectionnée au travers de la propriété RequestCulture de l’interface IRequestCultureFeature.

Ce fonctionnement est en fait assez similaire à ce qui pouvait être fait automatiquement par ASP.NET dans les précédentes versions via l’attribut enableClientCulture.

var requestCultureFeature = httpContext.Features.Get<IRequestCultureFeature>();
var requestCulture = requestCultureFeature.RequestCulture;

Avec l’enregistrement du middleware, en faisant une requête sur le site web, l’implémentation de IStringLocalizer injectée dans la méthode Configure va s’adapter automatiquement à la culture détectée par le middleware.

Notez que l’ordre de ces fournisseurs est extrêmement important. Il permet de décrire un fonctionnement du plus spécifique au plus générique. Ainsi, il est facile pour votre application de poser un cookie chez le client, ou de modifier les liens pour y ajouter une culture, plutôt que de modifier la valeur de l’en-tête Accept-Language qui est contrôlée par le navigateur internet du client.

Le mécanisme orienté MVC

L’étape suivante consiste à ajouter le paquet Microsoft.AspNet.Mvc.Localization. Celui-ci apporte une première méthode importante qui va ajouter de nouveaux services dans le conteneur IoC : AddViewLocalization. Attention, cette méthode d’extension est à utiliser sur les types MvcBuilder ou MvcCoreBuilder et pas directement sur l’interface IServiceCollection.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization();

        services.AddMvc().AddViewLocalization();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestLocalization();

        app.UseMvcWithDefaultRoute();

        app.UseIISPlatformHandler();
    }
}

A présent, les contrôleurs peuvent automatiquement se faire injecter des instances de IHtmlLocalizer. Cette interface fonctionne sur le même principe que IStringLolizer sur que les chaînes de caractères retournées ne sont pas des string mais des HtmlString. Cette classe sert de flag auprès de Razor pour ne pas encoder le contenu de la chaîne de caractères.

public class HomeController : Controller
{
    private readonly IHtmlLocalizer<HomeController> _htmlLocalizer;

    public HomeController(IHtmlLocalizer<HomeController> htmlLocalizer)
    {
        _htmlLocalizer = htmlLocalizer;
    }

    public IActionResult Index()
    {
        return View("Index", _htmlLocalizer["<b>Créer un compte</b>"]);
    }
}

Une seconde méthode d’extension aux types MvcBuilder et MvcCoreBuilder est disponible : AddDataAnnotationsLocalization. Elle est amenée par le paquet Microsoft.AspNet.Mvc.DataAnnotations.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization();

        services.AddMvc().AddViewLocalization()
                         .AddDataAnnotationsLocalization();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestLocalization();

        app.UseMvcWithDefaultRoute();

        app.UseIISPlatformHandler();
    }
}

Cette méthode permet simplement de rendre utilisable le IStringLocalizerFactory par les différents attributs d’annotations de modèle, sans plus de paramétrages.

Conclusion

Ce billet est une introduction à la localisation d’une application avec ASP.NET Core. Il couvre les éléments de bases et illustre le changement de direction pris depuis les précédentes versions d’ASP.NET. Cependant, il reste quelques éléments avancés, tels que la localisation des vues ou l’extensibilité que je couvrirai dans un autre billet très prochainement.

Nombre de vue : 388

COMMENTAIRES 3 commentaires

  1. Rakrouki dit :

    Bonjour
    Merci beaucoup pour cet article , j’ai une question comment gérer la localisation au niveau des layaout et des partial pages? en injectant IViewLocalizer dans mes views , j’arrive pas à récupérer le fichier resource de mon layout
    Merci

  2. Léonard LABAT dit :

    Bonjour,

    Quelle version du runtime et de la lib localization utilisez vous ?

    Merci

  3. rakrouki dit :

    Bonjour
    je suis en RC1:
    “Microsoft.AspNet.Localization”: “1.0.0-rc1-final”,
    “Microsoft.AspNet.Mvc.Localization”: “6.0.0-rc1-final”,
    “Microsoft.Extensions.Localization”: “1.0.0-rc1-final”,
    “Microsoft.Extensions.Localization.Abstractions”: “1.0.0-rc1-final”,
    “Microsoft.Extensions.Globalization.CultureInfoCache”: “1.0.0-rc1-final”

    et j’inject ça dans mon ViewImports :
    @inject IViewLocalizer LocString
    et en inject ça dans mon layout:
    @inject IHtmlLocalizer Global
    ( Global est celui du ressources du layout), il m’affiche:
    Error CS0053 Inconsistent accessibility: property type ‘IHtmlLocalizer’ is less accessible than property ‘_Page_Views_Shared__LayoutBack_cshtml.Global’

    peut être c’est pas la bonne approche ?
    Merci

AJOUTER UN COMMENTAIRE