Comment transformer en application multilingue, votre application lazarus/freepascal

Freepascal est un compilateur pascal libre. Il est compatible avec la plupart des compilateurs pascal de Borland. Lazarus est un environnement de développement dédié à freepascal. L’ensemble ressemble à Delphi/Kylix mais Lazarus/Freepascal va plus loin dans certains domaines.

D’abord il permet de compiler pour les plateformes windows, linux, Mac... Ensuite, il corrige certains défauts de son ainé (notamment les boulets que sont les librairies borland tout-en-un donc 5 Mo dès la première ligne de code). Enfin, il utilise des outils simples mais diablement efficaces pour internationaliser son application...

L’internationalisation d’une application existante nécessite trois grandes étapes :
 l’isolation des chaînes de caractères,
 la création d’un fichier de chaines externe, la traduction
 l’utilisation du fichier de chaines traduites.

1. L’isolation des chaînes de caractères

1.1 dans le code source

Bien évidemment, notre objectif, c’est d’abord l’interface de notre logiciel. Elle doit pouvoir s’afficher en anglais, français, allemand et jusqu’aux langues les plus improbables. Pour cela, nous devons créer une ressource de chaines avec le mot-clef resourcestring. Celle-ci peut être soit intégrée à une unité existante, soit créée dans une unité dédiée à cela. Le second choix est le meilleur car on regroupe toutes les chaînes de l’application en une seule unité au lieu de disperser les chaines à traduire unité par unité, plus difficile à maintenir et générateur de doublons.

Créons donc notre nouvelle unité :

unit UChaines; 

{$mode objfpc}{$H+} // mis par défaut par Lazarus, dans votre version cette ligne peut être différente

// Définition des constantes "string" de l'interface et traductions par défaut

interface

uses
  Classes;

resourcestring  // dans la partie interface car doit être visible par les unités qui ont besoin de ces ressources

rsok	=	'ok';  // une toute première chaîne à traduire et sa valeur dans la langue par défaut...
 
implementation

end.

Le mot clef resourcestring est donc suivi d’une longue longue liste de "constantes chaine" sous la forme classique nom=valeur. Voyons maintenant comment créer cette longue liste.

En ce qui concerne les textes inclus dans une procédure ou une fonction, il existe un raccourci dans le menu OUTILS qui s’appelle "construire une chaîne ressource". Nous allons l’utiliser dans deux exemples typiques.

Exemple simple :

[extrait de unit1]
procedure TForm1.button1click(Sender: TObject);
begin
label1.caption:='Hello World';
end;

Positionnez le curseur texte dans ’hello world’ et avec le curseur de la souris, sélectionnez l’option de menu "construire une chaine ressource". Un message d’erreur vous indique qu’il ne trouve pas de resourcestring ? Normal, nous avons omis d’ajouter notre nouvelle unité UChaines dans la clause uses de unit1. Faisons-le et recommencons la manoeuvre.

Une nouvelle fenêtre affiche la ligne à modifier, quelques options vous sont proposées et en bas, la future chaine qui sera créée. Validez ce choix et voyons le résultat :

label1.caption :=rsHelloWord ;

Désormais label1.caption est affecté par la variable rsHelloWord (rs pour resource string mais vous pouvez décider d’un autre préfixe) dont la définition se trouve dans notre unité uchaines :

resourcestring
rsHelloWorld = 'Hello World';

Exemple plus complexe :

label1.caption:='Salut ' + prenom + ', l''heure actuelle est '+time; 
// ce qui donne "Salut Gerard, l'heure actuelle est 11:35"

sera transformé en :

label1.caption:=format(rsSalutLheurea,[prenom,time]);

Tandis que la chaine créée dans UChaine sera :

rsSalutLheurea = 'Salut %s, l''heure actuelle est %s';

Comme vous le voyez, la concaténation de chaines et variables est gérée par cet outil. Toutes les variables sont symbolisées par %s dans la chaine créée qui seront remplacées par leurs valeurs dans l’ordre d’apparition.

1.2 dans les objets graphiques

Beaucoup d’objets graphiques contiennent un texte affiché, c’est le cas de l’objet TForm dont la constante "caption" désigne le contenu de la barre de titre, ou bien de TEdit avec sa zone "text" ou encore de tmemo et sa zone "strings", TMenu...

Il faut là aussi traduire ces chaînes qui ne sont pas dans le code source (c’est-à-dire non visibles dans le fichier .pp ou .pas). Il faut donc déplacer leur affectation dans le code source et cela bien avant que l’objet ne soit affiché à l’écran. Nous allons donc réécrire pour chaque unité le "constructor", c’est-à-dire la procédure create.

Prenons l’exemple d’une fiche tform1 contenant un bouton bitbtn1 et un label tlabel1.
Il faut ajouter la procédure "constructor create" ainsi :

//déclaration
type
tform1=class(tform)
  	bitbtn1:tbitbtn;
	label1:tlabel;
 	
	constructor Create(AOwner: TComponent); override; 
 	[..]
	
// implementation
constructor tform1.create(AOwner:Tcomponent);

begin
	inherited create (AOwner);
	form1.caption:='titre de ma fenêtre';
	label1.caption:='cliquez sur le bouton ci-dessous';
	bitbtn1.caption:='Ok';
end;

