Cette interface en AS3 est l'interface andromeda.i18n.ILocalizationLoader est définie avec la signature suivante :

/**
 * Indicates the Localization reference of the loader.
 */
function get localization():Localization ;
function set localization( localization:Localization ):void ;
 
/**
 * The path of the external 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 ;
 
/**
 * 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 andromeda.i18n.EdenLocalizationLoader ou la classe andromeda.i18n.JSONLocalizationLoader pour charger vos fichiers de localisation et mettre à jour automatiquement la ou les moteur de localisation de votre application.

Donc imaginons une application qui doit charger des textes et des contenus en version français et anglais, nous pouvons mettre en place 2 fichiers textes (UTF8) au format eden :

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

{
 
    lang        : "fr" ,
    namespace   : "http://www.ekameleon.net/2009/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/2009/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"
    }
}

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.events.LocalizationEvent ;
import andromeda.i18n.Localization ;
 
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 basée sur un "buffer", pour toutes les langues utilisées dans l'application un objet de type Locale est créé 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 n'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" :

import system.events.ActionEvent ;
 
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 et 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 "getLocale" qui permet de localiser plus finement un attribut en particulier dans l'objet Locale, par exemple :

var message:String = Localization.getInstance().getLocale("info.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.

Exemple complet

Voici un exemple simple des fonctionnalités décrites ci-dessus.

A noter que tous les exemples de ce tutoriel se trouvent dans le repository SVN dans le répertoire AS3/trunk/bin/test/andromeda/i18n .

Vous trouverez dans ce répertoire des exemples utilisant eden ou JSON avec les FLA pour Flash et les fichiers textes encodés en UTF8 (voir définition de ces fichiers plus haut).

import andromeda.events.LocalizationEvent ;
 
import andromeda.i18n.EdenLocalizationLoader ;
import andromeda.i18n.Lang ;
import andromeda.i18n.Locale ;
import andromeda.i18n.Localization ;
 
import asgard.text.CoreTextField ;  
 
import flash.text.TextFormat ; 
 
import system.events.ActionEvent ;
 
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.target ;
    var current:Lang        = e.current ;
    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 keyDown:Function = function( e:KeyboardEvent ):void
{
    var code:int = e.keyCode ;
    switch( code )
    {
        case Keyboard.UP :
        {
            localization.current = Lang.EN ;
            break ;
        }
        case Keyboard.DOWN :
        {
            localization.current = "fr" ;
            break ;
        }
    }
}
 
stage.addEventListener( KeyboardEvent.KEY_DOWN , keyDown ) ;

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 system.data.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, exemple :

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

4 - Initialisation du moteur de localisation avec des ressources de types "i18n"

Les ressources de type "i18n" permettent de charger un ou plusieurs fichier de localisation (par défaut au format eden) qui contiennent des variables qui seront utilisées par la suite dans les définitions d'objet de la fabrique mais aussi directement dans l'application en ciblant un singleton de la classe Localization via la méthode statique Localization.getInstance().

Regardons la signature minimale de l'objet générique qu'il est possible de définir dans l'attribut "imports" pour charger une ressource de type "i18n" :

{ resource : "fr" , type  : "i18n" }

Dans cet objet générique il est possible d'utiliser les attributs suivant :

Attributs obligatoires :

  • resource : indique la référence de la langue que l'on souhaite charger (obligatoire), cette chaine de caractère est définie en fonction des langues disponibles dans l'application (fr, en, it, etc.)
  • type : indispensable pour indiquer au chargeur que la ressource est de type "i18n".

Attributs optionnels :

  • path : indique le chemin du fichier à charger (exemple : path:"locale/" ).
  • prefix : Indique le préfixe utilisé pour créer le nom du fichier externe (par défaut le préfixe est défini avec la valeur "localize_")
  • suffix : Représente le nom de l'extension utilisée pour charger le fichier de localisation, par défaut cette extension a pour valeur la chaine de caractère : ".eden".
  • verbose : Un booléean qui active le mode debug de la ressource pour énumérer son contenu une fois chargée dans les logs de l'applications.
  • loader : Représente directement une référence vers une classe de type ILocalizationLoader ou une chaine de caractère qui représente le nom complet d'une classe de type ILocalizationLoader.

A noter que l'attribut "loader" permet de remplacer la classe EdenLocalizationLoader utilisée par défaut par la ressource pour chager le fichier de configuration externe. Il sera donc possible en cas de besoin d'utiliser un autre type de chargeur de localisation (JSON, XML, etc...). Il est préférable d'être un utilisateur avancé du framework pour toucher à cet attribut.

Les ressources de type "i18n" permettent d'initialiser des objets de type "andromeda.i18n.LocalResource" au niveau du chargeur de la fabrique IoC.

Exemple d'utilisation :

Le but de notre exemple est simple :

  • Créer un champ de texte dynamique et l'attacher sur la scène principale.
  • Afficher dans le champ de texte un message localisé en français ou anglais.
  • Avoir la possibilité de changer la langue de l'application à tout moment en appuyant sur une touche du clavier.
  • Enregistrer un événement pour écouter le moteur de localisation de l'application et changer le message du champ de texte en fonction de la langue courante.

Commençons par créer 2 fichiers textes encodés en UTF8 "locale/localize_fr.eden" et "locale/localize_en.eden" permettant respectivement de définir le message que nous voudrons afficher dans le champ de texte dynamique.

Contenu du fichier "localize_fr.eden" (langue française, code "fr") :

field =
{
    text : "Bonjour le monde"
};

Contenu du fichier "localize_en.eden" (langue anglaise, code "en") :

field =
{
    text : "Hello world"
};

Note : Nous pouvons créer autant de fichier de configuration que de langues disponibles dans l'application, il suffira pour chaque langue de standardiser le nom des fichiers textes avec la nomenclature désirée, ici "localize_xx.eden" avec xx le code de la langue définie dans le fichier de configuration au format eden.

Nous pouvons maintenant créer un fichier externe de configuration et de définition de la fabrique IoC de notre application :

imports =
[
    { resource : "fr" , type:"i18n" , prefix : "localize_" , path    :"locale/" , suffix : ".eden" , verbose : true } ,
    { resource : "en" , type:"i18n" , path   : "locale/"   , verbose : true }
] ;
 
objects =
[
    {
        id         : "localization" ,
        type       : "andromeda.i18n.Localization" ,
        singleton  : true ,
        lazyInit   : true ,
        staticFactoryMethod :
        {
            type : "andromeda.i18n.Localization" ,
            name : "getInstance"
        }
    }
    ,
    {
        id         : "field" ,
        type       : "flash.text.TextField" ,
        singleton  : true ,
        lazyInit   : true ,
        properties :
        [
            { name : "autoSize"          , value  : "left"                                           } ,
            { name : "defaultTextFormat" , value  : new flash.text.TextFormat("Arial", 14, 0xEEED2B) } ,
            { name : "text"              , locale : "field.text"                                     } ,
            { name : "x"                 , value  : 25                                               } ,
            { name : "y"                 , value  : 25                                               }
        ]
    }
    ,
    {
        id               : "application" ,
        type             : "examples.Application" ,
        factoryReference : "#root" ,
        singleton        : true ,
        properties       :
        [
            // dependencies
            { name : "field"        , ref : "field"        } ,
            { name : "localization" , ref : "localization" } ,
 
            // childs
             { name : "addChild" , arguments : [ { ref : "field" } ] }
        ]
        ,
        listeners :
        [
            { dispatcher : "localization" , type : "change" , method : "changeLocalization" }
        ]
    }
]

Nous définissons tout d'abord deux ressources de type i18n pour charger directement en mémoire les configurations localisées pour les langues "en" et "fr".

imports =
[
    { resource : "fr" , type:"i18n" , prefix : "localize_" , path    :"locale/" , suffix : ".eden" , verbose : true } ,
    { resource : "en" , type:"i18n" , path   : "locale/"   , verbose : true }
] ;

Il ne faut pas oublier que le chargement des ressources se fait de bas en haut dans l'objet "imports". La dernière langue chargée (ici "fr") sera donc la langue courante par défaut de l'application une fois la fabrique IoC initialisée.

En utilisant un chargeur de type ECMAObjectLoader par défaut c'est le singleton de la classe andromeda.i18n.Localization défini avec la méthode statique Localization.getInstance() qui est initialisé. Il est bien entendu tout à fait possible de modifier l'objet de localisation utilisé dans le loader de la fabrique.

Nous avons vu que la fabrique IoC contient par défaut une référence magique #locale mais aussi un attribut dans les définitions d'objet "locale" qui permet de cibler à tout moment l'objet courant de type andromeda.i18n.Locale de la localisation si une langue courante est définie et initialisée avec un fichier externe de configuration.

{
    id         : "field" ,
    type       : "flash.text.TextField" ,
    singleton  : true ,
    lazyInit   : true ,
    properties :
    [
        { name : "autoSize"          , value  : "left"                                           } ,
        { name : "defaultTextFormat" , value  : new flash.text.TextFormat("Arial", 14, 0xEEED2B) } ,
        { name : "text"              , locale : "field.text"                                     } ,
        { name : "x"                 , value  : 25                                               } ,
        { name : "y"                 , value  : 25                                               }
    ]
}

Nous voyons dans la définition d'objet précédente l'utilisation très simple de l'attribut "locale" qui permet de récupérer dans la localisation courante la valeur de la chaine de caractère field.text défini dans nos fichiers eden localisés au début de cet exemple.

Nous pouvons donc définir facilement la valeur d'un champ de texte ou l'url d'une image ou d'un swf externe en ciblant l'objet de localisation courant. Nous trouvons donc ici une réponse à la problématique d'injection de dépendance entre un objet défini dans l'application et un objet ou une valeur définie dans la localisation courante.

A noter que le context IoC externe ci-dessus est chargé avec le code ActionScript suivant :

package examples
    {
        import andromeda.events.LocalizationEvent;
        import andromeda.i18n.Lang;
        import andromeda.i18n.Localization;
 
        import asgard.net.ECMAObjectLoader;
 
        import flash.display.MovieClip;
        import flash.events.KeyboardEvent;
        import flash.text.TextField;
        import flash.ui.Keyboard;
 
        public class Application extends MovieClip
        {
            public function Application()
            {
                var loader:ECMAObjectLoader = new ECMAObjectLoader( "application_i18n_resource.eden" , "context/" ) ;
 
                loader.root = this ;
 
                loader.run() ;
 
                stage.addEventListener( KeyboardEvent.KEY_DOWN , keyDown ) ;
            }
 
            /**
             * The TextField reference.
             */
            public var field:TextField ;
 
            /**
             * The Localization reference of the application to change the current lang.
             */
            public var localization:Localization ;
 
            /**
             * Invoked when the localization of the application is changed.
             */
            public function changeLocalization( e:LocalizationEvent ):void
            {
                trace( e ) ;
                if( field != null )
                {
                    field.text = e.getLocale("field.text") ;
                }
            }
 
            /**
             * Invoked to change the current lang with the Keyboard.UP and Keyboard.DOWN keys.
             */
            public function keyDown( 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 ;
                    }
                }
            }
        }
    }

