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 DataPrésentation d'Asp.net 3.5 Dynamic 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 é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 :
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 beacoup 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
"
));
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#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 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.
///
<
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 beacoup à The_badger_manThe_badger_man,
Philippe VialattePhilippe Vialatte,
SkyounetSkyounet,
tomlevtomlev pour leur aide.
Merci spécial à jacques_jeanjacques_jean pour la relecture.