Créer une application Access multilingue

Dans ce tutoriel, vous apprendrez à créer une application multilingue, permettant à différents utilisateurs d'obtenir des écrans dans leur langue. Vous étudierez les différents aspects du "multilingue", tant au niveau des libellés de formulaires que des infobulles, textes de barres d'état, messages divers et écrans d'aide. Vous apprendrez également à construire un ruban multilingue et à créer des menus contextuels qui tiendront compte de la langue souhaitée par l'utilisateur.

Ce tutoriel ne s'adresse donc pas a priori à un débutant, mais au professionnel confronté à l'adaptation d'un logiciel utilisé par des personnes s'exprimant dans différentes langues.Commentez Donner une note à l'article (0) 

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

Image non disponible
1 Formulaire muni de deux zones de type Textbox

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.

Image non disponible
2 Nommez vos contrôles avec des préfixes explicites

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.

Image non disponible
3 Création du module standard

Le code de déclaration de la variable globale tient en une ligne.

 
CacherSélectionnez
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.

Image non disponible

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.

Image non disponible
4Ajout d'un module de formulaire via le ruban

Vous pouvez également créer le module en modifiant la propriété Avec module de l'onglet Autres des propriétés du formulaire.

Image non disponible
5Ajout d'un module de formulaire via les 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.

Image non disponible
6Le module du formulaire est créé

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.
Image non disponible
7 Choix de l'événement à gérer via les listes déroulantes de VBA...
Image non disponible
8... ou via les événements du formulaire dans l'IHM

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.

 
Sélectionnez
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.

Image non disponible
9 Si gUserLanguage = "FR" (ou en tout cas autre chose que "EN", les libellés apparaissent en français
Image non disponible
10 Si gUserlanguage = "EN", nous avons les libellés en anglais.

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.

Image non disponible
11 Tableau descriptif des tables applicatives
Image non disponible
12 Schéma relationnel des tables applicatives

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.

Image non disponible
13 Tables applicatives remplies pour le formulaire et les contrôles traités

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é.

Image non disponible
14 La requête en mode création...
Image non disponible
15 ... et son résultat

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.

Image non disponible
16 La requête dans le QBE avec les critères...
Image non disponible
17 Et le résultat attendu
Image non disponible
18 Le QBE permet de visualiser la commande SQL littérale

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 :

 
Sélectionnez
  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.

 
Sélectionnez
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 :

 
Sélectionnez
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.
Image non disponible
20 Ajout des données du nouveau contrôle dans les deux tables concernées

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.

Image non disponible
21 Le code existant gère automatiquement le nouveau contrôle en français...
Image non disponible
22... ou en anglais.

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.

Image non disponible

Vous observerez qu'à nouveau, rien ne doit être modifié dans le code pour que le bilinguisme fonctionne.

Image non disponible
23 Le bouton en français...
Image non disponible
24 ... ou en anglais

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.

Image non disponible
25 Les ajouts sont réalisés dans les deux tables concernées

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.

Image non disponible
26 L'écran en espagnol, avec zéro modifications de code

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.

 
Sélectionnez
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.

 
Sélectionnez
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.
Image non disponible

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.
Image non disponible

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.

 
Sélectionnez
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.

Image non disponible
27 Formulaire et sous-formulaire en français...
Image non disponible
28 ... ou en anglais (je n'ai pas traduit en espagnol)

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.

Image non disponible
29 Les données spécifiques du formulaire sont ajoutées dans les tables

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.

vb
Sélectionnez
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.

Image non disponible
30 Le titre du formulaire en français...
Image non disponible
31 ... ou en anglais

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.

 
Sélectionnez
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é.

Image non disponible
32 Le champ est ajouté à la table des textes...
Image non disponible
33... et les textes sont ajoutés pour les nouvelles propriétés

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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

Image non disponible
34 Infobulle et barre de statut en français…
Image non disponible
35 ... ou en anglais, selon la langue précisée

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.

Image non disponible
36 AJout des données dans les tables avec les données communes aux deux langues concernées

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.

Image non disponible
37 Les items de la liste en français...
Image non disponible
38 ... ou en anglais

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.

Image non disponible
39 Les tables "métier" pour la gestion des catégories

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.

Image non disponible
40 Les deux catégories et leurs versions linguistiques

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 ;
Image non disponible
41 La requête permettant le choix d'une catégorie en mode création…
Image non disponible
42 ... et les valeurs linguistiques.

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.

Image non disponible
43 Les valeurs de ControlText reprennent les requêtes SQL

Testez à nouveau le mécanisme mis en place en faisant varier la valeur de gUserLanguage.

Image non disponible
44 Les items de la liste en français…
Image non disponible
45 ... ou en anglais.

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.

Image non disponible
46 Ajout des données dans les tables...
Image non disponible
47 ... Et résultat dans le formulaire, ici en anglais

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.

Image non disponible
48 La table en mode création avec le descriptif de la liste des catégories...
Image non disponible
49... et la visualisation de quelques données avec les catégories GENERIQUES.

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.

Image non disponible

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.

Image non disponible
51 L'état en mode création...
Image non disponible
52... et en mode prévisualisation

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.

vb
Sélectionnez
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.

Image non disponible
53 Données dans les tables applicatives

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).
vb
Sélectionnez
1.
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.

Image non disponible
54 Problème avec les catégories qui affichent les données génériques.

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.

Image non disponible
55 La requête qContacts modifiée...
Image non disponible
56 expose bien les données permettant l'affichage linguistique et le filtre sur la langue

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.

Image non disponible
57 Textes SQL pour la requête du rapport

III-G. Test du rapport

Ouf ! Tout est fini. Nous pouvons tester l'état, en français puis en anglais.

Image non disponible
58 Les catégories apparaissent en français...
Image non disponible
59 ... ou en anglais.

IV. Messages d'information, de question ou d'erreur

V. Ruban et menu contextuel

VI. Formulaires d'aide

VII. Fenêtre de connexion

VIII. Conclusions

IX. Remerciements

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Pierre Fauconnier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.