Comment internationaliser une application Java avec les Resource Bundles ?

Jusqu’à présent, nous avons conservé tous les textes affichés aux utilisateurs dans les classes qui les utilisent. La localisation nécessite de les externaliser ailleurs.

Un resource bundle contient les objets spécifiques à une locale qui seront utilisés par un programme. C’est comme une map avec des clés et des valeurs. Le resource bundle est généralement stocké dans un fichier de propriétés. Un fichier de propriétés est un fichier texte dans un format spécifique avec des paires clé/valeur.

Notre programme de zoo a été un succès. Nous recevons maintenant des demandes pour l’utiliser dans trois autres zoos ! Nous avons déjà le support pour les zoos américains. Nous devons maintenant ajouter le Zoo de la Palmyre en France, le Grand Zoo de Vancouver au Canada anglophone, et le Zoo de Granby au Canada francophone.

Nous réalisons immédiatement que nous allons devoir internationaliser notre programme. Les resource bundles seront très utiles. Ils nous permettront de traduire facilement notre application dans plusieurs locales ou même de prendre en charge plusieurs locales à la fois. Il sera également facile d’ajouter d’autres locales plus tard si des zoos d’autres pays sont intéressés. Nous avons réfléchi aux locales que nous devons prendre en charge, et nous en avons identifié quatre :

Locale us = new Locale("en", "US");
Locale france = new Locale("fr", "FR");
Locale englishCanada = new Locale("en", "CA");
Locale frenchCanada = new Locale("fr", "CA");

Dans les sections suivantes, nous créons un resource bundle en utilisant des fichiers de propriétés. C’est conceptuellement similaire à une Map<String,String>, chaque ligne représentant une paire clé/valeur différente. La clé et la valeur sont séparées par un signe égal (=) ou deux-points (:). Pour simplifier, nous utiliserons le signe égal tout au long de ce chapitre. Nous verrons également comment Java détermine quel resource bundle utiliser.

Création d’un Resource Bundle

Nous allons mettre à jour notre application pour prendre en charge les quatre locales listées précédemment. Heureusement, Java ne nous oblige pas à créer quatre resource bundles différents. Si nous n’avons pas de resource bundle spécifique à un pays, Java utilisera celui spécifique à la langue. C’est un peu plus complexe que cela, mais commençons par un exemple simple.

Pour l’instant, nous avons besoin de fichiers de propriétés en anglais et en français pour notre resource bundle Zoo. Commençons par créer deux fichiers de propriétés.

Zoo_en.properties
hello=Hello
open=The zoo is open

Zoo_fr.properties
hello=Bonjour
open=Le zoo est ouvert

Les noms de fichiers correspondent au nom de notre resource bundle, Zoo. Ils sont suivis par un underscore (_), la locale cible, et l’extension de fichier .properties. Nous pouvons écrire notre tout premier programme qui utilise un resource bundle pour afficher ces informations.

public static void afficherMessageBienvenue(Locale locale) {
    var rb = ResourceBundle.getBundle("Zoo", locale);
    System.out.println(rb.getString("hello")
        + ", " + rb.getString("open"));
}

public static void main(String[] args) {
    var us = new Locale("en", "US");
    var france = new Locale("fr", "FR");
    afficherMessageBienvenue(us);      // Hello, The zoo is open
    afficherMessageBienvenue(france);  // Bonjour, Le zoo est ouvert
}

Les lignes créant les deux locales que nous voulons tester, mais la méthode fait le travail réel. Elle appelle une méthode factory sur ResourceBundle pour obtenir le bon resource bundle. Puis elle récupère la bonne chaîne du resource bundle et affiche les résultats.

Puisqu’un resource bundle contient des paires clé/valeur, vous pouvez même parcourir toutes les paires. La classe ResourceBundle fournit une méthode keySet() pour obtenir un ensemble de toutes les clés.

var us = new Locale("en", "US");
ResourceBundle rb = ResourceBundle.getBundle("Zoo", us);
rb.keySet().stream()
    .map(k -> k + ": " + rb.getString(k))
    .forEach(System.out::println);

Cet exemple parcourt toutes les clés. Il transforme chaque clé en une chaîne contenant à la fois la clé et la valeur avant d’afficher le tout.

hello: Hello
open: The zoo is open

Scénario du Monde Réel

Chargement des Fichiers Resource Bundle à l’Exécution

Dans vos propres applications, les resource bundles peuvent être stockés à divers endroits. Bien qu’ils puissent être stockés à l’intérieur du JAR qui les utilise, ce n’est pas recommandé. Cette approche vous oblige à reconstruire le JAR de l’application chaque fois que le texte change. L’un des avantages de l’utilisation des resource bundles est de découpler le code de l’application des données textuelles spécifiques à la locale.

Une autre approche consiste à avoir tous les fichiers de propriétés dans un JAR ou dossier de propriétés séparé et à les charger dans le classpath à l’exécution. De cette manière, une nouvelle langue peut être ajoutée sans modifier le JAR de l’application.

Sélection d’un Resource Bundle

Il existe deux méthodes pour obtenir un resource bundle que vous devriez connaître :

ResourceBundle.getBundle("nom");
ResourceBundle.getBundle("nom", locale);

La première utilise la locale par défaut. Vous êtes susceptible d’utiliser celle-ci dans les programmes que vous écrivez.

Java gère la logique pour choisir le meilleur resource bundle disponible pour une clé donnée. Il essaie de trouver la valeur la plus spécifique. Le tableau suivant montre ce que Java parcourt lorsqu’on lui demande le resource bundle Zoo avec la locale new Locale(“fr”, “FR”) lorsque la locale par défaut est l’anglais américain.

ÉtapeFichier recherchéRaison
1Zoo_fr_FR.propertiesLocale demandée
2Zoo_fr.propertiesLangue demandée sans pays
3Zoo_en_US.propertiesLocale par défaut
4Zoo_en.propertiesLangue de la locale par défaut sans pays
5Zoo.propertiesPas de locale du tout—bundle par défaut
6Si toujours pas trouvé, lancer MissingResourceExceptionNi locale ni bundle par défaut disponible

Pour mieux mémoriser l’ordre du tableau, apprenez ces étapes :

  1. Rechercher le resource bundle pour la locale demandée, suivi de celui pour la locale par défaut.
  2. Pour chaque locale, vérifier la langue/pays, puis seulement la langue.
  3. Utiliser le resource bundle par défaut si aucune locale correspondante ne peut être trouvée.

Java prend en charge les resource bundles à partir de classes Java et de propriétés. Lorsque Java recherche un resource bundle correspondant, il vérifiera d’abord s’il existe un fichier de resource bundle avec le nom de classe correspondant.

Voyons si vous comprenez le tableau. Quel est le nombre maximum de fichiers que Java devrait examiner pour trouver le resource bundle approprié avec le code suivant ?

Locale.setDefault(new Locale("hi"));
ResourceBundle rb = ResourceBundle.getBundle("Zoo", new Locale("en"));

La réponse est trois. Les voici :

  1. Zoo_en.properties
  2. Zoo_hi.properties
  3. Zoo.properties

La locale demandée est en, donc nous commençons par là. Comme la locale en ne contient pas de pays, nous passons à la locale par défaut, hi. Encore une fois, il n’y a pas de pays, donc nous terminons avec le bundle par défaut.

Sélection des Valeurs du Resource Bundle

Bien compris ? Parfait—car il y a un rebondissement. Les étapes que nous avons discutées jusqu’à présent concernent la recherche du resource bundle correspondant à utiliser comme base. Java n’est pas obligé d’obtenir toutes les clés du même resource bundle. Il peut les obtenir de n’importe quel parent du resource bundle correspondant. Un resource bundle parent dans la hiérarchie supprime simplement des composants du nom jusqu’à atteindre le sommet. Le tableau suivant montre comment procéder.

Resource bundle correspondantFichiers de propriétés d’où peuvent provenir les clés
Zoo_fr_FRZoo_fr_FR.properties
Zoo_fr.properties
Zoo.properties

Une fois qu’un resource bundle a été sélectionné, seules les propriétés le long d’une seule hiérarchie seront utilisées. Ce comportement contraste avec le tableau précédent, dans lequel le resource bundle par défaut en_US est utilisé si aucun autre resource bundle n’est disponible.

Qu’est-ce que cela signifie exactement ? Supposons que la locale demandée soit fr_FR et que la locale par défaut soit en_US. La JVM fournira des données de en_US seulement s’il n’y a pas de resource bundle fr_FR ou fr correspondant. Si elle trouve un resource bundle fr_FR ou fr, alors seuls ces bundles, avec le bundle par défaut, seront utilisés.

Mettons tout cela ensemble et affichons quelques informations sur nos zoos. Nous avons plusieurs fichiers de propriétés cette fois.

Zoo.properties
name=Vancouver Zoo

Zoo_en.properties
hello=Hello
open=is open

Zoo_en_US.properties
name=The Zoo

Zoo_en_CA.properties
visitors=Canada visitors

Supposons que nous ayons un visiteur du Québec (qui a une locale par défaut de français canadien) qui a demandé au programme de fournir des informations en anglais. À votre avis, qu’est-ce que cela affiche ?

Locale.setDefault(new Locale("en", "US"));
Locale locale = new Locale("en", "CA");
ResourceBundle rb = ResourceBundle.getBundle("Zoo", locale);
System.out.print(rb.getString("hello"));
System.out.print(". ");
System.out.print(rb.getString("name"));
System.out.print(" ");
System.out.print(rb.getString("open"));
System.out.print(" ");
System.out.print(rb.getString("visitors"));

Le programme affiche :

Hello. Vancouver Zoo is open Canada visitors

La locale par défaut est en_US, et la locale demandée est en_CA. D’abord, Java parcourt les resource bundles disponibles pour trouver une correspondance. Il en trouve une immédiatement avec Zoo_en_CA.properties. Cela signifie que la locale par défaut en_US est sans importance.

La première ligne ne trouve pas de correspondance pour la clé hello dans Zoo_en_CA.properties, donc elle remonte la hiérarchie jusqu’à Zoo_en.properties. La ligne suivante ne trouve pas de correspondance pour name dans aucun des deux premiers fichiers de propriétés, donc elle doit remonter tout en haut de la hiérarchie jusqu’à Zoo.properties. La troisième ligne a la même expérience que la première, utilisant Zoo_en.properties. Enfin, la dernière ligne a une tâche plus facile et trouve une clé correspondante dans Zoo_en_CA.properties.

Dans cet exemple, seuls trois fichiers de propriétés ont été utilisés : Zoo_en_CA.properties, Zoo_en.properties, et Zoo.properties. Même lorsque la propriété n’a pas été trouvée dans les resource bundles en_CA ou en, le programme a préféré utiliser Zoo.properties (le resource bundle par défaut) plutôt que Zoo_en_US.properties (la locale par défaut).

Que se passe-t-il si une propriété n’est trouvée dans aucun resource bundle ? Une exception est alors lancée. Par exemple, tenter d’appeler rb.getString(“close”) dans le programme précédent entraîne une MissingResourceException à l’exécution.

Formatage des Messages

Souvent, nous voulons simplement afficher les données textuelles d’un resource bundle, mais parfois nous voulons formater ces données avec des paramètres. Dans les programmes réels, il est courant de substituer des variables au milieu d’une chaîne de resource bundle. La convention consiste à utiliser un numéro entre accolades comme {0}, {1}, etc. Le numéro indique l’ordre dans lequel les paramètres seront passés. Bien que les resource bundles ne prennent pas cela en charge directement, la classe MessageFormat le fait.

Par exemple, supposons que nous ayons cette propriété définie :

helloByName=Hello, {0} and {1}

En Java, nous pouvons lire la valeur normalement. Après cela, nous pouvons la passer à la classe MessageFormat pour substituer les paramètres. Le second paramètre de format() est un vararg, permettant de spécifier n’importe quel nombre de valeurs d’entrée.

Supposons que nous ayons un resource bundle rb :

String format = rb.getString("helloByName");
System.out.print(MessageFormat.format(format, "Tanguy", "Olivier"));

Cela affichera :

Hello, Tanguy and Olivier

Utilisation de la Classe Properties

Lorsque vous travaillez avec la classe ResourceBundle, vous pouvez également rencontrer la classe Properties. Elle fonctionne comme la classe HashMap que vous avez apprise, sauf qu’elle utilise des valeurs String pour les clés et les valeurs. Créons-en une et définissons quelques valeurs.

import java.util.Properties;

public class OptionsZoo {
    public static void main(String[] args) {
        var props = new Properties();
        props.setProperty("nom", "Notre zoo");
        props.setProperty("ouvert", "10h");
    }
}

La classe Properties est couramment utilisée pour gérer les valeurs qui pourraient ne pas exister.

System.out.println(props.getProperty("chameau"));       // null
System.out.println(props.getProperty("chameau", "Bob")); // Bob

Si une clé existait réellement, les deux instructions l’afficheraient. C’est ce qu’on appelle communément fournir une valeur par défaut, ou une valeur de secours, pour une clé manquante.

La classe Properties inclut également une méthode get(), mais seule getProperty() permet une valeur par défaut. Par exemple, l’appel suivant est invalide car get() ne prend qu’un seul paramètre :

props.get("ouvert");                                    // 10h
props.get("ouvert", "Le zoo sera ouvert bientôt");      // NE COMPILE PAS