Localisation d’une application

Développer une application c’est bien, mais la traduire dans plusieurs langues c’est mieux  (surtout si vous êtes Tlingit).

Dans cet article, je vais vous présenter comment on fait normalement, puis je vous présenterez une librairie de mon cru pour simplifier la localisation de vos application.

En .Net, la localisation d’une application se fait traditionnellement via un fichier de ressources. En effet, le Framework .Net gère de façon native la localisation de ces fichiers.  Pour cela, il suffit de créer un nouveau fichier  portant le même nom et comprenant le code de la culture correspondante. Par exemple, pour un fichier localisation.resx, la version française du fichier sera localisation.fr-FR.resx.

localisation_img1

De cette façon, lors  de la récupération d’une ressource, l’application utilise automatiquement le fichier correspondant à la culture de l’application.

Ensuite il faut remplacer le texte à traduire par la ressource  correspondante dans le code de l’application. Le fonctionnement est le suivant :

// Création d’un ResourceManager
ResourceManager ressManager = new ResoureManager("MyApplication.Localisation.localisation ", Assembly.GetExecutingAssembly());

// Récupération du texte depuis le fichier de ressource
string text = ressManager.GetString([Reference]);

Avec [Reference] correspondant au code de la ressource. La culture utilisée par défaut est celle du thread principal. On peut aussi forcer le choix de la culture:

string text = ressManager.GetString([Reference], [Culture]);

Avec [Culture] correspondant à la culture à utiliser. Si le fichier de ressource pour cett culture n’existe pas, c’est le fichier par défaut (localisation.resx) qui est utilisé.

Module de localisation

Je vais maintenant vous présenter le module de localisation que j’ai développé.  Je suis parti de l’idée de créer un module de localisation plus « User-Friendly », en m’inspirant du module de localisation de Qt (Qt Linguist) que j’utilise en C++. Cette librairie possède deux particularités :

  • Garder le texte « en clair » dans le code plutôt que d’avoir le code d’une ressource.
  • Générer automatiquement le fichier de ressource via une fonction préprocesseur.

Cette description est assez légère et n’explique que les grandes lignes de la librairie. Je vous invite grandement à regarder les sources disponibles en téléchargement ici.

Manager de Ressources

Avant tout, nous allons créer un gérer pour le fichier de ressource et le changement de culture. De cette façon, on centralise la gestion de la traduction en seul point plutôt que d’avoir des ResourceManager partout dans le code.

Ce rôle est géré par la classe LocManager :

localisation_img_2

La première chose à faire est de créer un singleton pour le ResourceManager. Pour cela on définit un attribut _resourceManager et on gère l’unicité par la propriété associée.

/// <summary>
/// Manager de ressources pointant sur le bon fichier de localisation
/// </summary>
private static ResourceManager _resourceManager = null;
 
/// <summary>
/// Manager de ressources
/// </summary>
public static ResourceManager ResourceManager
{
 get
 {
  if(_resourceManager == null) _resourceManager = GetResourceManager();
  return _resourceManager;
 }
 
 set { _resourceManager = value; }
}

La méthode GetResourceManager() initialise un ResourceManager avec le fichier de ressource contenu dans l’assembly du thread principal.

Ensuite, il nous faut pouvoir modifier la culture de l’application à la volée. Pour cela, on expose une propriété UICulture qui correspond à la culture du thread principal :

/// <summary>
/// Culture (Langue) en cours
/// </summary>
public static CultureInfo UICulture
{
 get
 {
  return Thread.CurrentThread.CurrentUICulture;
 }
 
 set
 {
  Thread.CurrentThread.CurrentUICulture = value;
  OnCultureChanged();
 }
}

Le changement de la culture lève un événement OnCultureChanged() qui est utilisé pour forcer la mise à jour des éléments WPF de l’IHM. De cette façon, à chaque modification de la culture, tous les champs de l’IHM sont traduits dans la nouvelle langue (si le fichier correspondant existe).

 

Localisation du texte

