Internationalisation d'une application ASP.Netgrâce à une base de données

Ou comment permettre à vos visiteurs de bien comprendre ce que votre site raconte.

Immobilis

Cet article traite d'une méthode d'internationalisation d'une application ASP.Net, d'un site Internet, grâce à une base de données. Il décrit une méthode parmi d'autres pour rendre votre site multilingue Commentez

- Titre : Internationalisation d'une application ASP.Netgrâce à une base de données

- Auteur : Immobilis

- Parution : 30 septembre 2010

- Mise à jour : 9 octobre 2010

- Licence : Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

- I. Présentation

- II. La base de données

- III. Le code

- III-A. Solution

- III-A-1. Projet de données et modèle : Data

- III-A-2. Projet logique métier : Business

- III-A-3. Projet interface utilisateur : WebApplication

- III-B. Mise en pratique

- III-B-1. Affichage des versions par du code "en dur"

- III-B-2. Affichage des versions par du code dynamique

- III-B-3. Les formats de dates, nombres et monnaies

- IV. Améliorations et optimisations

- IV-A. Compilation des requêtes LINQ

- IV-B. Diminution du nombre de requêtes vers la base de données

- IV-B-1. Mise en mémoire

- IV-B-2. Exploitation des traductions en mémoire

- IV-C. Traduction en une seule passe avec les expressions régulières

- IV-C-1. Principe

- IV-C-2. Remplacement de plusieurs occurrences d'un modèle dans une chaîne de caractères grâce à un MatchEvaluator

- IV-C-3. Surcharge de la méthode "Render"

- IV-C-4. Création d'une classe de base pour factoriser la surcharge de la méthode "Render"

- V. Conclusion

- Remerciements

I - Présentation

La gestion de la culture est souvent mal prise en considération dans la réalisation d'une application Internet. Pourtant, elle est primordiale quand on sait qu'un site peut être consulté depuis n'importe quel pays.

Cependant, est-ce encore nécessaire quand on constate l'efficacité des outils de traduction en ligne en temps réel? Eh bien, à mon avis, tout à fait. Il existe un vocabulaire qui correspond à chaque métier et qui échappe encore totalement à ces systèmes. Ainsi, les images, les métaphores sont totalement incompréhensibles pour eux. Tout le problème est là d'ailleurs, ces automates ne comprennent pas. Pas encore.

