Flash Player 8/AS 2 et Localization: ma méthode

Tout d’abord, pourquoi un article sur Flash Player 8 et Actionscript 2, à l’heure de l’apogée de la technologie Flex/AS3 ? Réponse simple, au vu des statistiques de pénétration du player. En effet, Flex c’est génial, mais beaucoup d’utilisateurs sont encore munis de la version 8 du player, incompatible avec Flex.

Je viens donc de travailler sur une application dont la principale particularité est qu’elle est codée uniquement en AS2, pour cibler la majorité des utilisateurs. Le problème que j’ai rencontré est le suivant : je devais internationaliser l’interface, c’est-à-dire permettre de changer de langue à n’importe quel moment. Alors vous me direz, avec Flex c’est facile, avec les ResourceBundle. Certes, mais pour ce qui est de l’AS2, c’est une autre histoire...

J’ai donc fais pas mal de recherches sur le net, mais finalement rien de concluant : la majorité des solutions que j’ai trouvées consistait à créer un tableau, global à l’application, rescençant une liste de mot-clés associés à une langue. Ensuite, une fonction était chargée de changer la valeur dans toutes les zones de textes impliquées. C’est le principe utilisé par la classe la plus courante, distribuée par Shaoken. Ceci est parfois très pratique, sauf que dans mon cas, la liste des champs à manipuler est assez importante et répartie dans des profondeurs de clip d’autant plus complexe. Enfin mon application étant destinée à être customisée par des clients, je n’ai aucun moyen de connaitre précisément la liste des champs mis en place… Un vrai dilemne n’est-ce pas?

La seule solution qui s’offre donc à moi, est de me reposer sur un modèle évènementiel, comme notre cher Flex. Seconde embuche! Flash est plutôt capricieux quand il s’agit de gérer des évènements. Mais voilà mon raisonnement :

Il faudrait que j’utilise un objet global à mon application, qui gèrerait seul tout cet aspet de localization (langue courante, dictionnaire…). De plus, il faudrait que tout mes champs se mettent à jour lorsque l’utilisateur appuie sur un bouton. Mais je ne sais connait pas la liste des champs dans l’appli. Il faut donc que ce soit eux-mêmes qui prennent connaissance du changement de langue.

Donc deux étapes : créer cet objet global, jusque là rien de compliqué, une simple classe suffira. Cependant, il devra être capable de diffuser l’évènement du changement de langue... Il faudra donc étendre la classe EventDispatcher. Ensuite, pour les champs je vais devoir créer un composant, basé sur un champ texte dynamique, mais capable de répondre à un évènement. Soit!

Parlons peu, parlons bien. Commençons par la classe gérant la localization :

import mx.events.EventDispatcher;
import mx.xpath.XPathAPI;
import mx.utils.Delegate;
 
class data.com.Localizer extends EventDispatcher {
 
 private var dispatchEvent:Function;
 private var dispatchQueue:Function;
 public var addEventListener:Function;
 public var removeEventListener:Function;
 
 private var localeXML:XML;
 private var dictionnary:Object;
 
 public var language = "fr";
 public var dictionnaryPath = "";
 
 function Localizer(){
  mx.events.EventDispatcher.initialize(this);
  this.dictionnary = new Object();
  this.localeXML = new XML();
  this.localeXML.ignoreWhite = true;
  this.localeXML.onLoad = Delegate.create(this, dotranslate);
 }
 
 function loadDico(){
  this.localeXML.load(this.dictionnaryPath);
 }
 
 function dotranslate(loaded:Boolean){
  if(loaded){
   translate();
  } else {
   trace("Error loading dictionnary!");
  }
 }
 
 function translate(){
  var aNodes:Array = this.localeXML.firstChild.childNodes;
  var nMaxNodes:Number = aNodes.length;
  for (var i:Number = 0; i < nMaxNodes; i++){
   var sVarName:String = aNodes[i].attributes.id;
   var sVarValue = XPathAPI.selectSingleNode(aNodes[i], "/*/"+this.language).firstChild.nodeValue;
   if (sVarValue == undefined)
    sVarValue = XPathAPI.selectSingleNode(aNodes[i], "/*/default").firstChild.nodeValue;
   if (sVarValue == undefined) {
    trace("Localized XML node can't be selected, please verify your language or set a default value tag");
   }
   this.dictionnary[sVarName] = sVarValue;
 
  }
  dispatchEvent({target:this, type:"languageChanged"});
 }
}