Pour la localisation du texte dans l’application, nous allons utiliser 2 classes : l’une pour le texte dans le code C# et l’autre pour le texte dans le xaml.

Localisation C#

La localisation du code C# est réalisée par la classe Loc.

localisation_img_5

Cette classe statique possède seulement deux méthodes.

La méthode GetKey génère une clé unique à partir du texte à traduire. Cette clé sera utilisée pour identifier le texte dans le fichier de ressource.

/// <summary>
/// Fonction de création de la clé du fichier de ressource
/// </summary>
/// <param name="text">Texte à traduire</param>
/// <returns>Clé correspondante</returns>
public static string GetKey(string text)
{
 if (string.IsNullOrWhiteSpace(text)) return null;
 
 return string.Format("Loc_{0}", text.GetHashCode());
}

La clé doit être unique pour un texte donné, du coup on utilise le HashCode du texte pour générer la clé.

La méthode Trad retourne la traduction du texte passé en paramètre suivant la culture définie par le LocManager.

/// <summary>
/// Fonction de traduction du texte
/// </summary>
/// <param name="text">Texte de base</param>
/// <returns>Texte traduis</returns>
public static string Trad(string text)
{
 string trad = null;
 if (LocManager.ResourceManager != null)
 {
  var key = GetKey(text);
  if(key != null)
   trad = LocManager.ResourceManager.GetString(key);
 }
 
 return trad ?? text;
}

Si on ne trouve pas de correspondance, on retourne le texte d’origine. De cette façon, s’il n’y a pas de localisation, l’application reste dans sa langue de développement.

 

Localisation xaml

Pour les IHM en WPF, on pourrait très bien utiliser la classe Loc dans le code-behind pour traduire les champs au chargement de la fenêtre.  Mais ce serait assez long et fastidieux. Du coup,  nous allons utiliser une MarkupExtension  qui fournit une valeur dynamiquement au chargement de l’interface en xaml via la méthode ProvideValue.

localisation_img_3

On crée donc une classe Trad (je sais, je ne me suis pas foulé pour les noms des classes…), qui hérite de MarkupExtension. Cette héritage implique de fournir la méthode ProvideValue. C’est cette méthode qui renvoie l’objet à placer dans le xaml, dans notre cas le texte traduit.  La clé du fichier de ressource est passée en paramètre via la propriété Key, que l’on définit comme argument du constructeur :

[ConstructorArgument("key")]
public string Key { get; set; }

Ainsi pour traduire un texte, il suffit de placer le code suivant dans le fichier xaml :

<Label Content="{Trad  ‘Mon texte à traduire’ }" />

L’objet « Content » du label est de type Trad , instancié via le constructeur Trad(« Mon texte à traduire »).

Pour éviter de devoir définir notre librairie Localisation dans l’entête du fichier xaml, nous allons la définir comme une librairie WPF. Pour cela, il suffit d’ajouter ces trois lignes dans le fichier AssemblyInfo.cs :

//Définitions de l'assembly pour la reconnaissance directe dans le xaml
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Localisation")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2007/xaml/presentation", "Localisation")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2008/xaml/presentation", "Localisation")]

Le problème de la MarkupExtension,  c’est que l’objet Trad est instancié au chargement de l’interface, du coup la traduction ne sera pas recalculée s’il on change de langue pendant que la fenêtre est active.  C’est là que rentre en scène l’événement CultureChanged du LocManager.

Lors de l’appel de la méthode ProvideValue, nous allons nous abonner à l’évènement CultureChanged  et ainsi pouvoir forcer le rafraichissement du composant WPF appelant (dans notre exemple, le <Label />).

//Abonnement au changement de culture
LocManager.CultureChanged += LocManager_OnCultureChanged;

Pour forcer le rafraichissement du composant WPF, on va remplacer directement la  valeur de la DependencyProperty pour la nouvelle traduction.

