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) :



Aperçu des webservices de métadonnées musicales

Il y a deux jours, je me suis demandé : "Existe-t-il un/des webservice(s) qui permettraient de récupérer des informations sur un artiste/album/titre ?" La réponse est Oui! Voici donc un petit aperçu des trois grands webservices :

MusicBrainz

Musicbrainz est une base de données d’informations, entretenue par des utilisateurs, accessible depuis un navigateur web ou différents types d’application (lecteur multimédia, webservice…)

Son atout majeur est qu’il est accessible totalement librement, c’est-à-dire qu’il est le seul à ne pas nécessiter d’identification par une quelconque API_KEY. De plus, étant géré par des utilisateurs, il est en constante évolution et propose une quantité d’informations très intéressante.

Le point négatif est qu’il ne fournit pas toujours les informations désirées : si, par exemple, on cherche à récupérer la track list d’un album particulier, cela se complique un peu… Je n’ai pas tout exploré en détail mais je suppose que cela est du au fait que l’API n’est pas tout à fait complète.

La documentation sur l’API du service est cependant assez claire, même si elle mériterait à mon sens d’être un peu plus exhaustive et illustrée d’exemples.

LastFM

Le fameux service de diffusion audio en ligne propose également une API pour son webservice. Il propose des informations et des méthodes très intéressantes, mais à mon avis, un peu trop calquées sur ce que propose le site en lui-même : recherche des titres les mieux notés pour un artiste, des albums les plus écoutés pour un artiste… Il est donc impossible par exemple de récupérer la track list (oui encore! c’est ce que je cherche en fait) pour un album.

Je pense que ce service gagnerait à se généraliser un peu, même s’il est certain que les informations qu’il fournit sont déjà très pertinentes pour un site d’informations musicales avec classement des titres, artistes, etc.

Yahoo! Music

Dans le cadre de son Developer Network, le portail/moteur de recherche met également à disposition de tous une base d’informations musicales. Son API est un peu moins simple d’utilisation que les autres puisque moins basée sur le principe de folksonomy.

Elle permet en tout cas d’avoir des informations un peu plus génériques sur les artistes/albums.

Je ne l’ai pas encore beaucoup testé, mais il semble qu’il devient rapidement complexe de rechercher des informations précises, selon plusieurs critères : il est possible d’utiliser dans l’URL de recherche un paramètre &intersectsWith= pour multiplier les critères, mais encore une fois, la documentation gagnerait à être un peu plus claire et fournie.

Dans tous les cas, je continuerai dans les prochains jours à compléter cet article au fur et à mesure des mes expérimentations. Si vous avez déjà pratiquez l’un de ses webservices, n’hésitez pas à m’en faire part!


Fichier(s) joint(s) :