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 codeInternationalisation d'un site web avec Visual Studio 2005 sans une seule ligne de code par Jérôme LambertJérôme Lambert, Internationalisation d'une application asp.NETInternationalisation d'une application asp.NET par DitchDitch, Localisation d'une application SilverlightLocalisation d'une application Silverlight par Benjamin RouxBenjamin 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)Using Language Identifiers (RFC 3066) et List of Culture CodesList 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 ».
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 évidemment 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 :
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 disposedDisposable 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 :
///
<
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 :
<%@ 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 :
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 FAQLes questions fréquentes au sujet de l'internationalisation à 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 :
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 ».
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 :
<%@ 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).
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 » :
///
<
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 :
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 beaucoup de code.
Parmi les outils indispensables du développeur, il y a FxCopFxCop. 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 CultureInfoCA1304: 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 :
double
num =
Convert.
ToDouble
(
"1,234.5"
);
Ce code provoque une erreur de type FormatException. Voici un exemple de ce qu'il faut faire :
double
num =
Convert.
ToDouble
(
"1,234.5"
,
CultureInfo.
CreateSpecificCulture
(
"en-GB"
));
Évidemment, vous choisissez la méthode la mieux adaptée pour apporter cette information.
IV. Améliorations et optimisations▲
Il existe évidemment 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#Introduction aux délégués en C#.
Le résultat se présente ainsi:
///
<
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:
///
<
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 :
///
<
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 » :
///
<
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:
///
<
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.
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 :
///
<
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 :
<asp:
Label ID
=
"Label1"
runat
=
"server"
Text
=
"##000001-Hello...##"
></asp
:
Label>
Ou comme cela :
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.
///
<
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 moments 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.
///
<
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.
public
partial
class
Default :
BasePage
{
protected
void
Page_Load
(
object
sender,
EventArgs e)
{
}
}
La classe de base :
///
<
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 beaucoup à The_badger_manThe_badger_man, Philippe VialattePhilippe Vialatte, SkyounetSkyounet, tomlevtomlev pour leur aide. Merci spécial à jacques_jeanjacques_jean pour la relecture.