Afin de se rapprocher de ses internautes, une application Web doit encore être traduite par des humains. Parmi toutes les méthodes que je connais (Internationalisation d'un site web avec Visual Studio 2005 sans une seule ligne de code par Jérôme Lambert, Internationalisation d'une application asp.NET par Ditch, Localisation d'une application Silverlight par Benjamin Roux), celle que je préfère est sans conteste l'utilisation d'une base de données. Une base de données est beaucoup plus facile à maintenir que des fichiers de ressources éparpillés dans un site.

Il y a au moins deux choses à internationaliser. Premièrement, les traductions structurelles du site. Il s'agit par exemple des textes des boutons, des messages, des labels. Deuxièmement, les traductions liées aux enregistrements, aux objets, présentés par le site. Ce peut être des descriptifs d'un produit, les messages d'un blog, des articles d'un journal en ligne.

Dans cet article, nous allons aborder les traductions structurelles.

II - La base de données

Elle est constituée simplement de deux tables.

Table [Culture] :

- [CultureId] : identifiant de l'enregistrement ;

- [Code] : le code du pays constitué par le code de la langue et de la région sur cinq caractères. Par exemple, "fr-FR" pour le Français de France et "fr-BR" pour nos amis de Bretagne. Il y a de nombreuses normes à ce sujet. Voici néanmoins deux liens qui résument bien l'état des lieux Using Language Identifiers (RFC 3066) et List of Culture Codes.

Table [Translations] :

- [TranslationId] : identifiant de l'enregistrement ;

- [CultureId] : clef étrangère ;

- [Key] : clef métier sur six caractères. Identifiant métier permettant d'associer les traductions d'un même texte. Ce pourrait aussi être un type numérique ;

- [Text] : la traduction. Les huit mille caractères sont le nombre maximum supporté par SQL Server 2005 pour ce type de champ.

Il y a une clef unique entre l'identifiant de la culture et la clef métier. Elle pourrait suffire et nous épargner l'utilisation d'une clef privée. Cependant, la gestion de la table sera plus rapide à mettre en place avec cette clef et un projet "Dynamique Data".

Script SQL de génération des tables

USE [StructuralTranslations]
GO
/****** Object:  Table [dbo].[Cultures]    Script Date: 10/01/2010 12:27:08 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Cultures](
[CultureId] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Cultures_CultureId]  DEFAULT (newid()),
[Code] [char](5) NOT NULL,
CONSTRAINT [PK_Cultures] PRIMARY KEY CLUSTERED
(
[CultureId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON,
ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
/****** Object:  Table [dbo].[Translations]    Script Date: 10/01/2010 12:27:08 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Translations](
[TranslationId] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Dictionary_DictionaryId]  DEFAULT (newid()),
[CultureId] [uniqueidentifier] NOT NULL,
[Key] [varchar](6) NOT NULL,
[Text] [varchar](max) NOT NULL,
CONSTRAINT [PK_Dictionary] PRIMARY KEY CLUSTERED
(
[TranslationId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
CREATE UNIQUE NONCLUSTERED INDEX [Key] ON [dbo].[Translations]
(
[Key] ASC,
[CultureId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF,
ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
GO
/****** Object:  ForeignKey [FK_Dictionary_Cultures]    Script Date: 10/01/2010 12:27:08 ******/
ALTER TABLE [dbo].[Translations]  WITH CHECK ADD  CONSTRAINT [FK_Dictionary_Cultures] FOREIGN KEY([CultureId])
REFERENCES [dbo].[Cultures] ([CultureId])
GO
ALTER TABLE [dbo].[Translations] CHECK CONSTRAINT [FK_Dictionary_Cultures]
GO

Il ne sera pas utile de charger la base de données. Dans notre exemple, nous choisirons deux cultures (française et anglaise) et deux traductions.

Voici le contenu de la table [Culture]

Voici celui de la table [Translations]

La base est prête, il faut maintenant implémenter le code nécessaire pour l'exploiter.

III - Le code

III-A - Solution

J'utiliserai Visual Studio 2010 et ciblerai le Framework 4. Nous ferons une solution avec trois projets : données/modèle, métier, interface utilisateur. LINQ sera évidement de la partie.

III-A-1 - Projet de données et modèle : Data

Ajoutez à la solution un projet librairie de classes. Ajoutez un composant LINQ to SQL et nommez-le "StructuralTranslations.dbml". Ajoutez les tables précédemment créées.

Afin de préserver une certaine indépendance de notre code avec la source de données, nous allons créer un fournisseur. Ajoutez une classe nommée "StructuralTranslationsProvider.cs". Voici une première version de son code :

Code de la classe StructuralTranslationsProvider

using System;
using System.Linq;

namespace Data
{
/// <summary>
/// Classe chargée de récupérer les données en base
/// </summary>
public class StructuralTranslationsProvider : IDisposable
{
private bool _disposed;
private StructuralTraductionsDataContext db = new StructuralTraductionsDataContext();

/// <summary>
/// Retourne la totalité des traductions
/// </summary>
/// <returns></returns>
public IQueryable<Translation> Load()
{
return from t in db.Translations
join c in db.Cultures on t.CultureId equals c.CultureId
select t;
}

#region IDisposable Membres

/// <summary>
/// Destructeur de la classe. Permet de libérer les ressources non gérées par le code selon un processus automatique.
/// Permet de réaliser toutes les opérations de nettoyage nécessaires avant que les ressources
/// ne soient réclamées par le garbage collector
/// </summary>
~StructuralTranslationsProvider()
{
Dispose(false);
}

/// <summary>
/// Réalise les opérations définies par le code pour detruire les objets,
/// libérer ou réinitialiser les ressources non gérées.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Réalise les opérations définies par le code pour detruire les objets,
/// libérer ou réinitialiser les ressources non gérées.
/// </summary>
/// <param name="disposing">Spécifier <c>true</c> quand la méthode est
/// appelé directement ou non par le code. Sinon spécifier <c>false</c></param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
db.Dispose();
db = null;
_disposed = true;
}
}

#endregion
}
}