Nous avons donc défini la classe "main" de notre application avec la classe examples.Application qui hérite de la classe flash.display.MovieClip.

{
    id               : "application" ,
    type             : "examples.Application" ,
    factoryReference : "#root" ,
    singleton        : true ,
    properties       :
    [
        // dependencies
        { name : "field"        , ref : "field"        } ,
        { name : "localization" , ref : "localization" } ,
 
        // childs
        { name : "addChild" , arguments : [ { ref : "field" } ] }
    ]
    ,
    listeners :
    [
        { dispatcher : "localization" , type : "change" , method : "changeLocalization" }
    ]
}

Nous noterons donc 2 dépendances sur cette définition d'objet avec les propriétés :

  • field : permet de créer une référence vers le champ de texte "field" défini dans la fabrique IoC avec la définition d'objet du même nom.
  • localization : permet de créer une référence vers la localisation de l'application et ainsi de pouvoir à tout moment changer la langue courante.

La localisation de l'application peut être définie dans le conteneur IoC et référencée très simplement en utilisant une définition d'objet utilisant une stratégie de type staticFactoryMethod :

{
    id         : "localization" ,
    type       : "andromeda.i18n.Localization" ,
    singleton  : true ,
    lazyInit   : true ,
    staticFactoryMethod :
    {
        type : "andromeda.i18n.Localization" ,
        name : "getInstance"
    }
}

