import andromeda.i18n.Lang ;
 
trace( "Lang.ES.toString() : " + Lang.ES.toString() ) ; // es
trace( "Lang.ES.valueOf()  : " + Lang.ES.valueOf() ) ; // es
trace( "Lang.ES.toSource() : " + Lang.ES.toSource() ) ; // new asgard.system.Lang("es","Spanish")
trace( "Lang.ES.label : " + Lang.ES.label ) ; // Spanish
 
trace( "Lang.get('fr') == Lang.FR : " + ( Lang.get("fr") == Lang.FR ) ) ; // true
trace( "Lang.validate('fr') : " + Lang.validate('fr') ) ; // true
trace( "Lang.validate( Lang.FR ) : " + Lang.validate( Lang.FR ) ) ; // true
 
trace( "Lang.LANGS : " + Lang.LANGS ) ;
// {pl:pl,nl:nl,es:es,tr:tr,it:it,da:da,pt:pt,fi:fi,zh-CN:zh-CN,no:no,ja:ja,de:de,ru:ru,fr:fr,zh-TW:zh-TW,xu:xu,ko:ko,en:en,sv:sv,cs:cs,hu:hu}
 
trace( "Lang.LANGS.size() : " + Lang.LANGS.size() ) ; // 21

2 - Fichier externe et nomenclature

Les données externes permettant la création des différentes versions du contenu de l'application localisée doivent être par défaut au format eden dans vos applications développées avec VEGAS. Le format eden permet de créer rapidement des fichiers de configuration simples. Je ne vais pas entrer dans le détail ici sur ce format mais pour résumer rapidement son action, je peux simplement illustrer ce format par un petit exemple.

De façon général, j'utilise une nomenclature spécifique pour créer mes fichiers textes externes de localisation. En effet si par exemple je cherche à réaliser un fichier de configuration en français avec un contexte spécifique : "locale/localize_fr.eden".

Nous isolons donc cette notation en la divisant en 4 éléments distincts : {path}{prefix}{lang}[suffix}

J'ai centralisé ces éléments dans une interface simple qui permet de créer facilement des classes qui permettent de charger les fichiers de localisation. Cette interface en AS3 est l'interface andromeda.i18n.ILocalizationLoader qui définie les propriétés et méthodes suivantes :

/**
         * (Read-write) Indicates the Localization object.
         */
        function get localization():Localization ;
        function set localization( localization:Localization ):void ;
 
        /**
         * (Read-write) The path of the localization file.
         */
        function get path():String ;
        function set path( value:String ):void ;
 
        /**
         * Determinates the prefix value of the localization files.
         */
        function get prefix():String ;
        function set prefix(s:String):void ;
 
        /**
         * (Read-write) The suffix of the localization file.
         */
        function get suffix():String ;
        function set suffix( value:String ):void ;
 
        /**
         * Sends and loads data from the specified passed-in lang value (the passed-in argument must be a Lang reference or a valid string).
         * @param lang The localization Lang value.
         */
        function loadLang( lang:* ):void

Ainsi par défaut vous pouvez utiliser la classe EdenLocalizationLoader ou la classe JSONLocalizationLoader pour charger vos fichiers de localisation et mettre à jour automatiquement la ou les moteur de localisation de votre application.

Donc imaginons que j'ai une application qui doit contenir une contenu en version française et anglaise, je vais donc créer 2 fichiers textes au format eden comme ceci :

Contenu du fichier localize_fr.eden (français) :

{
 
    lang        : "fr" ,
    namespace   : "http://www.ekameleon.net/2008/andromeda" ,
    version     : "1.0.0.0" ,
 
    type : "eden" ,
 
    door :
    {
        connectStarted    : "DEBUT CONNEXION" ,
        connectFinished   : "FIN CONNEXION" ,       
        connectInProgress : "CONNEXION EN COURS" ,       
        connectOutOfTime  : "DELAIS DE CONNEXION DEPASSE"
    }
    ,
    info :
 
    {
        finish_loading   : "Fin du chargement" ,
        progress_loading :  "Chargement en cours" ,
        start_loading    :  "Début du chargement"
    }
    ,
    myField :
    {
        selectable : true ,
        text : "Bonjour le monde"
    }
 
}

Contenu du fichier localize_en.eden (anglais) :

{
 
    lang        : "en" ,
    namespace   : "http://www.ekameleon.net/2008/andromeda" ,
    version     : "1.0.0.0" ,
 
    type : "eden" ,
 
    door :
    { 
        connectStarted    : "START CONNECTION" ,
        connectFinished   : "FINISH CONNECTION" ,       
        connectInProgress : "CONNECTION IN PROGRESS" ,       
        connectOutOfTime  : "THE CONNECTION IS OUT OF TIME"
 
    }
    ,
    info :
    {
        finish_loading   : "THE LOADING IS FINISHED" ,
        progress_loading : "THE LOADING IS IN PROGRESS" ,
        start_loading    : "THE LOADING IS STARTED"
    }
    ,
    myField :
    {
        selectable : false ,
        text       : "Hello world"
    }
 
}

Les 2 fichiers au dessus contiennent donc un objet ECMAScript totalement compatible avec un objet défini en ActionScript ou Javascript. Il est tout à fait possible d'ailleurs de copier/coller le contenu des fichiers texte et de le tester rapidement sur un script de scénario dans Flash par exemple. J'apprécie avant toute chose cette notation car contrairement au XML ou autre, elle permet d'être testée et validée rapidement.. surtout par un développeur ActionScript débutant ou non ;)

Comme le format JSON, le format eden permet d'utiliser une notation ECMAScript et donc d'utiliser des objets simples de type Object, Array, Number, Boolean, String, Date, etc... mais aussi des objets avec un type personnalisé.

Par exemple si l'on souhaite définir un format de texte spécifique pour un champ de texte dynamique dans l'application en fonction de la langue il sera possible dans le fichier de localisation au format eden d'ajouter un attribut "format" comme ceci :

{
 
    format : new flash.text.TextFormat("arial", 12) , // utilisation d'un objet de type "custom"
 
    // etc...
}

Le format eden permet de réaliser bien d'autres choses .. mais j'écrirai un tutoriel complet sur le sujet et il serait vraiment dommage de bâcler mes explications sur le sujet ici.

3 - La classe Localization

La classe andromeda.i18n.Localization centralise le moteur de localisation de VEGAS avec une gestion globale via multi-singletons des références de type Localization. En effet il est possible de créer plusieurs références distinctes et globales pour gérer de façon séparé plusieurs localisations dans une même application. Malgré cela je vous avoue qu'il est assez rare d'utiliser plusieurs référence de type Localization dans une même application.

La classe Localization contient donc une méthode statique "getInstance(id:String)" qui renvoie à tout moment un singleton de type Localization dans le code de l'application.

Exemple :

var localization:Localization  = Localization.getInstance() ;

Je vous conseille de lire tranquillement la documentation de cette classe avant de lire la suite : http://www.ekameleon.net/vegas/docs/andromeda/i18n/Localization.html

La classe Localization contient une HashMap qui lui permet de stocker simplement des objets de type andromeda.i18n.Locale dynamiques qui contiendront les propriétés chargées dans les fichiers de texte externes en fonction des "code langues" utilisés dans l'application.

La classe Locale est une classe très très simple et surtout "dynamique" qui permet de créer des objets typés compatibles avec le moteur de localisation. Les instances de type Locale servent avant tout à récupérer les données contenus dans les objets externes.

La classe Localization peut diffuser des événements, le plus important de tous est l'évènement LocalizationEvent.CHANGE qui notifie la mise à jour du moteur de localisation. L'application utilisateur peut ainsi changer si la langue courante du moteur change.

Exemple :

import andromeda.i18n.Localization ;
import andromeda.events.LocalizationEvent ;
 
var change:Function = function( e:LocalizationEvent )
{
    trace( e ) ; // changer les vues de l'application maintenant
}
 
var localization:Localization = Localization.getInstance() ;
localization.addEventListener( LocalizationEvent.CHANGE , change ) ;

Voyons maintenant de plus prêt comment fonctionne le moteur de localisation. La classe Localization possède une gestion des langues par via un buffer, pour toutes les langues utilisées dans l'application un objet de type Locale correspond et permet de récupérer simplement les textes et données. Si pour un identifiant de langue donné l'instance de type Localization ne possède pas encore les données (aucun objet Locale existe encore), alors la Localization utilise un objet spécialisé qui va charger automatiquement les données nécessaires dans le fichier externe correspondant à la langue courante sélectionnée.

Pour changer à tout moment de langue courante il suffit donc sur l'objet de type Localization d'utiliser la propriété virtuelle "current" comme ceci :

Localization.getInstance().current = Lang.FR ; // français avec utilisation de l'énumération Lang.

ou directement :

Localization.getInstance().current = "en" ; // anglais utilisation directe d'un identifiant String valide.

Pour charger les données externes, la localisation utilise en interne un chargeur qui implémente l'interface andromeda.i18n.ILocalizationLoader comme défini au début de cet tutoriel. Il est possible à tout moment de récupérer ou modifier la référence interne du loader de la Localization via la propriété "loader" :