La classe implémente l'interface "IDisposable". Cela est rendu nécessaire car nous y déclarons une seule instance du contexte de données. L'étendue de cet objet est la classe entière. Or un contexte de données étant un objet "disposable" il faut aussi rendre notre classe "disposable" (cf. CA2213: Disposable fields should be disposed). Cependant, cela ne serait pas nécessaire si nous utilisions notre contexte de données dans un bloc "using". Je ne le fais pas afin de ne pas créer trop d'objets. Cela améliore les performances.

III-A-2 - Projet logique métier : Business

Ajoutez à la solution un projet librairie de classes. Ajoutez une classe nommée "StructuralTranslationsManager.cs".

Référencez le projet "Data".

Le code de la méthode de chargement est le suivant :

Code de la méthode StructuralTranslationsManager.Load(string, Culture)

/// <summary>
/// Chargement d'une traduction en fonction de la clef métier et la culture
/// </summary>
/// <param name="key">la clef métier</param>
/// <param name="culture">la culture</param>
/// <returns>la traduction</returns>
public static string Load(string key, CultureInfo culture)
{
// Allons chercher en base de données
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
Translation t = db.Load().Where(x => x.Key == key && x.Culture.Code == culture.IetfLanguageTag).FirstOrDefault();
// La clef existe
if (t != null)
{
return t.Text;
}
else // La clef n'existe pas en base
{
return null;
}
}
}

Nous utilisons une simple requête LINQ. Cette méthode admet deux paramètres :

- key : la clef de la traduction ;

- culture : la version linguistique demandée.

La classe est statique comme souvent dans une couche métier. Une classe statique n'étant pas "disposable", elle utilise le fournisseur dans un bloc "using".

III-A-3 - Projet interface utilisateur : WebApplication

Ajoutez à la solution un projet application Web vide. Référencez les projets "Data" et "Business".

Ajoutez-lui une page web que vous définirez comme page de démarrage. Ajoutez au fichier "aspx" un contrôle "Label". Laissez l'identifiant automatique "Label1".

Le code obtenu est le suivant :

Code de la page Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication.Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server" Text="Label1"></asp:Label>
</div>
</form>
</body>
</html>

Le code behind :

Code de la classe Default

using System;

namespace WebApplication
{
public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

}
}
}

Tous les éléments sont maintenant en place, la solution devrait ressembler à ceci :

Tout est maintenant prêt pour réaliser nos premiers essais.

III-B - Mise en pratique

N'hésitez pas à consulter la FAQ à ce sujet.

III-B-1 - Affichage des versions par du code "en dur"

Dans le code "behind", affectez la valeur de retour de la méthode "Load" du gestionnaire des traductions à la propriété "Text" du label. Les paramètres à passer sont la clef "000001" et la culture française "fr-FR". Le code obtenu est :

Code de la classe Default. Assignation de la culture française.

protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = StructuralTranslationsManager.Load("000001", CultureInfo.CreateSpecificCulture("fr-FR"));
}

Lancez l'exécution du projet. La page s'affiche ainsi :

La version obtenue est bien la version française.

Modifiez le code pour passer en paramètre la culture anglaise "en-GB".

Code de la classe Default. Assignation de la culture anglaise.

protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = StructuralTranslationsManager.Load("000001", CultureInfo.CreateSpecificCulture("en-GB"));
}

Lancez l'exécution du projet. La page s'affiche ainsi :

La version obtenue est bien la version anglaise.

Les mêmes résultats sont obtenus en plaçant un scriptlet d'expression liée dans le code de la page "aspx" ainsi :

Code de la page Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication.Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server" Text='<%# Business.StructuralTranslationsManager.Load("000001",
System.Globalization.CultureInfo.CreateSpecificCulture("fr-FR")) %>'></asp:Label>
</div>
</form>
</body>
</html>

Ne pas oublier d'appeler la liaison des données (cf. Scriptlet expressions liées).

Code de la classe Default

protected void Page_Load(object sender, EventArgs e)
{
this.DataBind();
}

III-B-2 - Affichage des versions par du code dynamique

Afin de rendre le passage du paramètre culture dynamique, il conviendra de se baser sur l'un des moyens suivants :

- la culture de l'interface (celle des menus) : Thread.CurrentThread.CurrentUICulture ;

- la langue paramétrée dans les options : HttpContext.Current.Request.UserLanguages[0].ToString() ;

- les URL : http://www.<domaine>.<tld>/fr-FR/ et http://www.<domaine>.<tld>/en-GB/ ;

- sélection manuelle dans une liste déroulante ou bien des drapeaux.

Dans tous les cas, on remplacera, partout où il se trouve, le paramètre "culture" passé à la méthode "StructuralTranslationsManager.Load". Une bonne solution est de faire appel à une nouvelle méthode pour cacher/encapsuler la logique permettant de définir la langue. Par exemple, comme ceci en ajoutant cette méthode à la classe "StructuralTranslationsManager" :

Code de la méthode StructuralTranslationsManager.GetTranslationCulture(HttpContext)

/// <summary>
/// Renvoi la culture correspondant à l'internaute en fonction du contexte http
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public static CultureInfo GetTranslationCulture(HttpContext httpContext)
{
try
{
// Culture en tenant compte de la première langue définie dans le navigateur
return new CultureInfo(
httpContext.Request.UserLanguages[0].ToString());
}
// Des erreurs peuvent se produire si la région n'est pas spécifiée dans
// le code de la culture (ex: [fr] au lieu de [fr-FR])
catch
{
// Culture en tenant compte du thread
return Thread.CurrentThread.CurrentUICulture;
}
}

Le code "behind" de la page Web devient :

Code de la classe Default

protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = StructuralTranslationsManager.Load("000001", StructuralTranslationsManager.GetTranslationCulture(HttpContext.Current));
}

Voici donc exposée la logique de base qui vous permettra de rendre rapidement votre site multilingue. Nous allons essayer d'aller un peu plus loin dans les paragraphes suivants.

Nous verrons comment traduire la totalité de la page en une seule passe, mais aussi comment optimiser les requêtes LINQ et le nombre d'appels à la base de données.

III-B-3 - Les formats de dates, nombres et monnaies

Quand on parle d'internationalisation, il ne s'agit pas seulement de langue. Le formatage des données numériques est aussi à considérer. Il faut d'ailleurs s'y prendre très tôt pour éviter de refactoriser beacoup de code.

Parmi les outils indispensables du développeur, il y a FxCop. Ce dernier relève les infractions aux bonnes pratiques de codage. À mon humble avis, s'il en est une qu'il faut impérativement résoudre, c'est bien celle-ci : CA1304: Specify CultureInfo. Cette règle indique que, si une méthode comporte une surcharge acceptant comme paramètre une information sur la culture, il faut la spécifier.

Pourquoi ? Si jamais votre code récupère cette date "01-02-2010" et que vous ne spécifiez pas la culture de son format, il adoptera un comportement par défaut et fera potentiellement une erreur d'interprétation lors de la conversion en DateTime. De même, si votre code reçoit ce nombre "123,456" et que vous ne spécifiez pas le format, vous prenez le risque de générer une erreur de conversion.

