Comment adapter votre application Java pour différentes langues et régions?

De nombreuses applications doivent fonctionner dans différents pays et avec différentes langues. Par exemple, considérez la phrase « L’événement spécial du zoo aura lieu le 4/1/22 pour observer les comportements des animaux. » Quand aura lieu l’événement ? Aux États-Unis, c’est le 1er avril. Cependant, un lecteur britannique l’interpréterait comme le 4 janvier. Un lecteur britannique pourrait également se demander pourquoi nous n’avons pas écrit « behaviours » (orthographe britannique). Si nous créons un site web ou un programme qui sera utilisé dans plusieurs pays, nous voulons utiliser le langage et le formatage corrects.

L’internationalisation est le processus de conception de votre programme pour qu’il puisse être adapté. Cela implique de placer des chaînes de caractères dans un fichier de propriétés et de s’assurer que les formateurs de données appropriés sont utilisés. La localisation signifie la prise en charge de plusieurs locales ou régions géographiques. Vous pouvez considérer une locale comme étant une association entre une langue et un pays. La localisation comprend la traduction de chaînes dans différentes langues. Elle comprend également l’affichage des dates et des nombres dans le format correct pour cette locale.

Initialement, votre programme n’a pas besoin de prendre en charge plusieurs locales. L’essentiel est de préparer votre application pour l’avenir en utilisant ces techniques. Ainsi, lorsque votre produit rencontrera le succès, vous pourrez ajouter la prise en charge de nouvelles langues ou régions sans tout réécrire.

Dans cette section, nous examinerons comment définir une locale et l’utiliser pour formater les dates, les nombres et les chaînes de caractères.

Choisir une Locale

Bien qu’Oracle définisse une locale comme « une région géographique, politique ou culturelle spécifique », vous ne verrez que des langues et des pays. La classe Locale se trouve dans le package java.util. La première Locale utile à trouver est la locale actuelle de l’utilisateur. Essayez d’exécuter le code suivant sur votre ordinateur :

Locale locale = Locale.getDefault();
System.out.println(locale);

Lorsque nous l’exécutons, il affiche en_US. Cela pourrait être différent pour vous. Cette sortie par défaut nous indique que nos ordinateurs utilisent l’anglais et sont situés aux États-Unis.

Remarquez le format. D’abord vient le code de langue en minuscules. La langue est toujours requise. Ensuite vient un underscore suivi du code de pays en majuscules. Le pays est optionnel.

Formats de Locale

Voici les deux formats pour les objets Locale que vous devez vous rappeler :

Locale (langue)Locale (langue, pays)
fren_US
↑      ↑
Code de langue
en minuscules
Code de langue
en minuscules   Code de pays
en majuscules

Comme exercice, assurez-vous de comprendre pourquoi chacun de ces identifiants Locale est invalide :

US    // Impossible d'avoir un pays sans langue
enUS   // Underscore manquant
US_en  // Le pays et la langue sont inversés
EN     // La langue doit être en minuscules

Les versions corrigées sont fr et fr_FR.

Vous n’avez pas besoin de mémoriser les codes de langue ou de pays. Vous devez juste reconnaître les formats valides et invalides. Faites attention aux majuscules/minuscules et à l’underscore. Par exemple, si vous voyez une locale exprimée comme es_CO, vous devriez savoir que la langue est es et le pays est CO, même si vous ne saviez pas qu’ils représentent respectivement l’espagnol et la Colombie.

En tant que développeur, vous devez souvent écrire du code qui sélectionne une locale autre que celle par défaut. Il existe trois façons courantes de le faire. La première consiste à utiliser les constantes intégrées dans la classe Locale, disponibles pour certaines locales courantes.

System.out.println(Locale.FRENCH);     // fr
System.out.println(Locale.FRANCE);     // fr_FR

Le premier exemple sélectionne la langue française, qui est parlée dans de nombreux pays, notamment la Belgique (fr_BE) et le Canada (fr_CA). Le deuxième exemple sélectionne à la fois le français comme langue et la France comme pays. Bien que ces exemples puissent sembler similaires, ils ne sont pas identiques. Un seul inclut un code de pays.

La deuxième façon de sélectionner une Locale est d’utiliser les constructeurs pour créer un nouvel objet. Vous pouvez passer juste une langue, ou à la fois une langue et un pays :

System.out.println(new Locale("fr"));        // fr
System.out.println(new Locale("fr", "CA"));  // fr_CA

Le premier est la langue française, et le second est le français au Canada. Encore une fois, vous n’avez pas besoin de mémoriser les codes. Il existe un autre constructeur qui vous permet d’être encore plus précis sur la locale. Heureusement, fournir une valeur de variante n’est pas nécessaire.

Java vous permettra de créer une Locale avec une langue ou un pays invalide, comme xx_XX. Cependant, elle ne correspondra pas à la Locale que vous voulez utiliser, et votre programme ne se comportera pas comme prévu.

Il existe une troisième façon de créer une Locale qui est plus flexible. Le modèle de conception du constructeur vous permet de définir toutes les propriétés qui vous intéressent, puis de construire la Locale à la fin. Cela signifie que vous pouvez spécifier les propriétés dans n’importe quel ordre. Les deux valeurs Locale suivantes représentent toutes deux fr_FR :

Locale l1 = new Locale.Builder()
    .setLanguage("fr")
    .setRegion("FR")
    .build();

Locale l2 = new Locale.Builder()
    .setRegion("FR")
    .setLanguage("fr")
    .build();

Lors du test d’un programme, vous pourriez avoir besoin d’utiliser une Locale autre que celle par défaut de votre ordinateur.

System.out.println(Locale.getDefault());  // en_US
Locale locale = new Locale("fr");
Locale.setDefault(locale);
System.out.println(Locale.getDefault());  // fr

Essayez-le, et ne vous inquiétez pas — la Locale change uniquement pour ce programme Java. Cela ne change aucun paramètre sur votre ordinateur. Cela ne change même pas les exécutions futures du même programme.

Dans la pratique, nous écrivons rarement du code pour changer la locale par défaut d’un utilisateur.

Localisation des Nombres

Cela pourrait vous surprendre que le formatage ou l’analyse des valeurs monétaires et numériques puisse changer en fonction de votre locale. Par exemple, aux États-Unis, le symbole du dollar est placé avant la valeur avec un point décimal pour les valeurs inférieures à un dollar, comme $2.15. En Allemagne, cependant, le symbole de l’euro est ajouté à la valeur avec une virgule pour les valeurs inférieures à un euro, comme 2,15 €.

Heureusement, le package java.text inclut des classes pour nous aider. Les sections suivantes couvrent la façon de formater les nombres, les devises et les dates en fonction de la locale.

La première étape pour formater ou analyser des données est la même : obtenir une instance d’un NumberFormat.

Méthodes d’usine pour obtenir un NumberFormat

DescriptionUtilisation de la Locale par défaut et d’une Locale spécifiée
Formateur à usage généralNumberFormat.getInstance()
NumberFormat.getInstance(Locale locale)
Identique à getInstanceNumberFormat.getNumberInstance()
NumberFormat.getNumberInstance(Locale locale)
Pour formater des montants monétairesNumberFormat.getCurrencyInstance()
NumberFormat.getCurrencyInstance(Locale locale)
Pour formater des pourcentagesNumberFormat.getPercentInstance()
NumberFormat.getPercentInstance(Locale locale)
Arrondit les valeurs décimales avant affichageNumberFormat.getIntegerInstance()
NumberFormat.getIntegerInstance(Locale locale)
Renvoie un formateur de nombres compactsNumberFormat.getCompactNumberInstance()
NumberFormat.getCompactNumberInstance(Locale locale, NumberFormat.Style formatStyle)

Une fois que vous avez l’instance NumberFormat, vous pouvez appeler format() pour transformer un nombre en String, ou vous pouvez utiliser parse() pour transformer un String en nombre.

Les classes de format ne sont pas thread-safe. Ne les stockez pas dans des variables d’instance ou statiques.

Formatage des Nombres

Lorsque nous formatons des données, nous les convertissons d’un objet structuré ou d’une valeur primitive en String. La méthode NumberFormat.format() formate le nombre donné en fonction de la locale associée à l’objet NumberFormat.

Revenons à notre zoo pour un moment. Pour la documentation marketing, nous voulons partager le nombre moyen mensuel de visiteurs du Zoo de San Diego. Voici comment afficher le même nombre dans trois locales différentes :

int visiteursParAn = 3_200_000;
int visiteursParMois = visiteursParAn / 12;

var us = NumberFormat.getInstance(Locale.US);
System.out.println(us.format(visiteursParMois));  // 266,666

var allemagne = NumberFormat.getInstance(Locale.GERMANY);
System.out.println(allemagne.format(visiteursParMois));  // 266.666

var canadaFrancais = NumberFormat.getInstance(Locale.CANADA_FRENCH);
System.out.println(canadaFrancais.format(visiteursParMois));  // 266 666

Cela montre comment nos visiteurs américains, allemands et canadiens français peuvent tous voir la même information dans le format numérique auquel ils sont habitués. En pratique, nous appellerions simplement NumberFormat.getInstance() et nous nous fierions à la locale par défaut de l’utilisateur pour formater la sortie.

Le formatage des devises fonctionne de la même manière.

double prix = 48;
var maLocale = NumberFormat.getCurrencyInstance();
System.out.println(maLocale.format(prix));

Lorsqu’il est exécuté avec la locale par défaut en_US pour les États-Unis, ce code affiche $48.00. En revanche, lorsqu’il est exécuté avec la locale par défaut en_GB pour la Grande-Bretagne, il affiche £48.00.

Dans le monde réel, utilisez int ou BigDecimal pour l’argent et non double. Faire des calculs sur des montants avec double est dangereux car les valeurs sont stockées sous forme de nombres à virgule flottante. Votre patron n’appréciera pas si vous perdez des centimes ou des fractions de centimes lors des transactions !

Enfin, voici des exemples qui montrent le formatage des pourcentages :

double tauxReussite = 0.802;
var us = NumberFormat.getPercentInstance(Locale.US);
System.out.println(us.format(tauxReussite));  // 80%

var allemagne = NumberFormat.getPercentInstance(Locale.GERMANY);
System.out.println(allemagne.format(tauxReussite));  // 80 %

Pas beaucoup de différence, nous le savons, mais vous devriez au moins être conscient que la capacité d’afficher un pourcentage est spécifique à la locale !

Analyse des Nombres

Lorsque nous analysons des données, nous les convertissons d’un String en un objet structuré ou une valeur primitive. La méthode NumberFormat.parse() accomplit cela et prend en compte la locale.

Par exemple, si la locale est l’anglais/États-Unis (en_US) et que le nombre contient des virgules, les virgules sont traitées comme des symboles de formatage. Si la locale concerne un pays ou une langue qui utilise des virgules comme séparateur décimal, la virgule est traitée comme un point décimal.

La méthode parse(), présente dans divers types, déclare une exception vérifiée ParseException qui doit être gérée ou déclarée dans la méthode dans laquelle elle est appelée.

Examinons un exemple. Le code suivant analyse un prix de billet réduit avec différentes locales. La méthode parse() lance une ParseException vérifiée, alors assurez-vous de la gérer ou de la déclarer dans votre propre code.

String s = "40.45";
var en = NumberFormat.getInstance(Locale.US);
System.out.println(en.parse(s));  // 40.45

var fr = NumberFormat.getInstance(Locale.FRANCE);
System.out.println(fr.parse(s));  // 40

Aux États-Unis, un point (.) fait partie d’un nombre, et le nombre est analysé comme vous pourriez vous y attendre. La France n’utilise pas de point décimal pour séparer les nombres. Java l’analyse comme un caractère de formatage, et il arrête de regarder le reste du nombre. La leçon est de s’assurer que vous analysez en utilisant la bonne locale !

La méthode parse() est également utilisée pour analyser les devises. Par exemple, nous pouvons lire le revenu mensuel du zoo provenant des ventes de billets :

String revenu = "$92,807.99";
var cf = NumberFormat.getCurrencyInstance();
double valeur = (Double) cf.parse(revenu);
System.out.println(valeur);  // 92807.99

La chaîne de devise “$92,807.99” contient un symbole dollar et une virgule. La méthode parse élimine les caractères et convertit la valeur en nombre. La valeur de retour de parse est un objet Number. Number est la classe parente de toutes les classes d’emballage java.lang, donc la valeur de retour peut être convertie en son type de données approprié. Le Number est converti en Double puis automatiquement déballé en double.

Formatage avec CompactNumberFormat

La deuxième classe qui hérite de NumberFormat est CompactNumberFormat. Elle est similaire à DecimalFormat, mais elle est conçue pour être utilisée dans des endroits où l’espace d’impression peut être limité. Elle est partiale dans le sens où elle choisit un format pour vous, et spécifique à la locale dans le sens où la sortie peut changer en fonction de votre emplacement.

Considérez l’exemple de code suivant qui applique un CompactNumberFormat cinq fois à deux locales, en utilisant une importation statique pour Style (une enum avec valeur SHORT ou LONG) :

var formateurs = Stream.of(
    NumberFormat.getCompactNumberInstance(),
    NumberFormat.getCompactNumberInstance(Locale.getDefault(), Style.SHORT),
    NumberFormat.getCompactNumberInstance(Locale.getDefault(), Style.LONG),
    
    NumberFormat.getCompactNumberInstance(Locale.GERMAN, Style.SHORT),
    NumberFormat.getCompactNumberInstance(Locale.GERMAN, Style.LONG),
    NumberFormat.getNumberInstance());
    
formateurs.map(s -> s.format(7_123_456)).forEach(System.out::println);

Voici ce qui est imprimé par ce code lorsqu’il est exécuté dans la locale en_US :

7M
7M
7 million
7 Mio.
7 Millionen
7,123,456

Remarquez que les deux premières lignes sont identiques. Si vous ne spécifiez pas de style, SHORT est utilisé par défaut. Ensuite, remarquez que les valeurs sauf la dernière (qui n’utilise pas un formateur de nombres compacts) sont tronquées. Il y a une raison pour laquelle on l’appelle un formateur de nombres compacts ! Aussi, remarquez que la forme courte utilise des étiquettes communes pour les grandes valeurs, comme K pour mille. Enfin, la sortie peut être différente pour vous lorsque vous l’exécutez, car elle a été exécutée dans une locale en_US.

En utilisant les mêmes formateurs, essayons un autre exemple :

formateurs.map(s -> s.format(314_900_000)).forEach(System.out::println);

Cela imprime ce qui suit lorsqu’il est exécuté dans la locale en_US :

315M
315M
315 million
315 Mio.
315 Millionen
314,900,000

Remarquez que le troisième chiffre est automatiquement arrondi pour les entrées qui utilisent un CompactNumberFormat. Voici un résumé des règles pour CompactNumberFormat :

  • Il détermine d’abord la plage la plus élevée pour le nombre, comme mille (K), million (M), milliard (B) ou billion (T).
  • Il renvoie ensuite jusqu’aux trois premiers chiffres de cette plage, en arrondissant le dernier chiffre si nécessaire.
  • Enfin, il imprime un identifiant. Si SHORT est utilisé, un symbole est renvoyé. Si LONG est utilisé, un espace suivi d’un mot est renvoyé.

Pour l’examen, assurez-vous de comprendre la différence entre les formats SHORT et LONG et les symboles communs comme M pour million.

Localisation des Dates

Comme les nombres, les formats de date peuvent varier selon la locale.

Méthodes d’usine pour obtenir un DateTimeFormatter

DescriptionUtilisation de la Locale par défaut
Pour formater des datesDateTimeFormatter.ofLocalizedDate(FormatStyle dateStyle)
Pour formater des heuresDateTimeFormatter.ofLocalizedTime(FormatStyle timeStyle)
Pour formater des dates et des heuresDateTimeFormatter.ofLocalizedDateTime(FormatStyle dateStyle, FormatStyle timeStyle)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle dateTimeStyle)

Chaque méthode du tableau prend un paramètre FormatStyle (ou deux) avec les valeurs possibles SHORT, MEDIUM, LONG et FULL.

Et si vous avez besoin d’un formateur pour une locale spécifique ? Facile — il suffit d’ajouter withLocale(locale) à l’appel de méthode.

Voyons tout cela ensemble. Jetez un coup d’œil à l’extrait de code suivant, qui s’appuie sur une importation statique pour la valeur java.time.format.FormatStyle.SHORT :

public static void imprimer(DateTimeFormatter dtf,
                            LocalDateTime dateHeure, Locale locale) {
    System.out.println(dtf.format(dateHeure) + " --- "
            + dtf.withLocale(locale).format(dateHeure));
}

public static void main(String[] args) {
    Locale.setDefault(new Locale("fr", "FR"));
    var italie = new Locale("it", "IT");
    var dt = LocalDateTime.of(2022, Month.OCTOBER, 20, 15, 12, 34);
    
    // 20/10/22 --- 20/10/22
    imprimer(DateTimeFormatter.ofLocalizedDate(SHORT), dt, italie);
    
    // 15:12 --- 15:12
    imprimer(DateTimeFormatter.ofLocalizedTime(SHORT), dt, italie);
    
    // 20/10/22 15:12 --- 20/10/22, 15:12
    imprimer(DateTimeFormatter.ofLocalizedDateTime(SHORT, SHORT), dt, italie);
}

D’abord, nous établissons fr_FR comme locale par défaut, avec it_IT comme locale demandée. Nous produisons ensuite chaque valeur en utilisant les deux locales. Comme vous pouvez le voir, l’application d’une locale a un grand impact sur les formateurs de date et d’heure intégrés.

Spécification d’une Catégorie de Locale

Lorsque vous appelez Locale.setDefault() avec une locale, plusieurs options d’affichage et de formatage sont sélectionnées en interne. Si vous avez besoin d’un contrôle plus fin de la locale par défaut, Java subdivise les options de formatage sous-jacentes en catégories distinctes avec l’énumération Locale.Category.

L’énumération Locale.Category est un élément imbriqué dans Locale qui prend en charge des locales distinctes pour l’affichage et le formatage des données. Vous devriez être familier avec les deux valeurs enum :

ValeurDescription
DISPLAYCatégorie utilisée pour afficher des données sur la locale
FORMATCatégorie utilisée pour formater des dates, des nombres ou des devises

Lorsque vous appelez Locale.setDefault() avec une locale, les catégories DISPLAY et FORMAT sont définies ensemble. Examinons un exemple :

public static void afficherDevise(Locale locale, double argent) {
    System.out.println(
            NumberFormat.getCurrencyInstance().format(argent)
            + ", " + locale.getDisplayLanguage());
}

public static void main(String[] args) {
    var espagne = new Locale("es", "ES");
    var argent = 1.23;
    
    // Imprimer avec la locale par défaut
    Locale.setDefault(new Locale("fr", "FR"));
    afficherDevise(espagne, argent);  // 1,23 €, espagnol
    
    // Imprimer avec la locale d'affichage sélectionnée
    Locale.setDefault(Category.DISPLAY, espagne);
    afficherDevise(espagne, argent);  // 1,23 €, español
    
    // Imprimer avec la locale de format sélectionnée
    Locale.setDefault(Category.FORMAT, espagne);
    afficherDevise(espagne, argent);  // 1,23 €, español
}

Le code imprime les mêmes données trois fois. D’abord, il imprime la langue de l’espagne et les variables argent en utilisant la locale fr_FR. Ensuite, il l’imprime en utilisant la catégorie DISPLAY de es_ES, tandis que la catégorie FORMAT reste fr_FR. Enfin, il imprime les données en utilisant les deux catégories définies sur es_ES.

Vous n’avez pas besoin de mémoriser les diverses options d’affichage et de formatage pour chaque catégorie. Vous devez juste savoir que vous pouvez définir des parties de la locale indépendamment. Vous devriez également savoir qu’appeler Locale.setDefault(us) après l’extrait de code précédent changera les deux catégories de locale en fr_FR.