A noter que nous utilisons un attribut "listeners" pour écouter la localisation de l'application et être notifié à tout moment du changement de langue courante avec la méthode changeLocalization définie dans la classe examples.Application.

listeners :
[
    { dispatcher : "localization" , type : "change" , method : "changeLocalization" }
]

Notes

Voici donc après quelques mois de retard cette nouvelle partie du chapitre sur les ressources externes de type "i18n" dans le moteur de ressources du chargeur de context IoC de VEGAS.

Pour cet article je ne vais pas dupliquer les liens vers les articles se trouvant sur mon blog mais je préfère vous rediriger vers Google Documents et vers les pages de mon e-book (toujours en pleine évolution) qui regroupe l'ensemble des chapitres concernant le design pattern d'injection de dépendance et de son utilisation avec VEGAS.

A noter qu'il est préférable de consulter les articles sur google code car ils sont sans arrêt mis à jour en cas de problème (faute d'orthographe, changement dans le code source des exemples ou dans l'API du framework).

Pour toute question comme toujours sur VEGAS je vous conseille de vous inscrire sur le Google Groups du projet : http://groups.google.com/group/vegasos

En cas de soucis dans cette documentation ou dans le code source de VEGAS je vous conseille d'utiliser les "issues" des différents projets et différentes extensions du framework :

A suivre avec un prochain article sur les ressources de type "shader" et l'utilisation des effets PixelBender dans les ressources du moteur IoC de VEGAS :)