Cela arrive plus souvent qu'on ne le croit. Par exemple, il arrive fréquemment qu'une application analyse un fichier texte, obtenu sur un site FTP, pour importer des données. Si ce fichier contient des valeurs numériques, votre code ne pourra pas se baser sur la culture du client pour interpréter les formats.

Ainsi, comment faire ? Il faut systématiquement préciser le format d'entrée des types sensibles à la culture. Ci-dessous, un mauvais exemple :

Conversion sans spécification de la culture

double num = Convert.ToDouble("1,234.5");

Ce code provoque une erreur de type FormatException. Voici un exemple de ce qu'il faut faire :

Conversion avec spécification de la culture

double num = Convert.ToDouble("1,234.5", CultureInfo.CreateSpecificCulture("en-GB"));

Evidement, vous choississez la méthode la mieux adaptée pour apporter cette information.

IV - Améliorations et optimisations

Il existe évidement de nombreux moyens d'augmenter les performances de cette application. En voici quelques-uns parmi les plus importants.

IV-A - Compilation des requêtes LINQ

Les requêtes LINQ sont moins rapides que les autres. Pour nous rapprocher des performances des requêtes textuelles, il faut les compiler. On gagne en vitesse mais on perd en souplesse car les requêtes ne sont plus modifiables. C'est-à-dire que si on veut obtenir un jeu de résultats différent (filtré, trié, etc.), la nouvelle requête LINQ s'appliquera sur le jeu de résultats déjà renvoyé.

Dans le cas des traductions, ce n'est pas très important. Nous allons créer une requête compilée admettant les deux paramètres de clef et de culture.

Je passe sous silence la syntaxe des requêtes compilées.

Si vous souhaitez plus de détails sur les fonctions déléguées vous pouvez lire cet article: Introduction aux délégués en C#.

Le résultat se présente ainsi:

Code des méthode Load(string, CultureInfo) et LoadByKeyAndCulture(StructuralTraductionsDataContext, string, Culture) la classe StructuralTranslationsProvider.

/// <summary>
/// Requête LINQ compilée pour récupérer une traduction selon sa clef et sa culture
/// </summary>
private Func<StructuralTraductionsDataContext, string, CultureInfo, IQueryable<Translation>> LoadByKeyAndCulture = CompiledQuery.Compile(
(StructuralTraductionsDataContext db, string key, CultureInfo culture) => from t in db.Translations
join c in db.Cultures on t.CultureId equals c.CultureId
where t.Key == key && c.Code == culture.IetfLanguageTag
select t
);

/// <summary>
/// Retourne un enregistrement de la table de traductions
/// </summary>
/// <param name="key">la clef métier correspondant à une traduction</param>
/// <param name="culture">la culture associée à la traduction</param>
/// <returns></returns>
public Translation Load(string key, CultureInfo culture)
{
if (!string.IsNullOrEmpty(key) && culture != null)
{
return LoadByKeyAndCulture(db, key, culture).FirstOrDefault();
}
else
{
return null;
}
}

Il faut retenir :

- la nécessité de créer une fonction déléguée à laquelle on passe le contexte et les paramètres de clef et de culture ;

- le changement du type en retour de la méthode "Load". Il passe de "IQueryable<Translation>" à "Translation".

La méthode "StructuralTranslationsManager.Load" devient donc:

Code de la méthode StructuralTranslationsManager.Load(string, Culture)

/// <summary>
/// Chargement d'une traduction en fonction de la clef métier et la culture
/// </summary>
/// <param name="key">la clef métier</param>
/// <param name="culture">la culture</param>
/// <returns>la traduction</returns>
public static string Load(string key, CultureInfo culture)
{
// Allons chercher en base de données
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
Translation t = db.Load(key, culture);
// La clef existe
if (t != null)
{
return t.Text;
}
else // La clef n'existe pas non plus en base
{
return null;
}
}
}