var loader:EdenLocalizationLoader = localization.loader as EdenLocalizationLoader ;
 
loader.addEventListener( ActionEvent.START , debug ) ;
loader.addEventListener( ActionEvent.FINISH , debug ) ;
 
loader.path = "locale/" ;

Dans ce petit bout de code je récupère la référence du chargeur et j'écoute les événements ActionEvent.START et ActionEvent.FINISH qui notifient le début et la fin du chargement opéré par le loader. Je modifie aussi la propriété "path" de l'objet pour définir un chemin différent pour le fichier "localize_ID.eden" que je vais charger quand la propriété "current" sera utilisé pour la première fois pour un identifiant de langue donné.

A noter que si pour un identifiant de langue, l'objet de type Locale existe déjà dans la Localization, l'évènement LocalizationEvent.CHANGE est directement invoqué étant donné que le moteur n'a pas besoin de charger à nouveau les données. Cette optimisation permet d'éviter certaines lourdeurs dans une application. Il est évidant qu'il est assez rare de devoir modifier plusieurs fois dynamiquement le contenu pour une langue donnée des données externes d'une même application.

En cas de soucis il est tout de même possible à tout moment d'utiliser la méthode "remove( id )" pour vider pour un identifiant de langue spécifique le buffer interne de la Localization. Par la suite si la propriété "current" est réutilisé pour le même identifiant de langue, les données externes seront chargées à nouveau et remplies dans le buffer du moteur de localisation.

Localization.getInstance().remove( Lang.FR ) ; // vider le cache
 
Localization.getInstance().current = Lang.FR ; // recharger et remettre à jour la vue

4 - La méthode Localization.getLocale()

Dans les points précédents nous avons pu voir comment charger les données externes et modifier à tout moment la langue courante du moteur de localisation avec notification de ce changement par un événement. Maintenant il reste à comprendre comment récupérer les données contenues dans l'objet de type Locale défini pour la langue courante.

Pour récupérer à tout moment l'objet de type Locale il suffit donc d'utiliser la méthode getLocale() comme ceci :

var locale:Locale = Localization.getInstance().getLocale() ;
 
for (var prop:String in locale)
{
    trace("  * " + prop+ " : " + locale[prop]) ; // énumération des données contenues dans l'objet 
}

Si par exemple je cherche à récupérer dans les exemples définis au début du tutoriel un texte en particulier je peux taper par exemple directement :

var message:String = Localization.getInstance().getLocale().info.start_loading ;
trace(message) ; // Début du chargement

Maintenant j'ai ajouté une petite fonctionnalité particulière dans cette méthode avec la possibilité d'inclure un paramètre dans la méthode "id" qui permet de localiser plus finement un attribut en particulier dans l'objet Locale, par exemple si je tape :

var message:String = Localization.getInstance().start_loading ;
trace(message) ; // Début du chargement

Je vais récupérer automatiquement dans l'objet "info" contenu dans l'objet Locale la valeur voulue. J'utilise beaucoup cette fonctionnalité avec un câblage un peu particulier de mes éléments graphiques. En effet de façon général tous mes éléments graphiques (clips, champs de texte etc... ) sont identifiables et possèdent tous un identifiant unique qui leur permet d'être unique dans l'application. Mais je me sers beaucoup de cette fonctionnalité pour récupérer pour chacun de mes éléments graphiques le contenu localisé qui leur est destiné.

5 - La méthode Localization.init()

Dernière petite méthode particulière du moteur de localisation avec la méthode "init()", cette méthode est définie comme ceci :

function init( o:Object , id:String=null , callback:Function=null ):void

Elle prend en paramètre les arguments suivant :

  • o : un objet que l'on souhaite mapper avec toutes les valeurs contenues dans la localisation pour un identifiant donné.
  • id : un identifiant de type String qui permet une recherche dans l'objet Locale pour la langue courante et qui permet de copier membre à membre toutes les propriétés contenus dans l'attribut spécifique dans l'objet spécifié dans la méthode. Si cet argument n'est pas défini, la recherche se fait dans l'objet Locale directement.
  • callback : une méthode optionnelle que l'on souhaite invoquer une fois que l'opération est terminée.

6 - Exemple complet.

Voici un exemple simple et complet des fonctionnalités décrites au dessus.

A noter que tous les exemples de ce tutoriel se trouvent dans le repository SVN de VEGAS dans le répertoire AS3/trunk/bin/test/andromeda/i18n . Vous trouverez dans ce répertoire des exemples utilisant eden ou JSON avec les flas pour Flash CS3 et les fichiers textes encodés en UTF8 (voir définition de ces fichiers plus haut). (A noter que vous trouverez un exemple AS2 dans le répertoire AS2/trunk/bin/test/asgard/system du repository SVN du projet)

