I. Introduction▲
I-A. Préambule▲
La mondialisation de l'industrie et du secteur tertiaire amène de plus en plus le développeur d'une application à devoir la penser pour des utilisateurs qui souhaiteront y accéder dans leur langue de prédilection. Dans cette optique, développer une application multilingue apporte une plus-value non négligeable et permet une diffusion plus large du logiciel créé.
Parfois, la capacité à développer une application multilingue sera LE facteur déterminant qui permettra d'emporter le marché.
Le présent tutoriel a pour ambition de vous guider pas à pas dans la mise en place d'une version multilingue qui permettra à l'utilisateur de choisir sa langue "à chaud", pendant l'utilisation de l'application.
Cette technique permettra de maintenir une seule application, en permettant l'ajout d'une langue sans devoir toucher au code... et donc sans l'intervention du développeur.
Cette version multilingue remplacera avantageusement la collection de versions linguistiques délicates à maintenir et qui nécessitaient une intervention rapide (et donc une disponibilité) de la part du développeur.
I-B. Organisation du tutoriel▲
Nous allons aborder ce tutoriel par l'adaptation des étiquettes (labels) des contrôles des formulaires. Cette partie nous permettra d'aborder le concept de façon relativement simple. Nous la transposerons et l'adapterons aux messages puis à l'aide.
J'ai volontairement choisi de débuter par des méthodes que je qualifierai de mauvaises, pour illustrer les problèmes liés à l'utilisation de ces démarches. Cela permettra de mettre d'autant mieux en évidence les avantages d'une technique fonctionnelle et adaptable, amenant à des interventions de maintenance réduites au minimum.
I-C. Niveau et prérequis▲
Nous aborderons très peu l'organisation des tables. Il n'est donc pas nécessaire d'être ferré en conception et en organisation des données.
Par contre, une bonne connaissance de l'interface Access sera nécessaire, ainsi qu'une certaine aisance en VBA.
Ce tutoriel est donc a priori destiné à un public de niveau moyen ou avancé.
I-D. Version utilisée▲
La version utilisée est Access 2013. Néanmoins, mise à part l'adaptation du ruban, les techniques et les codes proposés ici devraient fonctionner avec toutes les versions d'Access depuis la 1997. N'hésitez pas à me contacter si vous constatez des erreurs avec votre version. Cela me permettra de préciser les spécificités de chaque version en addendum.
II. Les formulaires▲
II-A. Identification des zones multilingues▲
II-B. Méthode « hard coding »▲
Considérons un formulaire tout simple, composé uniquement de deux contrôles de type Textbox et possédant chacun une étiquette liée.
Nous souhaitons pouvoir afficher les étiquettes Nom et Prénom selon la langue de l'utilisateur. Pour l'instant, nous souhaitons rendre notre application bilingue FR-EN.
Pour ce faire, nous allons utiliser une variable globale que nous manipulerons tout au long du tutoriel. Dans un premier temps, nous modifierons la valeur de cette variable à la main, et nous étudierons plus tard comment permettre à l'utilisateur de modifier cette valeur.
II-B-1. Processus mis en place▲
L'idée de départ est simple. En interceptant l'événement "sur chargement" du formulaire, nous allons tester la variable globale et réagir en conséquence en utilisant un bloc Case... End Case. Ce bloc évaluera une variable globale et déterminera les valeurs des étiquettes en conséquence.
II-B-2. Mise en place▲
II-B-2-a. Nommage des contrôles▲
Je ne le répéterai jamais assez : Nommez vos contrôles avec des noms explicites ! Je vois trop souvent des « applications « avec textbox1, textbox2, etc dans le code. La maintenance du code devient un véritable casse-tête.
II-B-2-b. Création de la variable globale▲
Créons la variable globale dans un module standard, puis attribuons-lui une valeur via la fenêtre d'éxécution. Cette méthode, très peu pratique, nous permettra pour l'instant d'illustrer la méthode. Bien entendu, dans notre application réelle, cette valeur sera choisie par l’utilisateur lors de sa connexion à l’application.
Si, suite à une erreur au niveau de l'exécution du code, vous deviez stopper le code, il faudra attribuer à nouveau une valeur à cette variable globale.
Entrons dans le VBE (Visual Basic Editor) via Alt+F11 et créons un module standard.
Le code de déclaration de la variable globale tient en une ligne.
Global
gUserLanguage As
String
Nous attribuons la valeur "FR" à cette variable via la fenêtre d'exécution (CTRL+G) dans laquelle nous pouvons lancer des instructions. La première ligne sert à attribuer une valeur à la variable, la seconde ligne permet de vérifier la valeur. Le signe « ? » est l'équivalent de Debug.Print utilisé au sein d'une procédure.
II-B-2-c. Gestion de l’événement OnLoad▲
En basculant le formulaire en mode création, nous pouvons atteindre son code via la commande idoine du ruban Outils de création du formulaire.
Vous pouvez également créer le module en modifiant la propriété Avec module de l'onglet Autres des propriétés du formulaire.
Lorsque vous avez créé le module du formulaire, celui-ci apparaît dans l'arborescence de votre projet sous le groupe Microsoft Access Objects.
Le module du formulaire ne peut pas être renommé. Il est toujours de structure Form_ suivi du nom Access du formulaire. Par facilité, ne nommez jamais vos formulaires en utilisant des espaces.
Vous pouvez alors créer la procédure événementielle en choisissant Form dans la liste de gauche et Load dans celle de droite. L'éditeur crée la procédure correspondante. A nouveau, deux façons de procéder s’offrent à vous :
- création de la procédure en VBA ;
- création de la procédure via les événements du formulaire en mode création.
Vous remarquerez que dans l’IHM, les événements sont nommés selon la langue d’Access, alors qu’ils sont toujours nommés en anglais dans le VBE (Visual Basic Editor).
Le corps de la procédure événementielle est facile à comprendre. Soit gUserLanguage vaut EN et nous aurons les libellés en anglais, soit elle vaut autre chose et nous aurons les libellés en français.
Private
Sub
Form_Load
(
)
Select
Case
gUserLanguage
Case
"EN"
lblName.Caption
=
"Lastname"
lblFirstname.Caption
=
"Firstname"
Case
Else
lblName.Caption
=
"Nom"
lblFirstname.Caption
=
"Prénom"
End
Select
End
Sub
Vérifions si tout fonctionne correctement en ouvrant le formulaire, d’abord « en français », puis en anglais.
Pour tester la version anglaise, il faut, via la fenêtre d’exécution, modifier la valeur de gUserLanguage.
Prévoyez des étiquettes suffisamment larges que pour pouvoir contenir l’entièreté des libellés, quelle que soit la langue utilisée.
II-B-3. Problèmes inhérents à cette méthode de travail.▲
En procédant de la sorte, nous mettons en place une procédure « rapide » pour... deux étiquettes dans... deux langues!
Je vous laisse imaginer les lignes de code lorsque nous aurons cinquante étiquettes, avec autant d'infobulles et de textes de barre d'état, dans cinq ou six langue différentes. La mission va se révéler impossible. Outre les lignes de codes à saisir, on comprend aisément que la maintenance va devenir un vrai casse-tête en cas d'ajout ou de suppression d'étiquette, puisqu’il faudra constamment triturer le code arriver à nos fins.
Le problème sera tout aussi bloquant si l'on doit ajouter une version linguistique non prévue au départ, puisqu'il faudra passer en revue tous les formulaires et états pour ajouter les lignes de code. De plus, en cas de faute de frappe ou de traduction, il faudra modifier le texte dans le code.
Néanmoins, cette approche permet de valider la faisabilité de l'entreprise grâce à la manipulation de certaines propriétés des contrôles par code VBA. En effet, quelle que soit la solution qui sera mise en place, elle consistera à retrouver le libellé souhaité et à adapter la propriété caption du contrôle.
Il ne reste plus qu'à mettre en place ce qui permettra de ne plus intervenir dans le code lors de l'ajout de contrôles ou de versions linguistiques.
Oui oui, vous avez bien lu ! En mettant en place les mécanismes abordés dans ce tutoriel, vous pourrez ajouter des contrôles à des formulaires (ou des états) et ajouter des langues sans devoir modifier le code de votre application !
II-C. Création d’un formulaire multilingue▲
II-C-1. Réflexions sur la méthode à mettre en place▲
Si vous ne voulez pas toucher au code pour ajouter des versions linguistiques à vos étiquettes, vous devrez logiquement stocker les libellés ailleurs que dans le code.
Trois techniques s’offrent à vous, la troisième étant un assemblage des deux premières :
- stocker les données linguistiques dans des tables applicatives. Cette méthode permet de ne pas dépendre d’un fichier externe. L’inconvénient est qu’il faut distribuer une nouvelle application lors de l’ajout d’une langue.
- stocker les données linguistiques dans un fichier externe, idéalement un xml. Cette méthode permet de ne pas devoir déployer une nouvelle appli lors de l’ajout d’une langue, puisqu’il suffira de modifier le fichier xml. L’inconvénient est que si le xml est détruit, l’appli devient inutilisable ;
- stocker les données dans des tables et prévoir un mécanisme de mise à jour grâce à un fichier xml. Ainsi, l’appli peut fonctionner sans le xml et il suffit de transmettre un xml contenant les nouvelles versions linguistiques pour mettre à jour les données des tables applicatives.
Nous allons privilégier la première solution pour le développement du tutoriel, mais vous trouverez en annexe les explications pour la mise en place des deuxième et troisième solutions.
Pour l’instant, nous allons travailler avec un seul formulaire. Par la suite, nous évoluerons pour mettre en place une technique valable pour tous les formulaires et les rapports de notre application.
II-C-2. Tables utilisées▲
Pour retrouver le libellé à afficher, nous avons besoin :
- du nom du contrôle ;
- du nom de son conteneur (formulaire ou état) ;
- de la langue choisie par l'utilisateur ;
- du libellé à afficher.
Nous pourrions être tentés par la gestion de tous ces éléments en une seule table, et ce serait parfaitement réalisable. Toutefois, nous nous priverions de quelques fonctionnalités intéressantes, et le risque de saisir des données erronées serait important. J'ai donc opté pour une structure forte qui permettra la gestion de la langue, l'ajout de conteneurs (formulaires ou états) et l'ajout de contrôles de façon sécurisée et en intervenant le moins possible dans le code.
La mise en oeuvre de ce processus passe par la création des tables d'application. Nous pouvons identifier quatre tables :
- la table des langues, qui servira plus tard à préciser le choix de l'utilisateur;
- la table des formulaires et états affectés par le multilinguisme;
- la table des contrôles qui seront identifiés par leur nom et le formulaire auquel ils appartiennent;
- la table des libellés des contrôles, spécifiant les textes à utiliser en fonction de la langue.
Le schéma relationnel des tables est illustré ci-dessous. J'ai ajouté des listes déroulantes sur les colonnes des clés externes (FK) de façon à saisir rapidement les informations dans les tables.
Nous commençons avec deux langues (FR et EN) et un formulaire, mais nous verrons par la suite comment ajouter facilement une langue, un formulaire ou un contrôle. Ci-dessous, vous trouverez le tableau descriptif des tables applicatives utilisées.
La structure créée permettra d’ajouter des contrôles et des langues sans devoir modifier structurellement notre application et sans devoir modifier le code VBA utilisé.
Pour l’instant, nous ne travaillons qu’avec un seul formulaire, mais la table LanguageObejct sera très utile dès que nous évoluerons vers une solution permettant de traiter tous les formulaires de l’application.
II-C-3. Saisie des données dans les tables▲
Votre application contient pour l'instant un formulaireappelé fContact. Ce formulaire contient deux contrôles Textbox auxquels sont associées deux étiquettes (label) nommées lblName et lblFirstname. Nous comptons pour l'instant gérer deux langues, le français (FR) et l'anglais (EN). Voici les tables remplies en fonction de ces informations.
J’ai bien entendu utilisé des listes déroulantes sur certains champs. La création de ces listes déroulantes n’entre pas dans la matière abordée par ce tutoriel.
II-C-4. Création de la requête applicative▲
Pour pouvoir gérer facilement les textes des étiquettes, vous allez créer une requête permettant de récupérer les textes par contrôle et par formulaire. Vous nommerez cette requête aq_LanguageTexts. Elle sera utilisée par le code VBA pour récupérer les textes des contrôles du formulaire qui est chargé.
II-C-5. Gestion de la langue au chargement du formulaire▲
L'objectif consiste ici à récupérer la colonne souhaitée d'un enregistrement unique au sein d'un recordset et de l'attribuer à la propriété Caption du contrôle manipulé.
Un recordset est un jeu d'enregistrements éventuellement filtrés ou triés. Ce recordset s'appuie sur une source de données (table ou requête) et peut extraire les données de toutes ou de certaines colonnes. On peut apparenter cette notion à celle d'une requête dans l'IHM d'Access.
Ici, notre recordset va s'appuyer sur la requête que nous venons de créer, en filtrant les éléments sur base du contrôle, du formulaire parent et de la langue choisie grâce à une clause WHERE.
Vous aurez compris que la clause WHERE s’appuie sur trois critères :
- Le nom du contrôle ;
- Le nom du formulaire ;
- La donnée permettant le choix de la langue (gUserLanguage).
Il est intéressant de connaître au moins les bases du langage SQL, mais si cela vous pose problème, vous pouvez vous aider du QBE d’Access (Query By Example) qui crée la requête à votre place. Il suffit alors de copier et d’adapter dans votre code le texte créé par le QBE.
Comme pour l’instant, il n’y a qu’un seul formulaire, vous pourriez être tenté de ne pas « critériser » le formulaire dans la requête, mais souvenez-vous ne notre objectif d’avoir un mécanisme fonctionnel pour tout formulaire multilingue.
II-C-6. Adaptation de la procédure OnLoad▲
Bien sûr, il faut encore adapter la procédure OnLoad du formulaire. Vous pourriez être tenté par cette approche, qui est parfaitement fonctionnelle :
lblName.Caption
=
CurrentDb.OpenRecordset
(
"select controltext "
&
_
"from aq_LanguageTexts "
&
_
"where ((objectname = 'fContact') and (controlname = 'lblName') and "
&
_
"(Initials = '"
&
gUserLanguage &
"'))"
).Fields
(
"controltext"
)
lblFirstname.Caption
=
CurrentDb.OpenRecordset
(
"select controltext "
&
_
"from aq_LanguageTexts "
&
_
"where ((objectname = 'fcontact') and "
&
_
"(controlname = 'lblfirstname') and "
&
_
"(initials = '"
&
gUserLanguage &
"'))"
).Fields
(
"controltext"
)
Pour que ce code fonctionne, vous devez initialiser la langue, même pour le français, sinon le recordset sera vide et le code lèvera une erreur à l’exécution.
Cependant, chaque ajout de contrôle au sein du formulaire va encore nécessiter une modification du code. De plus, un formulaire avec une cinquantaine d’étiquettes nécessiterait un code beaucoup trop volumineux, ainsi qu’un appel répétitif à une requête.
De plus, si la procédure OnLoad doit effectuer d’autres tâches sur ledit formulaire, une chatte n’y retrouvera plus ses jeunes. Autrement dit, vous serez dans l’impossibilité de maintenir votre code.
II-C-7. Procédure propre et boucle sur les contrôles à gérer▲
L’idée va donc être de sortir la procédure de traitement des textes de OnLoad d’une part, et de boucler sur les contrôles à gérer d’autre part.
II-C-7-a. Procédure propre▲
Sortir le code de la procédure OnLoad n’est pas bien compliqué. Il suffit de créer une procédure LanguageManager dans le module du formulaire et d’y faire appel dans OnLoad.
Option Compare Database
Option Explicit
Private Sub Form_Load()
LanguageManager
End Sub
Sub LanguageManager()
End Sub
II-C-7-b. Procédure itérative pour la gestion des textes d’étiquettes▲
Nous avons à notre disposition la requête aq_LanguageTexts. Si nous supprimons le filtre sur le contrôle, nous obtenons alors un recordset contenant tous les textes du formulaire à gérer. Il suffira de boucler sur ce recordset et d’affecter le contrôle sur le nom duquel le recordset pointe. Voici le code qui normalement se passe de commentaires :
Private Sub Form_Load()
LanguageManager
End Sub
Sub LanguageManager()
Dim Ctrl As Control
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("select * from aq_languageTexts " & _
"where ((Objectname = 'fContact') and
(Initials = '" & gUserLanguage & "'))")
Do While Not rs.EOF
Me.Controls(rs!ControlName).Caption = rs!ControlText
rs.MoveNext
Loop
End Sub
Testons ce code en faisant varier les initiales de la langue souhaitée, c’est –à-dire la variable gUserLanguage. N’oubliez pas de modifier la valeur de gUserLanguage et de fermer le formulaire entre chaque test. Concluant ?
II-C-8. Ajout d’un contrôle dans le formulaire▲
Quelles sont les étapes à réaliser pour ajouter un contrôle multilingue à notre formulaire ?
II-C-8-a. Ajout du contrôle dans le formulaire▲
Ajoutez un contrôle de type combobox (liste déroulante) pour permettre la sélection d’une catégorie, nommez son étiquette (par exemple lblCategory). L’alimentation de la liste déroulante est laissée de côté pour l’instant.
II-C-8-b. Ajout des données dans les tables applicatives▲
Vous devez également modifier les tables applicatives en ajoutant les données :
- lblCategory dans la table des contrôles, en renseignant son formulaire fContact ;
- les deux textes (français et anglais) dans la table des textes.
II-C-8-c. Modification du code▲
…
…
RIEN !! ABSOLUMENT RIEN !!
Lors du chargement du formulaire, le recordset récupère les lignes des trois contrôles, et la boucle prend en charge l’adaptation du texte pour chaque contrôle, y compris donc la nouvelle étiquette.
A ce stade, vous observerez que ce n’est pas le type de contrôle qui est traité par le code, mais sa propriété Caption. Tout autre contrôle exposant une propriété Caption pourrait donc être géré sans modification de code.
Pour vérifier cela, ajoutez un bouton de commande sur le formulaire qui servira à imprimer les données du contact et nommez-le btnPrint, puis procédez comme pour la catégorie en ajoutant l’entrée dans la table des contrôles et les deux textes (FR et EN) dans la table des textes.
Le signe « & » du texte sert à souligner la lettre lors de l’affichage du contrôle, et vous pourrez donc utiliser le raccourci CTRL+Lettre soulignée pour activer le contrôle.
Vous observerez qu’à nouveau, rien ne doit être modifié dans le code pour que le bilinguisme fonctionne.
II-C-9. Ajout d’une langue▲
Pour ajouter une langue à notre application, le travail est un peu plus conséquent. Il faut en effet ajouter :
- la nouvelle langue dans la table des langues ;
- les nouveaux libellés pour nos trois contrôles ;
- voir du côté du code s’il y a des adaptations à réaliser.
II-C-9-a. Ajout des données dans les tables▲
Vous allez ajouter l’espagnol dans les langues et ajouter les textes traduits. L’illustration suivante expose les ajouts réalisés dans les tables.
II-C-9-b. Modification du code▲
RIEN !! ABSOLUMENT RIEN !!
La seule chose à laquelle il faut penser pour tester l’espagnol, c’est de modifier la valeur de la variable gUserLanguage.
II-C-10. Sous-formulaire▲
Le sous-formulaire n'est pas un contrôle comme les autres au sein du formulaire, puisqu'il pointe lui-même vers un formulaire, et ce formulaire sera chargé juste avant le chargement du formulaire principal.
II-C-10-a. Modifications du code▲
En ce qui concerne l'étiquette du sous-formulaire, c'est un contrôle du formulaire principal comme un autre. Il suffit donc de l'ajouter dans la table des contrôles et d'ajouter ses libellés linguistiques dans la table des libellés.
Pour le formulaire sous-jacent, il faudrait qu'il puisse lancer lui aussi la procédure LanguageManager. Le problème vient du fait que cette procédure a été créée dans le module d'un formulaire, et n'est donc a priori par disponible hors de ce formulaire, sauf à préfixer l'appel du nom VBA du formulaire, ce qui s'avérera rapidement peu pratique. De plus, cette procédure utilise l'objet Me qui pointe vers le formulaire qui héberge la procédure. Nous allons donc devoir sortir la procédure du module du formulaire pour la rendre générique. Cette opération engendrera quelques modifications du code de ladite procédure.
La première modification consistera à passer à la procédure le formulaire dont les labels doivent être modifiés et à remplacer Me par le nom de la variable passée.
La seconde modification consistera à sortir cette procédure du module du formulaire pour la déposer dans un module standard. Ainsi, elle sera disponible pour tous les formulaires qui en auront besoin.
En troisième lieu, il faudra modifier l’appel à cette procédure dans le OnLoad du formulaire pour lui passer le formulaire, c’est-à-dire Me. Il faudra également remplacer fContact dans la chaîne SQL par le nom du formulaire passé en paramètre.
Voici la procédure LanguageManager adaptée.
Sub LanguageManager(Item As Form)
Dim Ctrl As Control
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("select * from aq_languageTexts " & _
"where ((Objectname = '" & Item.Name & "') and
(Initials = '" & gUserLanguage & "'))")
Do While Not rs.EOF
Item.Controls(rs!ControlName).Caption = rs!ControlText
rs.MoveNext
Loop
End Sub
Pour rappel, cette procédure doit être déplacée dans un module standard. De plus, sortir cette procédure du module du formulaire permettra de l’utiliser pour n’importe quel formulaire qui l’appelle !
N’oubliez pas d’adapter la ligne appelante dans la procédure OnLoad.
Private Sub Form_Load()
LanguageManager Me
End Sub
II-C-10-b. Création du sous-formulaire▲
Vous allez ajouter un formulaire en tant que sous-formulaire dans le formulaire fContact. Ce sera un formulaire reprenant la liste des factures pour le contact. Appelez-le fInvoice. Bien entendu, il faut nommer vos contrôles, par exemple :
- lblInvoice ;
- lblDate ;
- lblAmount ;
- lblDueDate.
II-C-10-c. Ajout des données dans les tables▲
Il faut aussi ajouter les données dans les tables. Ici, trois tables sont concernées puisqu’il faut :
- ajouter le formulaire dans la table des objets ;
- ajouter les contrôles dans la table des contrôles ;
- ajouter les libellés dans la table des textes.
II-C-10-d. Insertion du sous-formulaire▲
Insérez le formulaire fInvoice dans le formulaire fContact comme sous-formulaire, par exemple par drag & drop.
II-C-10-e. Adaptation du code dans le formulaire▲
Il nous reste à créer la procédure OnLoad du formulaire fInvoice pour qu’au chargement, ce formulaire appelle la procédure LanguageManager.
Private Sub Form_Load()
LanguageManager Me
End Sub
II-C-10-f. Test du mécanisme avec le sous-formulaire▲
Il ne vous reste qu’à tester pour vérifier que tout fonctionne bien. N’oubliez pas de modifier la valeur de gUserLanguage.
Si vous souhaitez tester l’espagnol, vous devez ajouter les textes espagnols dans la table des tests, sinon, vous aurez les textes par défaut pour ceux qui ne sont pas trouvés dans la table.
II-C-11. Le cas particulier de la légende du formulaire▲
Le bandeau du formulaire, ou son onglet si vous affichez les fenêtres via les onglets, possède lui aussi une propriété Caption permettant de personnaliser le « titre » du formulaire. Mais le formulaire n’est pas repris dans la boucle qui gère les contrôles. Or, ce serait intéressant de pouvoir, ici aussi, utiliser une version linguistique du titre à afficher. Avec une petite astuce et à nouveau une modification minime de votre code, vous allez pouvoir agir également sur cette propriété.
L’idée consiste à créer dans la table des contrôles un contrôle fictif nommé de la même façon pour chaque formulaire, par exemple form. Il suffira de tester le nom du contrôle au sein de la boucle et de se brancher conditionnellement sur les lignes de code qui géreront ce cas précis.
II-C-11-a. Ajout du « contrôle des propriétés▲
L’ajout est assez simple à réaliser.
II-C-11-b. Modification du code▲
La modification du code est très simple, puisqu’il suffit de placer un bloc If…End If au sein de la boucle.
Sub LanguageManager(Item As Form)
Dim Ctrl As Control
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("select * from aq_languageTexts " & _
"where ((Objectname = '" & Item.Name & "') and
(Initials = '" & gUserLanguage & "'))")
Do While Not rs.EOF
If rs!ControlName = "form" Then
Item.Caption = rs!controltext
Else
Item.Controls(rs!ControlName).Caption = rs!controltext
End If
rs.MoveNext
Loop
End Sub
A nouveau, en modifiant la langue au sein de la variable globale, vous modifiez le titre du formulaire.
II-D. Rendre multilingues d’autres propriétés acceptant du texte▲
Jusqu’ici, nous avons uniquement travaillé avec la propriété Caption des contrôles. Bien que tout contrôle exposant cette propriété peut être rendu multilingue, comme nous l’avons vu avec le bouton de commande, il peut être utile de rendre multilingues d'autres zones "sensibles" du formulaire. Je pense notamment aux infobulles des contrôles de saisie (textbox, listbox, ...) et aux textes qui s'affichent sur la barre d'état lors de l'activation d'un contrôle particulier. Il sera agréable pour un utilisateur que ces textes lui soient affichés dans sa langue. Nous allons donc adapter nos procédures et nos tables pour pouvoir manipuler facilement ces contrôles et les rendre multilingues.
Jusqu'ici, nous avons considéré que les contrôles gérés par le multilinguisme étaient des étiquettes dont nous allions modifier la propriété Caption (la légende, dans l'interface Access). Cette approche nous permettait d'avoir une ligne qui adaptait directement la propriété, en manipulant simplement la propriété de l'objet.
Si vous souhaitez manipuler d’autres propriétés, vous allez devoir adapter votre. Il pourrait être tentant de tester le type du contrôle et d'agir en conséquence. Je vous propose une autre technique qui va permettre de manipuler une propriété dont on renseigne le nom.
II-D-1. Deux façons de manipuler une propriété d’un objet▲
Lorsque nous manipulons la propriété Caption, nous travaillons en fait avec l'élément nommé Caption de la collection des propriétés du contrôle. Access expose en effet les propriétés soit en accès direct (Controle.Caption), soit en appelant l'objet au travers de la collection des propriétés de cet objet (Controle.Properties("Caption")). Les deux lignes de code qui suivent donnent le même résultat.
Sub
CaptionProperties
(
)
Debug.Print
Form_fContact.lblName.Caption
Debug.Print
Form_fContact.lblName.Properties
(
"Caption"
)
End
Sub
En adaptant la table des légendes pour définir la propriété dont on spécifie le texte, vous pourrez alors stocker dans la table le texte de n’importe quelle propriété de n’importe quel contrôle de n’importe quel formulaire, pour autant que cette propriété accepte du texte.
Vous comprenez alors toute la puissance de ce que nous mettons en place, puisque, mises à part les quelques modifications mineures que nous allons apporter à LanguageManager, il sera possible de modifier le texte d’une propriété d’un contrôle sans toucher au code !
II-D-2. Infobulles et barre d’état▲
Vous avez déjà approché la propriété Caption. Si vous souhaitez modifier les propriétés d'infobulle ou de texte de barre d'état, vous devez connaître leur nom anglais. Les propriétés Texte d'infobulle et Texte de la barre d'état sont appelées respectivement ControlTypText et StatusBarText, et vous pouvez les adresser directement ou via la collection, comme illustré plus haut pour la propriété Caption.
En ajoutant dans la table des libellés la propriété qui sera modifiée, vous pourrez n'apporter qu'une modification mineure dans le code la procédure LanguageManager.
Vous allez donc ajouter le champ PropertyName à la table des libellés de manière à pouvoir préciser à quelle propriété du contrôle vous souhaitez attribuer le texte du libellé.
Il faut bien entendu ajouter Caption comme PropertyName pour les textes déjà encodés, et compléter les infos pour toutes les propriétés de tous les contrôles dans toutes les langues. Les textes qui ne seraient pas spécifiés seront ceux définis à la création du contrôle.
II-D-3. Modification du code et de la requête▲
Nous devons enfin nous occuper de modifier la procédure, pour récupérer la valeur et le nom de la propriété. Actuellement, nous n'avions qu'une ligne présente dans la table des libellés par contrôle, puisqu'il s'agissait de modifier la seule propriété Caption. Maintenant, pour certains contrôles, nous avons deux lignes présentes dans la table pour les propriétés ControlTypText et StatusBarText. Nous ne pouvons donc plus nous contenter de rechercher une seule valeur. La seule modification à apporter au code est de modifier la ligne qui transfère le texte du libellé. Auparavant, elle attribuait cette valeur à la propriété Caption. Elle va devoir maintenant l'attribuer à la propriété renseignée dans le champ PropertyName.
En effet, la ligne de code suivante modifie la valeur de la propriété Caption.
FormItem.Controls
(
rs!ControlName).Caption
=
rs!ControlText
Voici la nouvelle ligne de code au sein de la boucle qui parcourt les contrôles du formulaire renseignés dans la table.
FormItem.Controls
(
rs!ControlName).Properties
(
rs!Propertyname) =
rs!ControlText
Tant que vous y êtes, vous utiliserez également la collection Properties pour adapter la propriété Caption du formulaire. Cela unifiera le code et vous verrez plus loin dans le tutoriel pourquoi il est intéressant de procéder ainsi. A ce stade, voici le code de LanguageManager.
Sub
LanguageManager
(
Item As
Form)
Dim
Ctrl As
Control
Dim
rs As
DAO.Recordset
Set
rs =
CurrentDb.OpenRecordset
(
"select * from aq_languageTexts "
&
_
"where ((Objectname = '"
&
Item.Name
&
"') and
(
Initials =
'" & gUserLanguage & "'))")
Do
While
Not
rs.EOF
If
rs!ControlName =
"form"
Then
Item.Properties
(
rs!propertyname) =
rs!controltext
Else
Item.Controls
(
rs!ControlName).Properties
(
rs!propertyname) =
rs!controltext
End
If
rs.MoveNext
Loop
End
Sub
N’oubliez pas de modifier la requête aq_LanguageTexts pour qu’elle expose la nouvelle colonne PropertyName.
II-D-4. Tests sur le formulaire fContact▲
Après avoir initialisé la variable gUserLanguage en « FR », nous pouvons tester le mécanisme. Après ce test, faites varier la valeur de la variable pour tester également l’anglais.
II-E. ListBox et ComboBox▲
Les ListBox et ComboBox sont un peu particuliers à gérer car ils affichent souvent des données « métier ». Dans des cas relativement rares, les contrôles sont nourris par des listes de valeurs, mais la plupart du temps, ils s’appuient sur des tables ou des requêtes pour exposer à l’utilisateur des séries relativement longues de valeurs.
II-E-1. Listes de valeurs comme source des contrôles de liste▲
Petite précision pour commencer : L’alimentation des contrôles de liste par des valeurs « en dur » est le plus souvent une technique à proscrire puisqu’elle impose une modification structurelle de l’application (modification des entrées dans la liste) lors d’une modification des items composant la liste, que ce soit par ajout, modification ou suppression. Vous limiterez donc les listes en hard coding à celles dont vous êtes certain qu’elles ne seront pas modifiées durant la vie de l’application (par exemple : féminin/masculin). Vous pouvez faire le test : les exemples se comptent sur les doigts d’une main !
Malgré cette mise en garde, vous allez commencer par manipuler ce type de liste. Pourquoi ? Parce que vous n’avez pas grand-chose à faire pour que « ça fonctionne ». Il faut :
- nommer la liste si ce n’est déjà fait (par exemple cboCategory) ;
- modifier sa propriété Origine Source et choisir Liste de valeurs ;
- spécifier les propriétés de la liste déroulante ;
- ajouter le contrôle dans la table des contrôles ;
- ajouter les textes avec le nom de la propriété RowSource.
Pour les propriétés de la liste déroulante, vous pouvez utiliser le tableau suivant :
Propriété | Valeur | Explication |
Origine source | Liste valeurs | Spécifie que la propriété Contenu doit être interprétée comme une liste de valeurs et non comme une requête SQL |
Colonne liée | 1 | Spécifie que la première colonne contient la valeur qui sera affectée au contrôle, indépendamment de la valeur affichée |
Nbre Colonnes | 2 | Votre liste contiendra l’identifiant de la valeur et le texte, celui-ci étant affiché dans la langue de l’utilisateur |
Largeur Colonnes | 0cm ;6cm | Spécifie que la première colonne ne sera pas visible |
Largeur Liste | 6cm | Habituellement, cette largeur est égale à la somme des largeurs des colonnes |
Il n’est pas nécessaire, ici, de modifier le code. Ce qui est mis en place fonctionne et permet à la liste d’être elle aussi multilingue.
Néanmoins, il convient ici de bien comprendre ce qui est mis en place. La valeur saisie dans la liste doit être la même quelle que soit la langue, sinon l’analyse des données sera impossible à réaliser. Il convient donc de créer des listes à au moins deux colonnes, la première reprenant des valeurs communes à toutes les langues utilisées. La conception de la liste des valeurs sera donc réalisée avec beaucoup de précautions.
Pour créer une liste de valeurs avec plusieurs colonnes, on fait suivre les données par ligne de valeurs, en séparant chaque item par un point virgule. C’est la propriétés Nombre de colonnes qui déterminera quand Access doit passer à la ligne.
Les valeurs reprises comme ControlText illustrent bien que Gros client et Big Customer auront bien la même valeur, soit 1, et que c’est cette valeur qui sera contenue dans le contrôle.
Rappel : L’utilisateur choisit Petit client ou Little Customer, mais c’est bien la valeur 2 qui sera stockée dans le contrôle, grâce à la propriété Colonne liée mise à 1.
II-E-2. Table ou requête comme source de valeurs▲
C’est à peine plus compliqué pour une liste qui doit s’appuyer sur une requête. Il faut simplement bien réfléchir à ce qui va être mis en place. Je rappelle donc les objectifs, tant informatiques que métiers :
- permettre à l’utilisateur de dérouler la liste dans sa langue (obj. métier) ;
- faire en sorte que le choix de l’utilisateur soit le même quelle que soit la langue utilisée (obj. métier) ;
- ne pas devoir intervenir dans le code ou dans la structure de l’application lors de l’ajout d’items ou de langues (obj. informatique).
Les premier et troisième points coulent de source. Ils sont l’objet de ce tutoriel. Le deuxième point mérite une petite explication. Pour pouvoir analyser les données.
Plusieurs mécanismes peuvent être mis en place. Celui qui est exposé ici permet de ne pas modifier le code, mais c’est au prix d’une relative complexité dans l’élaboration du texte à stocker dans la table des textes.
II-E-2-a. Tables « métier »▲
Vous devez donc disposer de deux tables supplémentaires, l’une pour contenir les catégories métier, celle qui contiendra les valeurs qui serviront à l’analyse, l’autre pour contenir les textes à afficher dans la liste en fonction de la langue utilisée. Leur création étant assez simple à comprendre, seul le résultat final sera illustré ci-dessous.
Remarquez la clé primaire composite sur la table Category_Language. Si vous observez les deux tables, vous comprenez que l’utilisateur pointera vers Category_PK pour déterminer la catégorie d’un contact, quelle que soit sa version linguistique. Pour cela, vous aurez besoin d’une requête, que vous créerez dans l’IHM. Passer par l’IHM pour la requête permettra de simplifier la saisie du texte dans la table des textes, comme nous le verrons plus loin, et d’utiliser des textes plus courts, une requête avec quelques jointures pouvant vite dépasser les mille caractères.
N’oubliez pas de peupler les deux tables avec quelques données. Pour l’instant, saisissez deux catégories.
La création des listes déroulantes pour les colonnes CategoryFK et LanguageFK ne fait pas partie des objectifs de ce cours. La notation CategoryPK et CategoryFK permet de voir rapidement le lien entre les deux tables. A des fins didactiques, les noms génériques (Entreprise et Particulier diffèrent des libellés francophones pour ces catégories.
II-E-2-b. Requête permettant l’assemblage des données▲
Pour faciliter la mise en place du mécanisme, vous recourrez à une requête créée dans l’IHM. Gardez ici à l’esprit que cette requête doit contenir, en colonne 1, l’identifiant CategoryPK de la catégorie choisie par l’utilisateur, quelle que soit la langue choisie. Il faut aussi que cette requête expose les initiales de la langue, car vous en aurez besoin par la suite. Cette requête doit donc exposer trois champs :
- CategoryPK, issu de la table Category ;
- CategoryText issu de la table Category_Language ;
- Initials, issu de la table a_Language ;
La requête illustre bien que Entreprise commerciale et Company pointent bien toutes deux vers le même identifiant de catégorie.
II-E-2-c. Modification du contrôle dans le formulaire▲
Initialement, cboCategory recevait des valeurs séparées par des points-virgules, et sa propriété Origine Source valait donc Liste Valeurs. Maintenant, vous souhaitez que le combobox s’alimente sur une table, et il faut donc modifier Liste Valeurs qui doit maintenant être table / Requête. Les autres propriétés ne sont pas modifiées.
II-E-2-d. Modification des textes dans la table a_LanguageText▲
Ici, les choses se compliquent un peu. Les listes ne sont plus littérales, mais doivent être modifiées en requête SQL en fonction de la langue choisie. Afin de ne pas devoir adapter le code VBA existant, vous opterez pour une rédaction in extenso de la requête SQL, en ce compris les initiales de la langue. Cette technique est facilitée par la création de la requête qCategoryLanguage que vous avez étudiée plus haut.
Testez à nouveau le mécanisme mis en place en faisant varier la valeur de gUserLanguage.
Pour rappel, les vraies valeurs du combobox sont bien les identifiants CatégoryPK de la table Category, grâce à l’organisation de la requête qCategoryLanguage.
II-E-2-e. Ajout d’une nouvelle catégorie▲
A nouveau, l’ajout d’une catégorie s’effectue sans toucher au code. Il suffit d’ajouter la catégorie générique dans Category et ses versions linguistiques dans CategoryLanguage.
II-F. Conclusions sur les formulaires▲
Cette première partie, assez fournie, vous a permis de mettre en place une technique simple et maintenable pour offrir une expérience utilisateur intéressante (ça fait bien, hein !).
Ce mécanisme s’appuie sur quelques tables et un peu de code. Pour qu’un nouveau formulaire bénéficie du multilinguisme, vous devez ajouter les données dans les différentes tables et insérer une ligne de code dans la procédure OnLoad du formulaire concerné. Par la suite, l’ajout d’une langue ou de données dans les listes ne nécessitent aucune modification structurelle de votre application. Vous verrez en continuant votre lecture que vous pouvez confier la traduction des termes à un tiers non informaticien, par exemple dans un fichier Excel ou XML, grâce à une technique simple d’importation des données.
III. Transposition aux rapports▲
III-A. Introduction▲
Vous allez voir que la transposition de la technique aux rapports (états) n’est pas bien compliquée. Vous pourriez penser qu’elle se limite à prévoir les textes des étiquettes, puisque le rapport n’est pas interactif avec l’utilisateur, ce qui veut dire qu’il n’y a pas de listes déroulantes, mais c’est sans compter sur les versions linguistiques des données métier, comme par exemple les catégories de contact.
III-B. Création d’une table des contacts▲
Pour pouvoir imprimer un état avec des données, il faut… des données. Vous allez donc créer une table des contacts permettant de spécifier le nom, le prénom et la catégorie de vos contacts. J’espère que la création et le peuplement de la table ne vous pas de problèmes.
Pour alimenter l’état, je vous conseille de passer par une requête qui alimentera l’état avec, pour l’instant, les libellés génériques des catégories. Cela vous évite d’obtenir une liste déroulante dans les contrôles de l’état, et d’obtenir à la place une vraie zone de texte, qu’il faut donc alimenter avec du texte.
III-C. Création d’un rapport continu▲
III-C-1. Du côté de l’interface▲
Du côté de l’interface, ce n’est pas compliqué. Il vous suffit de créer une requête incluant les talbes Contact et Categoy, de manière à pouvoir exposer les champs souhaités. L’état étant créé, il faut le modifier pour adapter la mise en forme, mais aussi, et surtout, pour nommer correctement les étiquettes qui devront être adaptées selon le choix linguistique de l’utilisateur.
Les étiquettes, tant pour le titre du rapport que pour les colonnes, doivent être renommées et les noms seront ajoutés dans les tables applicatives.
III-C-2. Du côté du code VBA de l’état▲
Le code VBA est identique à celui des formulaires et consiste simplement à appeler la procédure de modification des textes en n’oubliant de lui passer l’objet report, c’est-à-dire Me.
Private Sub Report_Load()
LanguageManager Me
End Sub
A ce stade, n’essayez pas d’ouvrir l’état en mode visualisation, vous auriez une erreur dont vous allez très bientôt comprendre la raison.
III-D. Ajout des données dans les tables applicatives▲
Dans les tables, il faut ajouter quatre étiquettes, celle du titre et les trois des colonnes, mais aussi l’état en lui-même, que nous nommerons Report par convention.
III-E. Adaptation du code de LanguageManager▲
Le code de la procédure LanguageManager contient une référence explicite à un formulaire pour le paramètre passé. Or, vous devrez maintenant lui passer soit un formulaire, soit un état. De plus, une ligne de test vérifie que le « contrôle » est form pour pouvoir modifier le titre du bandeau de la fenêtre. Vous avez donc deux modifications à réaliser dans le code :
- Sub LanguageManager(Item As Object) (signature de la méthode) ;
- If rs!ControlName = "form" Or rs!ControlName = "Report" Then (test d’une condition alternative).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
Sub LanguageManager(Item As Object)
Dim Ctrl As Control
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("select * from aq_languageTexts " & _
"where ((Objectname = '" & Item.Name & "') and
(Initials = '" & gUserLanguage & "'))")
Do While Not rs.EOF
If rs!ControlName = "form" Or rs!ControlName = "Report" Then
Item.Properties(rs!propertyname) = rs!controltext
Else
Item.Controls(rs!ControlName).Properties(rs!Propertyname) =
rs!controltext
End If
rs.MoveNext
Loop
End Sub
Si vous visualisez le rapport (n’oubliez pas d’initialiser et de faire varier la valeur de gUserLanguage, vous remarquez que tout se passe bien au niveau des bandeau et étiquettes, mais pas au niveau des catégories, qui sont les catégories génériques et non les versions linguistiques.
III-F. Problème de la source unilingue du rapport▲
III-F-1. Source du problème▲
Le problème vient du fait que la requête du rapport utilise le champ de la table Category alors qu’elle devrait utiliser le champ de la table Category_Language. Mais bien évidemment, vous ne connaissez pas à l’avance la langue qui sera utilisée. Ce n’est qu’au chargement du rapport que la langue sera connue via la variable gUserLanguage.
III-F-2. Solution par requête filtrée dans la table applicative▲
Jusqu’ici, vous n’avez utilisé pour les formulaires et les rapports que la propriété caption des « faux contrôles » Form et Report. Vous allez donc utiliser une nouvelle propriété dans la table a_Language_Text appelée RecordSource et vous lui passerez comme texte la requête sql filtrée sur la langue.
Cette requête sql utilisera la requête qContacts précédemment créée, mais que nous devrons adapter. Initialement, elle exposait le champ Category.Categoryname et elle doit maintenant exposer les champs Category_Language.CategoryText et Language.Initials.
Vous observerez que les données apparaissent autant de fois qu’il y a de langues pour les catégories, mais le filtre sur la langue supprimera bien les doublons.
Dans la table a_LanguageText, il faudra ajouter le texte pour la nouvelle propriété RecordSource, sur le même modèle que ce que vous aviez réalisé pour les listes déroulantes.
III-G. Test du rapport▲
Ouf ! Tout est fini. Nous pouvons tester l’état, en français puis en anglais.