IV-B - Diminution du nombre de requêtes vers la base de données

Quand bien même la requête LINQ est compilée, envoyer une requête à la base de données pour chaque traduction n'est pas très optimisé. La connexion avec la base, le transit des données sur le réseau sont des opérations qui diminuent les performances d'une application. Dès lors, nous allons nous en affranchir en stockant la totalité de la table dans la mémoire du processus de l'application grâce à un champ statique. Nous allons répartir les traductions par culture dans des "Hashtable" imbriquées.

IV-B-1 - Mise en mémoire

Il faut tout d'abord ajouter une méthode dans le fournisseur de données qui renvoie la totalité de la table des traductions. Nous la nommerons "Load". Ce sera une surcharge. En voici le code avec la requête compilée :

Code des méthode Load() et LoadAll(StructuralTraductionsDataContext) la classe StructuralTranslationsProvider.

/// <summary>
/// Requête LINQ compilée pour récupérer la totalité de la table
/// </summary>
private Func<StructuralTraductionsDataContext, IQueryable<Translation>> LoadAll = CompiledQuery.Compile(
(StructuralTraductionsDataContext db) => from t in db.Translations
join c in db.Cultures on t.CultureId equals c.CultureId
select t
);

/// <summary>
/// Retourne la totalité des traductions
/// </summary>
/// <returns></returns>
public IQueryable<Translation> Load()
{
return LoadAll(db);
}

Un champ statique fonctionne comme une variable d'application en ce sens qu'il est commun à toutes les sessions des internautes.

Puis, il faut ajouter le champ statique qui nous servira de "cache" dans la classe "StructuralTranslationsManager". Nous le nommerons "translations" :

StructuralTranslationsManager: déclaration du champ statique privé permettant la mise en cache des traductions.

/// <summary>
/// Champ stockant les traductions regroupées selon la culture
/// </summary>
private static Hashtable translations = new Hashtable();

Afin de la remplir, nous allons également créer une nouvelle méthode qui sera appelée au moment du démarrage de l'application dans le "Global.asax". Voici son code:

Code de la méthode StructuralTranslationsManager.LoadTranslationsInMemory().

/// <summary>
/// Méthode permettant de charger toutes les traductions dans la <c>Hashtable</c>
/// </summary>
public static void LoadTranslationsInMemory()
{
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
// Regroupement des enregistrements selon leur culture
var c = db.Load().GroupBy(x => x.Culture.Code);
// Pour chaque culture
foreach (var i in c)
{
// Si la culture n'existe pas dans la Hashtable
if (!translations.ContainsKey(i.Key))
{
// On crée un dictionnaire trié de toutes les traductions liées à la culture en cours de traitement.
// La clef du dictionnaire est la clef métier de la traduction
Hashtable table = new Hashtable(i.ToDictionary(y => y.Key));
// On ajoute le dictionnaire à la table Hashtable
translations.Add(i.Key, table);
}
else
{
// Le dictionnaire existe déjà.
}
}
}
}

Voici l'appel au démarrage du site.

Code de la méthode Global.Application_Start()

protected void Application_Start(object sender, EventArgs e)
{
// Rafraîchissement du dictionnaire de traductions
StructuralTranslationsManager.LoadTranslationsInMemory();
}

IV-B-2 - Exploitation des traductions en mémoire

Maintenant que les traductions sont en mémoire, le code vérifiera systématiquement si une traduction s'y trouve avant d'envoyer une requête à la base. Pour y arriver, il faut tout "simplement" modifier la méthode "StructuralTranslationsManager.Load(string key, CultureInfo culture)" en faisant ainsi :

Code de la méthode StructuralTranslationsManager.Load(string, Culture)