import andromeda.events.* ;
import asgard.i18n.* ;
 
import asgard.text.CoreTextField ;
 
import flash.text.TextFormat ; 
 
var field:CoreTextField = new CoreTextField("myField") ;
 
field.defaultTextFormat = new TextFormat("arial", 12) ;
 
field.width  = 300 ;
field.height = 20  ;
 
field.x      = 25  ;
 
field.y      = 25  ;
 
 
var debug:Function = function( e:Event ):void
{
    trace("------- " + e.type ) ;
}
 
var change:Function = function( e:LocalizationEvent )
{
    var target:Localization = e.getTarget() ;
    var current:Lang        = e.getCurrent() ;
    var locale:Locale       = e.getLocale() ;
 
    trace("# change current " + current ) ;
    trace("# change locale  " + locale  ) ;
 
    for (var each:String in locale)
    {
        trace("  * " + each + " : " + locale[each]) ;
        for (var prop:String in locale[each])
        {
            trace("    * " + prop + " : " + locale[each][prop]) ;
        }
    }
 
    // map the CoreTextField instance with this id attribute 
 
    Localization.getInstance().init( field , field.id ) ; // field.id == "myField", see this attribute in the localize_fr.eden file for example
 
}
 
var localization:Localization  = Localization.getInstance() ;
localization.addEventListener( LocalizationEvent.CHANGE , change ) ;
 
var loader:EdenLocalizationLoader = localization.loader as EdenLocalizationLoader ;
loader.addEventListener( ActionEvent.START , debug ) ;
loader.addEventListener( ActionEvent.FINISH , debug ) ;
loader.path = "locale/" ;
 
localization.current = Lang.FR ;
 
var onKeyDown:Function = function( e:KeyboardEvent ):void
{
    var code:uint = e.keyCode ;
    switch( code )
    {
        case Keyboard.UP :
        {
            localization.current = Lang.EN ;
            break ;
        }
        case Keyboard.DOWN :
        {
            localization.current = "fr" ;
            break ;
        }
    }
}
 
stage.addEventListener( KeyboardEvent.KEY_DOWN , onKeyDown ) ;

Dans cet exemple j'utilise à peu prêt toutes les fonctionnalités décrites précédemment. A noter que pour le petit test, j'utilise la classe asgard.text.CoreTextField qui est une superclasse de la classe flash.text.TextField. Cette classe implémente l'interface vegas.core.Identifiable comme toutes les implémentations visuelles de ASGard (CoreSprite, CoreMovieClip, CoreBitmap, etc.), ainsi chaque instance possède un identifiant unique "id" dans l'application. Nous nous servons donc de cet identifiant dans les fichiers de configuration ou de localisation de l'application pour retrouver rapidement dans la structure objet de l'objet Locale l'attribut correspondant à l'élément graphique voulu. Voir la ligne de code suivante :

Localization.getInstance().init( field , field.id ) ; // field.id == "myField", see this attribute in the localize_fr.eden file for example

Conclusion

Voilà donc fini mon petit tutoriel sur le moteur de localisation de VEGAS. A noter pour bientôt un tutoriel à peu prêt identique sur le moteur de configuration de VEGAS qui fonctionne en gros de la même manière mais pour gérer un objet singleton global Config.getInstance() qui contient dans une application toutes les valeurs et données importantes chargée au runtime dans un fichier config.eden par exemple. Dans un fichier de configuration on pourra trouver de nombreuses informations importantes (chemin du serveur web pour les connexions HTTP, feuille de style des éléments graphique avec choix des couleurs, taille, position etc... )

A noter que pour me donner vos idées et remarques sur le sujet ou pour toutes les questions concernant précisément l'utilisation de ce moteur, vous pouvez abuser du Google Groups du projet :

Je vous invite également à télécharger mon framework opensource documentaire AST'r qui contient en AS2 et AS3 (avec IOC) des exemples d'utilisation de ce moteur de localisation dans des cas concrets :

PS : il y a de forte chance que j'ajoute prochainement une fonctionnalité pour ajouter dans le fichier Locale courant des données, c'est à dire passer au dessus de la sécurité actuelle qui oblige de vider le buffer pour recharger des données pour une langue donnée. Si vous pensez avoir le besoin d'une telle fonctionnalité n'hésitez pas à me le faire savoir.

A noter le lien sur le Google Group de VEGAS de ce tutoriel :