Comme vous pouvez le voir, cette class n’est pas bien complexe et repose sur le code de Shaoken. Voici son fonctionnement : à la création de l’objet, j’initialise la classe EventDispatcher pour pouvoir ensuite utiliser ses méthodes. Le fichier XML "localeXML" contient le dictionnaire, nous verrons sa syntaxe juste après. La méthode "translate()" parse le fichier et remplit le tableau "this.dictionnary" en fonction de la langue choisie. Une fois le tableau remplit, on envoie l’évènement avertissant que la traduction est disponible.

Regardons maintenant la structure du XML en question :



 Traduire
 Translate
 Traduire


 ...


Pour chaque variable, on a la liste des textes dans chanque langue.

Passons maintenant au plus important, le composant! :

#initclip
import mx.utils.Delegate;
 
LTextField.prototype = new MovieClip();
 
function LTextField () {
 this.textFormatter = new TextFormat();
 this.init();
}
 
LTextField.prototype.init = function () {
 _root.languageWizard.addEventListener("languageChanged",Delegate.create(this,LTextField.prototype.translation));
}
 
LTextField.prototype.translation = function () {
 this.label_txt.text = _root.languageWizard.dictionnary[this.labelKey];
 this.textFormatter.size = this.fontSize;
 this.textFormatter.align = this.halign;
 this.label_txt.setTextFormat(this.textFormatter);
}
 
// Connect the class with the linkage ID for this movie clip
Object.registerClass("LocalizedTextField", LTextField);
#endinitclip

J’ai ainsi créé un simple clip, avec sur la scène un DynamicTextField nommé : "label_txt". Voici le principe : quand le composant est créé sur la scène, le constructeur est appelé, puis la fonction "init()" qui va ajouter sur notre objet global un écouteur de l’évènement de changement de langue, lié à la fonction d’actualisation du composant ("translation()"). Ainsi, je découple complètement le gestionnaire des objets acteurs.

Je peux donc placer mes champs texte n’importe où dans l’application, à n’importe quel niveau, puisqu’ils sont directement liés à mon objet global. Ce dernier est créé par une méthode aussi simple que :

_root.languageWizard = new Localizer();
_root.languageWizard.dictionnaryPath = "data/localization.xml";
_root.languageWizard.loadDico();

Ensuite, voici le comportement du bouton permettant de passer d’une langue à l’autre :

function triggerLocale(){
 if(_root.languageWizard.language=="fr"){
  _root.languageWizard.language = "en";
  cbtTranslate.gotoAndStop(2);
 } else {
  _root.languageWizard.language = "fr";
  cbtTranslate.gotoAndStop(1);
 }
 _root.languageWizard.translate();
}
 
cbtTranslate.onPress = triggerLocale;

Maintenant, et parce que cela n’a pas été aussi simple qu’il n’y parait, je vous vous lister l’ensemble des difficultés que j’ai eu à créer ce code, afin de vous les épargner si vous avez à le réutiliser! :

  1. Pour ceux qui auraient oublié comment créer un composant dans Flash, voici un très bon tuto.
  2. Pour créer un objet capable de diffuser des évènements, il est indispensable d’étendre la classe EventDispatcher, et de déclarer toutes ses méthodes (removeEventListener…)
  3. Depuis le composant, lorsque l’on affecte un écouteur à l’objet global, il est impératif d’utiliser la classe Delagate, sans quoi Flash essaiera de trouver la méthode indiquée au même niveau que celui de l’objet (donc au niveau global (_root) et non pas dans le composant…)
  4. Enfin, je m’excuse auprès des puristes qui militent conter l’utilisation du _root, mais c’est une souplesse erreur que nous permet encore l’AS2

Fichier(s) joint(s) :

0 commentaires: