Internationalisation

images/bluebelt.png

Pourquoi internationaliser? :

Une application doit pouvoir s’adapter automatiquement à la langue de l’utilisateur (ou même permettre des écrans multilingues!)

Contextes culturels : Locale

Les mécanismes de localisation du langage Java utilisent une information de "contexte culturel" définie au moyen de la classe java.util.Locale.

Locale : constructeurs, constantes

Locale(langue, pays)
Locale(langue, pays,variante)
   new Locale("fr", "CA") ; //français du Canada
   new Locale ("fr","CA", "MAC") ;
   new Locale ("fr","FR", "EURO") ;
   // monnaie euro et français de France (maintenant par défaut)

Les arguments "langue" (chaîne en minuscule) et "pays" (chaîne en majuscules) obéissent à des standard de désignation (voir documentation et méthodes statiques getISOLanguages, getISOCountries).

La classe fournit également des constantes prédéfinies :

// constantes de classe
Locale.FRENCH ; // Locale("fr")
Locale.FRANCE ; // Locale ("fr", "FR")

Les objets Locale (et les désignations associées comme la chaîne "fr_FR") permettront de créer des hiérarchies de ressources:

  1. Le "Francais"
  2. Le "Français" de Belgique

Contexte culturel: obtention, mise en place

Au moment où une JVM démarre elle met en place un Locale par défaut (en fonction de paramètres de l’environnement)

[Note]

On peut agir sur le Locale par défaut au moment du lancement de la JVM en modifiant les propriétés user.language, user.country, user.variant.

Exemple: java -Duser.language=fr monpackage.MonProgramme

   Locale local = Locale.getDefault(); // local
   System.out.println(local.toString()) ;
   //-> fr_FR : chaîne "clef"
   System.out.println(local.getDisplayName());
   //-> Français (France)
   System.out.println(Locale.ENGLISH.getDisplayName());
   //-> Anglais
   // on a traduit en français le nom!

On peut positionner globalement cette valeur (Locale.setDefault(locale)), mais il est plus pertinent de la fixer pour chacun des objets qui utilisent ces contextes d’internationalisation:

// classe du package java.text
NumberFormat nbf = NumberFormat.getInstance(local) ;
// sert à mettre en forme des nombres
// on crée un "formatteur" propre au contexte

Comment demander à une classe les Locales qu’elle sait gérer:

Locale[] locs = NumberFormat.getAvailableLocales();
[Note]

La liste des Locales sachant gérer un service de ce type est la concaténation de ceux "cablés" dans Java (jdk.localedata) et de ceux installés spécialement pour rendre les services définis dans le package java.text.spi (ce package définit des services au moyen de classes abstraites).

Paramétrage de ressources: Resourcebundle

La "traduction" (d’un message par exemple) doit s’appuyer sur des ressources extérieures de paramétrage des applications.

Un mécanisme général est défíni pour rechercher ces ressources de localisation: les ResourceBundles. Quand on recherche l'équivalent localisé d’un objet on s’adresse à un mécanisme de ResourceBundle défini autour d’un "thème".

  • Pour un thème donné on a conceptuellement un dictionnaire qui associe à une clef (unique) une valeur correspondante
  • Le résultat de la recherche est en fait obtenu par la consultation d’une hiérarchie de dictionnaires suffixés par les chaînes caractéristiques des Locales. Ainsi par exemple pour un thème libellé "Erreurs" on pourra avoir :

    Erreurs_fr_CA_UNIX // locale demandé
    Erreurs_fr_CA
    Erreurs_fr
    Erreurs_<Locale_par_défaut>// obtenu par getDefault()
    Erreurs_<Locale_par_défaut_simplifié>
    Erreurs

Pour éviter des incidents il est conseillé d’avoir un hiérarchie cohérente qui se termine par le thème sans extension (qui, de plus servira, de ressource de référence). On peut aussi construire des chaînages explicites entre bundles (setParent())-

Un dictionnaire abstrait est obtenu par spécification du "thème" et du Locale.

   ResourceBundle messagesErreur =
      ResourceBundle.getBundle("Erreurs" ,locale) ;
   // ou getBundle("Erreurs") -locale du contexte par défaut
   // -> Exception MissingResourceException

La clef de recherche est une chaine. Selon les circonstances on pourra utiliser:

messagesErreur.getObject(clef) // pourquoi pas un glyphe?
messagesErreur.getStringArray(clef)// ou un ensemble de chaines
messagesErreur.getString(clef) ;// ou, bien sûr, une chaîne