/// <summary>
/// Chargement d'une traduction en fonction de la clef métier et la culture
/// </summary>
/// <param name="key">la clef métier</param>
/// <param name="culture">la culture</param>
/// <returns>la traduction</returns>
public static string Load(string key, CultureInfo culture)
{
// Si la culture existe dans la variable "translations"
if (translations.ContainsKey(culture.IetfLanguageTag))
{
// On récupère le dictionnaire associé
Hashtable dico = translations[culture.IetfLanguageTag] as Hashtable;
// Si le dictionnaire existe et qu'il contient la clef
if (dico != null && dico.ContainsKey(key))
{
// On retourne la traduction
return (dico[key] as Translation).Text;
}
else // Le dictionnaire existe mais ne contient pas la traduction. Il peut s'agir d'une nouvelle entrée
{
// Allons chercher en base de données
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
Translation t = db.Load(key, culture);
// La clef existe
if (t != null)
{
// On l'ajoute au dictionnaire
(translations[culture.IetfLanguageTag] as Hashtable).Add(t.Key, t);
return t.Text;
}
else // La clef n'existe pas non plus en base
{
return null;
}
}
}
}
else // Le dictionnaire demandé n'existe pas.
{
return null;
}
}

Ainsi, la base de données n'est plus appelée que lorsque la clef n'est pas dans le dictionnaire.

IV-C - Traduction en une seule passe avec les expressions régulières

IV-C-1 - Principe

Il peut être assez lourd et fastidieux de répéter systématiquement l'appel de la méthode "StructuralTranslationsManager.Load". Nous allons voir comment réaliser la totalité des traductions en une seule opération grâce aux expressions régulières.

Nous allons les utiliser pour reconnaître une syntaxe spécifique dans le code Html au moment du rendu de la page. Ainsi, il suffira de placer nos clefs de traduction dans le flux Html et de les remplacer à la volée.

Pour différencier les clefs du reste du texte, nous allons les préfixer et suffixer avec des caractères qui n'ont aucune chance de se retrouver normalement dans le texte.

Nous choisirons donc les doubles dièses "##". Ce caractère est facile d'accès sur tous les claviers. De plus, il est rare de trouver deux dièses successivement. Ainsi, à chaque fois que nous voudrons placer une traduction dans les pages, nous les entourerons de doubles dièses comme ceci :

Code de l'attibut ''Text'' du label de la page Default

<asp:Label ID="Label1" runat="server" Text="##000001-Hello...##"></asp:Label>

Ou comme cela :

Assignation de l'étiquette de traduction par le code ''behind''

Label1.Text = "##000001-Hello...##";

Le texte associé à la clef permet de savoir de quelle traduction il s'agit sans avoir à aller chercher dans la table. Nous n'en tiendrons pas compte lors de la traduction.

IV-C-2 - Remplacement de plusieurs occurrences d'un modèle dans une chaîne de caractères grâce à un MatchEvaluator

L'expression régulière pour reconnaître notre modèle est la suivante :

Nous y définissons deux groupes. D'une part la clef, d'autre part le texte associé.

Dans le code ci-dessous, nous :

- déclarons l'instance statique de l'expression régulière avec une portée sur la classe ;

- utilisons la surcharge de la méthode "Replace" admettant un "MatchEvaluator" afin d'appeler la méthode "Load" en passant chaque clef trouvée. Ceci nous permettra d'obtenir la traduction correspondante.

Code de la méthode StructuralTranslationsManager.Translate(string, Culture)

/// <summary>
/// Expression régulière permettant de récupérer le texte à traduire dans une chaîne
/// </summary>
private static Regex r = new Regex(@"##(?<key>[\d]{6})(?<text>[-]{1}[\w\D]*?)##",
RegexOptions.Compiled);

