Internationalisation d'une application ASP.Net
grâce à une base de données
Ou comment permettre à vos visiteurs de bien comprendre ce que votre site raconte.
Date de publication : 30 septembre 2010 , Date de mise à jour : 9 octobre 2010
Par
Immobilis (accueil)
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
2 commentaires 
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.
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
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
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
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>
</summary>
public class StructuralTranslationsProvider : IDisposable
{
private bool _disposed;
private StructuralTraductionsDataContext db = new StructuralTraductionsDataContext();
<summary>
</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>
</summary>
~StructuralTranslationsProvider()
{
Dispose(false);
}
<summary>
</summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
<summary>
</summary>
<param name="disposing"><c></c>
<c></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>
</summary>
<param name="key"></param>
<param name="culture"></param>
<returns></returns>
public static string Load(string key, CultureInfo culture)
{
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
Translation t = db.Load().Where(x => x.Key == key && x.Culture.Code == culture.IetfLanguageTag).FirstOrDefault();
if (t != null)
{
return t.Text;
}
else
{
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>
|
| 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>
</summary>
<param name="httpContext"></param>
<returns></returns>
public static CultureInfo GetTranslationCulture(HttpContext httpContext)
{
try
{
return new CultureInfo(
httpContext.Request.UserLanguages[0].ToString());
}
catch
{
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.
Le résultat se présente ainsi:
| Code des méthode Load(string, CultureInfo) et LoadByKeyAndCulture(StructuralTraductionsDataContext, string, Culture) la classe StructuralTranslationsProvider. |
<summary>
</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>
</summary>
<param name="key"></param>
<param name="culture"></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>
</summary>
<param name="key"></param>
<param name="culture"></param>
<returns></returns>
public static string Load(string key, CultureInfo culture)
{
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
Translation t = db.Load(key, culture);
if (t != null)
{
return t.Text;
}
else
{
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>
</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>
</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>
</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>
<c></c>
</summary>
public static void LoadTranslationsInMemory()
{
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
var c = db.Load().GroupBy(x => x.Culture.Code);
foreach (var i in c)
{
if (!translations.ContainsKey(i.Key))
{
Hashtable table = new Hashtable(i.ToDictionary(y => y.Key));
translations.Add(i.Key, table);
}
else
{
}
}
}
}
|
Voici l'appel au démarrage du site.
| Code de la méthode Global.Application_Start() |
protected void Application_Start(object sender, EventArgs e)
{
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>
</summary>
<param name="key"></param>
<param name="culture"></param>
<returns></returns>
public static string Load(string key, CultureInfo culture)
{
if (translations.ContainsKey(culture.IetfLanguageTag))
{
Hashtable dico = translations[culture.IetfLanguageTag] as Hashtable;
if (dico != null && dico.ContainsKey(key))
{
return (dico[key] as Translation).Text;
}
else
{
using (StructuralTranslationsProvider db = new StructuralTranslationsProvider())
{
Translation t = db.Load(key, culture);
if (t != null)
{
(translations[culture.IetfLanguageTag] as Hashtable).Add(t.Key, t);
return t.Text;
}
else
{
return null;
}
}
}
}
else
{
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>
</summary>
private static Regex r = new Regex(@"##(?<key>[\d]{6})(?<text>[-]{1}[\w\D]*?)##",
RegexOptions.Compiled);
<summary>
</summary>
<param name="html"></param>
<param name="culture"></param>
<returns></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>
</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>
</summary>
public class BasePage : System.Web.UI.Page
{
<summary>
</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


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.