Où se trouve les ressources?

Ici aussi on est obligé de considérer les deux cas d’architecture: avec ou sans modules.

Il faut considérer qu’une application doit fournir un premier "dictionnaire" de base (par exemple Erreurs) et que les codes de déploiement Erreurs_* peuvent se trouver dans des jars différents.

En fait l’argument de getBundle va correspondre à un nom nompackage.clef-bundle (on pourrait aussi dire un "nom canonique" de classe). Donc par exemple:

ResourceBundle.getBundle("com.biz.monapp.config.Erreurs" ,locale) ;

Ce qui veut dire que les "Bundles" seront déployés dans des répertoires/package de ce nom … qui peuvent être répartis entre plusieurs jars.

images/brownbelt.png

Or en architecture modulaire il est impossible d’avoir un même package sur plusieurs modules. Donc ou bien on déploie en ajoutant des jars non-modulaires soit on met en place une technique différente (un peu compliquée!).

  • La recherche d’un Bundle va passer par un service. Le mécanisme de recherche est déjà codé dans getBundle ce qui veut dire que le nom du service et son déploiement doivent obéir à des règles précises de nommage.
  • Les codes qui rendent le service vont à leur tour utiliser localement getBundle dans le contexte de leur module. Mais il vont être obligés de passer un argument de clef de recherche qui soit (légèrement) transformé. Si la clef du getBundle initiale est com.biz.monapp.config.Erreurs cette clef ne pourra pas être utilisé tel que dans le module qui rend le service puisqu’on ne peut pas avoir plusieurs package com.biz.monapp.config ! Il faudra par exemple avoir une transformation de nom de manière à chercher localement com.biz.monapp.config.fr.Erreurs.

Nommage du service de Bundle en architecture modulaire

Il faut créer une définition de service qui obéisse à des règles précises de nommage.

Le nom du Service sera : package.spi.clefProvider

Donc pour une recherche de com.biz.monapp.config.Erreurs on aura une définition de com.biz.monapp.config.spi.ErreursProvider

et donc un code:

package com.biz.monapp.config.spi ;

public interface ErreursProvider extends ResourceBundleProvider {
}

Les modules requérants le service feront:

uses com.biz.monapp.config.spi.ErreursProvider ;

Réalisation du service dans un module

Il faut réaliser une classe qui implante le service. En général on peut utiliser une sous-classe de AbstractResourceBundleProvider. A priori cette classe modifiera le nom à rechercher pour permettre d’avoir des noms de packages distincts.

Par exemple si on recherche com.biz.monapp.config.Erreurs on peut disposer le données liées aux divers aspects de la langue française dans com.biz.monapp.config.fr.

//dans module de déploiement
package com.biz.monapp.i18nimpl;

public class ErreursProviderImpl extends AbstractResourceBundleProvider
   implements ErreursProvider {
   // modification d'une méthode standard
   @Override
   public  String toBundleName(String baseName, Locale locale) {
     String autreBaseName = baseName ;
     String langue = locale.getLanguage() ;
     if(! langue.isEmpty()){
        autreBaseName = name.replace("Erreurs", langue + ".Erreurs") ;
     }
     return super.toBundleName(autreBaseName, locale) ;
   }
}

Le module-info du module déclarera alors

provides com.biz.monapp.config.spi.ErreursProvider with
  com.biz.monapp.i18nimpl.ErreursproviderImpl ;

ListResourceBundle

images/bluebelt.png

ResourceBundle est une classe abstraite qui peut connaître des réalisations diverses. Deux systèmes de spécifications de ressources sont livrés en standard : ListResourceBundle et PropertyResourceBundle.

Ces deux classes sont une illustration des priorités de recherche de getBundle() : à un niveau de la hiérarchie des Locale la méthode recherche d’abord une classe portant le nom du niveau par exemple Erreurs_fr.class (cette classe peut être de type ListResourceBundle) et ensuite un fichier Erreurs_fr.properties (fichier permettant de construire dynamiquement une instance de PropertyResourceBundle). Ces ressources doivent se trouver dans le chemin d’accès aux classes (classpath) associé à un ClassLoader donné.

Exemple de ListResourceBundle :

public class Erreurs_fr extends ListResourceBundle {
   public static final Object[][] tb = {
      { "err" , new ImageIcon("erreur.gif")} ,
      { "ERR" , new ImageIcon("terreur.gif")} ,
   } ;

   public Object[][] getContents() { return tb ;}
   }
}