/// <summary>
/// Remplace un pattern par la traduction correspondant à la clef et la culture
/// </summary>
/// <param name="html">chaîne à parser</param>
/// <param name="culture">culture représentant la langue de traduction</param>
/// <returns>la chaîne traduite</returns>
public static string Translate(string html, CultureInfo culture)
{
return r.Replace(html, new MatchEvaluator(delegate(Match m)
{
string key = m.Groups["key"].Value;
string result = Load(key, culture);
if (!string.IsNullOrEmpty(result))
{
return result;
}
else
{
return string.Format("{0}{1}", key, m.Groups["text"].Value);
}
}
));
}

IV-C-3 - Surcharge de la méthode "Render"

Un des derniers moment pour intervenir sur le code Html avant qu'il ne soit envoyé dans la réponse au client est lors de l'appel de la méthode "Render". Nous allons la surcharger pour rechercher les traductions à faire.

Il faut dans un premier temps récupérer le code Html généré par la classe de base dans un objet "HtmlTextWriter" tampon.

Le code obtenu sera "parsé" par l'expression régulière qui appellera notre méthode "StructuralTranslationsManager.Load" pour obtenir la traduction de chaque clef.

Enfin, le code Html obtenu sera écrit dans l'objet "HtmlTextWriter" utilisé par la page.

Surcharge de la méthode ''Render'' dans la classe Default

/// <summary>
/// No comment
/// </summary>
/// <param name="writer"></param>
protected override void Render(HtmlTextWriter writer)
{
using (StringWriter sw = new StringWriter())
{
using (HtmlTextWriter hw = new HtmlTextWriter(sw))
{
base.Render(hw);
}

writer.Write(StructuralTranslationsManager.Translate(sw.ToString(),
StructuralTranslationsManager.GetTranslationCulture(HttpContext.Current)));
}
}

IV-C-4 - Création d'une classe de base pour factoriser la surcharge de la méthode "Render"

Pour ceux qui créent des applications Web avec plusieurs pages, il peut être un peu lourd de devoir surcharger la méthode "Render" systématiquement.

Le plus simple, alors, est de faire hériter vos pages d'une classe de base héritant de "System.Web.UI.Page". La classe de base sera la seule à surcharger la méthode "Render". Voici le code "behind" de la page Web.

La classe Default hérite de la classe BasePage

public partial class Default : BasePage
{
protected void Page_Load(object sender, EventArgs e)
{

}
}

La classe de base :

Code de la classe de base BasePage

/// <summary>
/// Classe de base pour la factorisation de l'implémentation de la traduction
/// </summary>
public class BasePage : System.Web.UI.Page
{
/// <summary>
/// No comment
/// </summary>
/// <param name="writer"></param>
protected override void Render(HtmlTextWriter writer)
{
using (StringWriter sw = new StringWriter())
{
using (HtmlTextWriter hw = new HtmlTextWriter(sw))
{
base.Render(hw);
}

writer.Write(StructuralTranslationsManager.Translate(sw.ToString(),
StructuralTranslationsManager.GetTranslationCulture(HttpContext.Current)));
}
}
}

V - Conclusion

Cet article se base sur des techniques mises en pratique au courant de mon expérience professionnelle. Elles sont utilisées avec succès sur des sites de commerce électronique traduits en cinq langues. Ces sites reçoivent plusieurs milliers de visites par jour.

Cette expérience m'a permis de constater qu'une bonne gestion de l'internationalisation d'un site Internet est très importante. À moins de faire le choix délibéré de ne jamais rendre votre site multilingue, posez-vous dès le début la question de la gestion de la culture. Même si vous ne souhaitez pas proposer votre site en plusieurs langues, demandez-vous ce qui peut se passer quand un internaute parcourt votre site avec un navigateur canadien, anglais, allemand, etc. Les dates, les nombres, certains caractères peuvent ne pas être correctement supportés.

Remerciements

Merci beacoup à The_badger_man, Philippe Vialatte, Skyounet, tomlev pour leur aide. Merci spécial à jacques_jean pour la relecture.