Commentaire : le mot-clef "Override" signifie que nous surchargeons la procedure create existante. C’est donc celle-ci qui doit être exécutée et non la procedure create d’origine. Mais la première ligne qui contient le mot-clef inherited indique néanmoins d’effectuer toutes les opérations de création initialement prévues (bref, on fait un ajout aux opérations initiales existantes et non pas une substitution).

Arrivé là, nous sommes revenus à la même situation que précédemment : les chaines de texte sont dans le source. On peut utiliser notre outil de substitution vu plus haut.

2. La création d’un fichier de chaînes externe, la traduction.

Désormais, toutes nos chaines de texte à traduire sont dans une seule unité sous le mot-clef resourcestring. Il faut alors compiler notre aplication et nous trouverons un nouveau fichier qui s’appellera uchaine.rst

FreePascal est fourni avec un utilitaire du nom de rstconv qui, comme son nom l’indique, convertit un format rst en un autre format : le format .po. Celui-ci est utilisé par les logiciels de traduction tels que kbabel ou des éditeurs comme poedit.
On lancera donc rstconv sous cette forme :

rstconv -i uchaine.rst -o monlogiciel.po

Partant de ce fichier de base monlogiciel.po, nous allons pouvoir créer par copie monlogiciel.fr.po pour la traduction française, monlogiciel.de.po pour l’allemande, monlogiciel.en.po pour l’anglais...

Il n’est pas obligatoire d’utiliser un logiciel dédié pour la traduction, un simple éditeur de texte suffit même si KBabel rend le travail de traduction bien plus confortable. Le format est très simple :

	
# translation of sample.po to French
# beuz, 2005.
#

"Last-Translator: beuz\n"
"PO-Revision-Date: 2005-07-19 06:53+0200\n"
"Project-Id-Version: sample\n"
"Language-Team: French <fr@li.org>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.9.1\n"
"MIME-Version: 1.0\n"

# la première balise est vide : c'est le "modèle"
msgid ""
msgstr ""

# première traduction : rsfont
msgid "Font"
msgstr "Police"

Comme vous le devinez, les # indiquent une ligne de commentaire.
La ligne msgid est la zone à traduire (donc ne jamais la changer : elle doit correspondre exactement à notre unité UChaine).
La ligne msgstr contient la traduction, c’est celle-là qu’il faut réécrire dans la langue que vous souhaitez.

Une fois cette longue et fastidieuse traduction terminée, on va à nouveau changer le format en fichier .mo (et oui, troisième transformation, rappelez-vous que nous sommes parti d’un format rst natif à un format .po de traduction). Ce dernier format binaire ne diffère pas énormément du format .po, un simple examen montre qu’il est juste un tout petit peu moins lisible mais c’est celui qui sera exploité par notre logiciel.

Pour passer d’un format .po à un format .mo, un autre utilitaire doit être mis en oeuvre : msgfmt

Son utilisation est relativement simple :

msgfmt monlogiciel.po -o monlogiciel.mo

Evidemment, chaque erreur fera l’objet d’un arrêt avant la création du fichier .mo et vous aurez le type d’erreur et le numéro de la ligne de l’erreur.

3. l’utilisation d’un fichier de chaînes traduites

Il faut d’abord déterminer quelle langue doit être utilisée. Soit vous demandez à l’utilisateur de choisir sa langue, soit vous récupérez la langue du système d’exploitation. Dans le premier cas, le développement ne présente pas de difficultés : on propose une liste de langues supportées et on maintient une variable avec le nom du fichier .mo correspondant à ce choix.

L’autre solution est celle utilisée par Lazarus : il va chercher dans les variables d’environnement, la langue du système d’exploitation. La fonction getlanguageID effectue cette recherche. Pour la trouver, vous devez récupérer les sources de Lazarus. Selon la plateforme choisie, Lazarus inclut dans sa compilation le fichier lazconf.inc qui se trouve dans le dossier include de sa plateforme. Par exemple, si vous utilisez windows, vous trouverez dans le fichier lazarus_source\include\win32\lazconf.inc, la fonction de détection de la langue pour windows. De même pour linux, la fonction se trouve dans... /include/unix/lazbaseconf.inc.

Cette difficulté étant passée, il faut maintenant exploiter le fichier .mo dans son application. Là encore, nous allons "tricher" sur le code source de lazarus. Cela se fait dans l’unité translations.pas. La fonction Translateresourcestrings effectue ce travail : elle va rechercher dans le fichier .mo chacune des resourcestring de notre uchaines.pas. Si un msgid est égal à une chaine de uchaine.pas, elle va remplacer cette valeur par le contenu du msgstr correspondant.

On récupère donc ces deux fonctions (translateresourcestrings et getlanguageID) dans un fichier .pas que l’on va intégrer à notre application. On va le lancer dans le create de la 1ère unité utilisée (unit1.pas pour le tform1 de notre exemple) :

constructor TForm1.Create(AOwner: TComponent);

Begin
 inherited Create(AOwner);
 langue:=getlanguageID;  # donne langue='fr'
 translateresourcestrings('./',langue);
 Bitbtn1.caption:=rsOk;
 form1.caption:=rstitredefen;
 label1.caption:=rshelloworld;
end;

Conclusion :

Et bien voilà, c’est à peu près ce qu’il faut faire pour traduire une application... N’hésitez pas à me signaler toute erreur, omission ou imprécision.