La réalisation doit implanter la méthode getContents() et le tableau retourné doit associer une clef (chaîne de caractère) et un objet.

Dans l’exemple ci-dessus seul getObject() devra être utilisé.

PropertyResourceBundle

Un PropertyResourceBundle est un Bundle créé automatiquement par exploitation d’un fichier ".properties"

Le format d’un tel fichier est défini pour la classe java.util.Properties (voir méthode store() ou load() , le codage doit être de l’ISO8859-1)

# Errors_fr.properties
#Supposed to be translated in French ;-)
you\ stupid =  Une erreur malencontreuse est survenue
I'll\ scream = Patience, impossible n'est pas français

exploitation :

   ResourceBundle errs = ResourceBundle.getBundle("Errors") ;
   ...
   String message = errs.getString("you stupid") ;
   // et, heureusement, "fr" est le contexte par défaut

--exercices--

Classes de mise en forme/analyse

Les objets de mise en forme en fonction du contexte culturel dérivent de la classe java.text.Format et disposent de méthodes de mise en forme du type : String format(Object) et de méthodes d’analyse de chaînes de caractères du type: Object parseObject(String) (et méthodes spécialisées dérivées).

Classes principales de mise en forme : NumberFormat, DateFormat, MessageFormat.

Utilisation de formats numériques prédéfinis :

NumberFormat formatteur =
   NumberFormat.getNumberInstance(Locale.FRANCE);
System.out.println(formatteur.format(345987.246));

Donne:

345 987,246

NumberFormat permet aussi de se procurer des instances mettant en forme des montants monétaires (avec mention de la monnaie locale) et des pourcentages (voir aussi la classe Currency).

Analyse:

NumberFormat formch =
   NumberFormat.getNumberInstance(new Locale("fr","CH"));
try{
   Number nb = formch.parse("345'987.246");
} catch (ParseException exc) {
   ...

parse tente de retourner un Long ou sinon un Double mais la sous-classe DecimalFormat (d’ailleurs plus intéressante) peut retourner un BigDecimal sous certaines conditions.

[Avertissement]

Attention le séparateur des milliers en Français est vu comme une "espace insécable" (caractère \u00A0) et l’analyseur ne sait pas traiter une espace normale -il faut donc prendre des précautions-.

Numériques avec instructions de formatage

La classe DecimalFormat (sous-classe de NumberFormat) permet de contrôler la mise en forme :

NumberFormat formatteur =
   NumberFormat.getNumberInstance(Locale.FRANCE);
DecimalFormat df = (DecimalFormat)formatteur ;
df.applyPattern("000,000.00") ;
//séparateurs US + indicateurs chiffres (0 si manquant)
System.out.println(df.format(5987.246));

Donne:

005 987,25

Autres formats

  • DateFormat permet d’obtenir des objets de mise en forme/analyse des Dates , SimpleDateFormat et DateFormatSymbols permettent de fabriquer des formats de mise en forme de Dates.
  • MessageFormat et ChoiceFormat permettent de fabriquer des messages composés de plusieurs éléments paramétrables:
   int nbfichiers = 10 ;// obtenu en fait par calcul
   String nomFic = "prmtc34x5f" ; //;-) idem
   String param ="Le disque {1} contient {0} fichier(s)" ;
   // obtenu par exploitation d'un Bundle
   String formatté = MessageFormat.format(param,
      nbfichiers, nomfic) ;

Donne

Le disque prmtc34x5f contient 10 fichier(s)

images/brownbelt.png

Utilisation d’un paramétrage par exploitation de plages de valeurs (ChoiceFormat)

 //exemple très complexe
double[] limites = {0,1,2} ;
 //a chacune de ces limites est associé un format
//{0,number} analyse le parametre qui lui est passé
 //(et qui contient seulement un élément de type nombre)
String[] associés = {
  "est vide",
  "contient 1 fichier",
  "contient {0,number} fichiers"
} ;
ChoiceFormat choix = new ChoiceFormat(limites, associés) ;

String param = "Le disque {1} {0}" ; // obtenu par le Bundle
MessageFormat fmt2 = new MessageFormat(param) ;
// argument 0 de "param" est traité par le ChoiceFormat
fmt2.setFormatByArgumentIndex(0, choix) ;
System.out.println( fmt2.format(new Object[] {
        new Integer(nbfichiers),
        nomFic }));

La mise en forme donne:

Le disque prmtc34x5f contient 10 fichiers
[Note]

Pour générer une image minimum avec jlink (mais avec des données d’internationalisation) ne pas oublier l’option --include-locales