Pourquoi internationaliser? :
TextField awt
de saisie situé à
gauche du Label
d’invite -document lu de droite à gauche-)
Une application doit pouvoir s’adapter automatiquement à la langue de l’utilisateur (ou même permettre des écrans multilingues!)
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(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:
Au moment où une JVM démarre elle met en place un Locale par défaut (en fonction de paramètres de l’environnement)
![]() | |
On peut agir sur le Exemple: |
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 Locale
s qu’elle sait gérer:
Locale[] locs = NumberFormat.getAvailableLocales();
![]() | |
La liste des |
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 ResourceBundle
s. Quand on recherche l'équivalent
localisé d’un objet on s’adresse à un mécanisme de ResourceBundle
défini autour d’un "thème".
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 Locale
s. 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
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.
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!).
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.
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
.
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 ;
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 ;
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é.
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
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.
![]() | |
Attention le séparateur des milliers en Français est vu comme une
"espace insécable" (caractère |
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
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)
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
![]() | |
Pour générer une image minimum avec |