//On récupère la traduction dans la nouvelle langue.
var value = Loc.Trad(Key);
if (_targetProperty is DependencyProperty)
{
 var obj = _targetObjReference.Target as DependencyObject;
 var prop = _targetProperty as DependencyProperty;
 obj.SetValue(prop, value);
}
else
{
 var obj = _targetObjReference.Target;
 var prop = _targetProperty as PropertyInfo;
 prop.SetValue(obj, value, null);
}

Pseudo Macro .Net

Maintenant que nous avons la possibilité de traduire le texte dans le code et dans l’IHM, il est intéressant d’automatiser la localisation du code.

En effet, pour localiser notre texte, il est nécessaire de renseigner le fichier de ressource correctement et d’utiliser les bonnes méthodes de localisation, ce qui n’est pas toujours intuitif. Pour ma part, j’ai l’habitude de coder l’intégralité d’une fonctionnalité  (IHM et métier) dans la langue de base, je fais mes tests et ne rajoute la localisation qu’à la fin. Ce qui implique de reprendre le code avec le risque d’oublier certains champs ou de faire des erreurs.

Du coup, nous allons créer une tâche pour ajouter automatiquement le texte dans le fichier de ressource et  insérer le code de localisation lors de la compilation, à la manière d’une macro en C.  De cette façon, il suffira de préfixer le texte à traduire par « Loc :: » pour que celui-ci soit localiser lors de la prochaine compilation. Par exemple :

« Loc ::Mon texte à traduire »
se transformera en
Localisation.Loc.Trad(« Mon texte à traduire »)

Cette fonctionnalité est réalisée par l’objet LocalisationTask, qui hérite de Task.

localisation_img4

Lors de la compilation, la méthode Execute() est appelée pour réaliser une tâche donnée, dans notre cas remplacer le texte par l’appel aux méthodes de localisation.  Le processus est le suivant :

  1. Récupération du fichier de ressource s’il existe
  2. Recherche de tous les fichiers de code en « .cs » et d’IHM en « .xaml »
  3. Pour chaque fichier, recherche du texte commençant par  « Loc :: »
    1. Création d’une clé pour le fichier de ressource
    2. Ajout du texte dans le fichier de ressource
    3. Remplacement du texte dans le fichier
    4. Création du fichier de ressource pour la langue par défaut.

La recherche des fichiers, du texte à traduire et le remplacement se fait grâce à l’utilisation intensive de Regex (c’est trop fort !!) définies en constante.

Le paramétrage de la tâche passe par deux propriétés :

  • ApplicationDir définit le chemin vers le code source de l’application.
  • DefaultCulture définit la culture par défaut de l’application.

Pour exécuter la tâche lors de la compilation, il faut offrir le fichier du projet. Pour se faire, il existe deux méthodes :

  • Ouvrir le fichier .csproj avec un éditeur de texte comme Notepad++
  • Décharger le projet dans VS, puis clic droit > modifier … .csproj

Ensuite, on référence notre tâche de compilation dans le projet en ajoutant juste après l’ouverture de la balise <Project>

<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import […] />
<UsingTask AssemblyFile[Chemin de Localisation.dll]" TaskName="LocalisationTask" />
 

Puis, juste avant la fermeture de la balise <Project>, On appelle notre tâche avec les bons paramètres :

<Target Name="BeforeBuild">
<LocalisationTask ApplicationDir="." DefaultCulture="fr-FR" />
</Target>

 

Et voilà !  Maintenant à chaque compilation du projet, tous les textes préfixés par « Loc :: » seront ajoutés au fichier de ressource et remplacer par la méthode de localisation. Pour finir, il reste à ajouter les fichiers de ressources au projet.

 

Vous pouvez retrouver le code-source, ainsi que la dll sur la page de téléchargement du site, ou directement sur SourceForge.

 

A+ et bon code !

 

Vous avez aimé cet article ? Alors partagez-le avec vos amis en cliquant sur les boutons ci-dessous :

Twitter Facebook Google Plus email

Une réflexion sur “ Localisation d’une application ”

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *