i18n et L10n dans une application web Spring

Traduction des applications

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

L'internet est une vitrine qui n'a pas de frontières géographiques. C'est pourquoi notre application web sera adaptée à des utilisateurs venant d'autres pays, ayant d'autres habitudes de navigation (comme par exemple le format de la date affichée) et, surtout, parlant d'autres langues que français.

C'est pourquoi on va internationaliser notre application. Au titre d'introduction, il faut mentionner que ce processus s'appelle brièvement i18n. Pourquoi une nomination si étrange ? Les lettres i et n renvoient à la première et à la dernière lettre du mot internationalisation. Le chiffre 18 signifie que 18 lettres séparent ces les deux.

La localisation, pour les mêmes raisons qu'internationalisation, s'appelle brièvement L10n.

Internationaliser une application ?

Le processus d'internationalisation se répose sur la préparation d'une application à des langues et à des cultures différentes. Le but principal de cette action est de baser l'affichage des messages sur des fichiers contenant des traductions. Grâce à cela, l'ajout d'une nouvelle langue supportée va se traduire par l'ajout d'un nouveau fichier de traductions.

L'internationalisation est donc liée à la conception du logiciel, afin qu'il soit adaptable à des différentes langues et cultures.

Localiser une application ?

Cependant, l'internationalisation n'est pas un fin en soi. Elle sert à la localisation qui est sa mise en place réelle. Il s'agit donc de placer les messages traduits, les caractères de police, les règles rédactionnelles, l'affichage de la date ou mêmes les couleurs spécifiques à une culture.

Configurer internationalisation sous Spring

La mise en place d'internationalisation dans une application Spring se fait via les beans : localeChangeIntereceptor, messageSource et localeResolver, comme ci-dessous :

	<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="messageSource">
	  <property name="basenames">
	    <list>
	      <value>/WEB-INF/i18n/messages</value>
	      <value>/WEB-INF/i18n/application</value>
	    </list>
	  </property>
	  <property name="defaultEncoding" value="UTF-8"/>
	  <property name="fallbackToSystemLocale" value="false"/>
	</bean>
	<mvc:interceptors>
		<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" p:paramName="language"/>
	</mvc:interceptors>
	<beans:bean class="org.springframework.web.servlet.i18n.SessionLocaleResolver" id="localeResolver"> 
        <beans:property name="defaultLocale" value="fr" />
	</beans:bean>

Le premier bean, messageSource, définit les endroits où sont placés les fichiers avec les messages traduits (propriété nommée basenames). Les autres éléments déterminent l'encodage par défaut (defaultEncoding). La propriété fallbackToSystemLocale indique si l'application doit utiliser la Locale par défaut du système si le fichier des traductions n'est pas trouvée pour la langue explicitement souhaitée.

Le bean spécifié dans <mvc:interceptors /> permet d'intercepter un changement de la langue. Son attribut, p:paramName, signale le nom du paramètre qui va s'occuper de modifier la langue de l'application. Dans notre cas, si un lien va contenir ?language=pl, LocaleChangeInterceptor va se charger de traiter la requête en préparant l'application à un internaute polonais. Cette préparation va se faire via l'invocation de la méthode setLocale() du resolver défini dans localeResolver.

En ce qui concerne localeResolver. Ce bean implémente l'interface org.springframework.web.servlet.LocaleResolver. Il permet de récupérer les locales directement depuis la session associée à l'utilisateur connecté.

Les traductions dans les vues

L'utilisation des messages traduits dans les fichiers de vue est très simple. Il suffit d'importer le taglib du Spring et ensuite invoquer le message selon une des deux méthodes proposées :

    <%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
    <spring:message code="user.label.login" var="labelLogin"/>
    < -- ... -->
    <form:label path="login">${labelLogin}</form:label>

Cette façon de procéder garantit une bonne clarté du code. Tous les messages sont alors récupérés et traduits au début de fichier. Leur affichage se déroule par la suite via une simple écriture de variable (${labelLogin}). Cette méthode permet aussi une réutilisation des traductions. Une fois définie, la variable peut être placée dans plusieurs endroits différents sur la page.

<c:if test="${error} == true">
  <spring:message code="user.register.error" />
</c:if>

Cette solution est surtout beaucoup plus économe au niveau d'écriture. Cependant, elle ne permet pas de réutiliser le message traduit.

Le tag <spring:message /> peut aussi prendre un attribut supplémentaire, text, qui définira le texte par défaut à afficher si la traduction n'est pas trouvée.

Il est aussi possible de récupérer les messages pouvant prendre en compte les paramètres dynamiques :

<-- will display : Your login X is already used -->
<spring:message code="user.error.login"  arguments="${subscriber.login}" htmlEscape="false"/>

Attention, le séparateur par défaut est un virgule. On peut le surcharger en spécifiant l'attribut argumentSeparator. Cela peut être utile dans les situations où nos variables placées dans argumetns contiennent des virgules.

Pour le formatage des dates et des chiffres, on peut se baser sur la librairie de formatage et ses balises :
- <fmt:formatNumber /> : formate un nombre en fonction de la Locale. Elle peut aussi être utilisée dans le formattage et l'affichage des prix.
- <fmt:formatDate /> : formate une date en fonction de la Locale.

Les traductions dans le contrôleur

Parfois on peut vouloir récupérer les messages d'erreur directement dans l'application (logs ou autres fonctionnalités...). Pour achever cette tâche il faut injecter le bean messageSource et ensuite l'utiliser dans le contrôleur à la manière suivante :

    // ...
    @Autowired
    private MessageSource messageSource;
    
    // ...
    String labelLogin = messageSource.getMessage("user.label.login", null, Locale.English);
    String customLoginMsg = messageSource.getMessage("welcome.user",new Object[]{"Connected"}, Locale.ENGLISH);

Dans l'exemple ci-dessus on récupère les 2 différents messages. Le premier (labelLogin) prend le texte tel qu'il est traduit. Le deuxième rajoute une variable ("Connected") au message récupéré. Il se peut que le message final soit donc "Hello Connected".

Notre application a besoin d'une instance de la classe Lang. Cependant, Spring utilise Locale pour savoir quelle langue est actuellement utilisée par l'utilisateur. Afin de pouvoir récupérer la Lang adaptée, on va utiliser l'annotation nouvellement créée, @LocaleLang. Elle sera résolue grâce au LocaleLangResolver dont le code se présente ainsi :
public class LocaleLangResolver implements HandlerMethodArgumentResolver {
    final Logger logger = LoggerFactory.getLogger(LocaleLangResolver.class);
    @Autowired
    private LangRepository langRepository;
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(LocaleLang.class);
    }

    @Override
    public Lang resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, 
        NativeWebRequest nativeWebRequest, WebDataBinderFactory binderFactory) {
        Locale locale = RequestContextUtils.getLocale((HttpServletRequest) nativeWebRequest.getNativeRequest());
        logger.info("locale is " + locale);
        if (locale == null) locale = Locale.getDefault();

        Lang lang = null;
        lang = langRepository.getByIso(locale.getLanguage().toUpperCase());
        if (lang == null) lang = langRepository.getDefaultLang();

        return lang;
    }
}

La seule méthode intéressante est resolveArgument(). Au tout début on récupère la Locale de l'utilisateur grâce à sa requête. La récupération s'effectue grâce au RequestContextUtils. Il s'agit d'une classe utilitaire qui facilite l'accès à des propriétés de la requête spécifiées par DispatcherServlet. On l'utilise pour retrouver la Locale de l'utilisateur. La Locale trouvée est utilisée pour retrouver l'instance Lang adéquate. Si celle-ci n'existe pas, la langue par défaut est retournée.

Une question ? Une remarque ?

*

*

Un conseil Symfony2

Comment envoyer l'en-tête 404 ?

La détermination de l'en-tête 404 pour les pages qui n'existent pas est très facile sous Symfony2. L'un des moyens consiste à récupérer la page et renvoyer le code dans la réponse. Pour ce faire, dans un premier temps il faut récupérer le template qui sera affichée sur l'écran. Dans un deuxième temps, il faut utiliser la méthode setStatusCode() de l'objet \Symfony\Component\HttpFoundation\Response . L'ensemble se présente ainsi :

public function deniedAccessAction(Request $request)
{
  // get displayed template
  $response = $this->render('SitesTestBundle:Errors:deniedAccess.html.php', array());
  // set 404 status code
  $response->setStatusCode(404);
  // send all to the browser
  return $response;
}