Localiser des Applications avec l'IA

Autres langues : English Español Deutsch 日本語 한국어 Português 中文

Que vous envisagiez de localiser votre projet ou que vous appreniez simplement comment le faire, l’IA peut être un bon point de départ. Elle offre un point d’entrée économique pour les expériences et les automatisations.

Dans cet article, nous allons parcourir une telle expérience. Nous allons :

Si vous n’avez jamais fait de localisation et souhaitez apprendre, c’est peut-être une bonne idée de commencer ici. À part quelques détails techniques, l’approche est largement universelle, et vous pouvez l’appliquer dans d’autres types de projets.

Si vous êtes déjà familier avec les bases et souhaitez simplement voir l’IA en action, vous pouvez passer directement à Traduire les textes ou cloner mon fork pour parcourir les commits et évaluer les résultats.

Obtenir le projet

Créer une application juste pour une expérience de localisation serait excessif, alors faisons un fork d’un projet open-source. J’ai choisi Spring Petclinic, une application web exemple utilisée pour présenter le framework Spring pour Java.

Fork et clone Petclinic (nécessite GitHub CLI)
gh repo fork https://github.com/spring-projects/spring-petclinic --clone=true

Si vous n’avez jamais utilisé Spring auparavant, certains extraits de code peuvent ne pas vous sembler familiers, mais, comme je l’ai déjà mentionné, cette discussion est agnostique en termes de technologie. Les étapes sont à peu près les mêmes quel que soit le langage et le framework.

Internationalisation

Avant qu’une application puisse être localisée, elle doit être internationalisée.

L’internationalisation (aussi appelée i18n) est le processus d’adaptation d’un logiciel pour supporter différentes langues. Elle commence généralement par l’externalisation des chaînes d’interface utilisateur vers des fichiers spéciaux, communément appelés bundles de ressources.

Les bundles de ressources contiennent les valeurs textuelles pour différentes langues :

en.json:

{
  "greeting": "Hello!",
  "farewell": "Goodbye!"
}

es.json:

{
  "greeting": "¡Hola!",
  "farewell": "¡Adiós!"
}

Pour que ces valeurs arrivent à l’interface utilisateur, l’interface doit être explicitement programmée pour utiliser ces fichiers.

Cela implique généralement une bibliothèque d’internationalisation ou une fonctionnalité intégrée au langage, dont le but est de remplacer les textes de l’interface par les valeurs correctes pour une locale donnée. Des exemples de telles bibliothèques incluent i18next (JavaScript), Babel (Python), et go-i18n (Go).

Java supporte l’internationalisation nativement, donc nous n’avons pas besoin d’ajouter de dépendances supplémentaires au projet.

Examiner les sources

Java utilise des fichiers avec l’extension .properties pour stocker les chaînes localisées pour l’interface utilisateur.

Heureusement, il y en a déjà plusieurs dans le projet. Par exemple, voici ce que nous avons pour l’anglais et l’espagnol :


welcome=Welcome
required=is required
notFound=has not been found
duplicate=is already in use
nonNumeric=must be all numeric
duplicateFormSubmission=Duplicate form submission is not allowed
typeMismatch.date=invalid date
typeMismatch.birthDate=invalid date
welcome=Bienvenido
required=Es requerido
notFound=No ha sido encontrado
duplicate=Ya se encuentra en uso
nonNumeric=Sólo debe contener numeros
duplicateFormSubmission=No se permite el envío de formularios duplicados
typeMismatch.date=Fecha invalida
typeMismatch.birthDate=Fecha invalida

Externaliser les chaînes d’interface utilisateur n’est pas quelque chose que tous les projets font universellement. Certains projets peuvent avoir ces textes directement codés en dur dans la logique de l’application.

Tip icon

Externaliser les textes d’interface utilisateur est une bonne pratique avec des avantages au-delà de l’internationalisation. Cela rend le code plus facile à maintenir et favorise la cohérence dans les messages d’interface. Si vous démarrez un projet, envisagez d’implémenter l’i18n le plus tôt possible.

Test de fonctionnement

Ajoutons un moyen de changer la locale via les paramètres d’URL. Cela nous permettra de tester si tout est entièrement externalisé et traduit dans au moins une langue.

Pour y parvenir, nous ajoutons la classe suivante pour gérer le paramètre de locale :

WebConfig.java
import java.util.Locale;

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Bean
	public LocaleResolver localeResolver() {
		SessionLocaleResolver slr = new SessionLocaleResolver();
		slr.setDefaultLocale(Locale.US);
		return slr;
	}

	@Bean
	public LocaleChangeInterceptor localeChangeInterceptor() {
		LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
		lci.setParamName("lang");
		return lci;
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(localeChangeInterceptor());
	}
}

Maintenant que nous pouvons tester différentes locales, nous lançons le serveur et comparons la page d’accueil pour plusieurs paramètres de locale :

Page d'accueil de Spring Petclinic avec un message de bienvenue en espagnol et le reste en anglais.

Changer la locale se reflète dans l’interface, ce qui est une bonne nouvelle. Il apparaît cependant que le changement de locale n’a affecté qu’une partie des textes. Pour l’espagnol, Welcome est devenu Bienvenido, mais les liens dans l’en-tête sont restés les mêmes, et les autres pages sont toujours en anglais. Cela signifie que nous avons du travail à faire.

Modifier les templates

Le projet Spring Petclinic génère des pages en utilisant des templates Thymeleaf, alors examinons les fichiers de templates.

En effet, certains textes sont codés en dur, donc nous devons modifier le code pour référencer les bundles de ressources à la place.

Heureusement, Thymeleaf a un bon support pour les fichiers Java .properties , donc nous pouvons incorporer des références aux clés de bundle de ressources correspondantes directement dans le template :


findOwners.html
<h2>Find Owners</h2>
findOwners.html
<h2 th:text='#{heading.find.owners}'>Find Owners</h2>
messages.properties
heading.find.owners=Find Owners

Le texte précédemment codé en dur est toujours là, mais il sert maintenant de valeur de repli, qui ne sera utilisée que s’il y a une erreur lors de la récupération d’un message localisé approprié.

Le reste des textes est externalisé de manière similaire ; cependant, il y a plusieurs endroits qui nécessitent une attention particulière. Par exemple, certains avertissements proviennent du moteur de validation et doivent être spécifiés en utilisant des paramètres d’annotation Java :


@Column(name = "first_name")
@NotBlank
private String firstName;
@Column(name = "first_name")
@NotBlank(message = "{field.validation.notblank}")
private String firstName;

À quelques endroits, la logique doit être modifiée :

<h2>
    <th:block th:if="${pet['new']}">New </th:block>Pet
</h2>

Dans l’exemple ci-dessus, le template utilise une condition. Si l’attribut new est présent, New est ajouté au texte de l’interface. Par conséquent, le texte résultant est soit New Pet soit Pet selon la présence de l’attribut.

Cela peut casser la localisation pour certaines locales, à cause de l’accord entre le nom et l’adjectif. Par exemple, en espagnol, l’adjectif serait Nuevo ou Nueva selon le genre du nom, et la logique existante ne prend pas en compte cette distinction.

Une solution possible à cette situation est de rendre la logique encore plus sophistiquée. C’est généralement une bonne idée d’éviter la logique compliquée chaque fois que possible, j’ai donc opté pour le découplage des branches à la place :

<h2>
    <th:block th:if="${pet['new']}" th:text="#{pet.new}">New Pet</th:block>
    <th:block th:unless="${pet['new']}" th:text="#{pet.update}">Pet</th:block>
</h2>

Des branches séparées simplifieront également le processus de traduction et la maintenance future du code.


Le formulaire New Pet a aussi une astuce. Sa liste déroulante Type est créée en passant la collection de types d’animaux au template selectField.html :

<input th:replace="~{fragments/selectField :: select (#{pet.type}, 'type', ${types})}" />

Contrairement aux autres textes d’interface, les types d’animaux font partie du modèle de données de l’application. Ils proviennent d’une base de données au moment de l’exécution. La nature dynamique de ces données nous empêche d’extraire directement les textes vers un bundle de propriétés.

Il y a encore plusieurs façons de gérer cela. Une façon est de construire dynamiquement la clé du bundle de propriétés dans le template :


<option th:each="item : ${items}"
        th:value="${item}"
        th:text="${item}">dog</option>
<option th:each="item : ${items}"
        th:value="${item}"
        th:text="#{'pettype.' + ${item}}">dog</option>

Dans cette approche, plutôt que de rendre directement cat dans l’interface, nous le préfixons avec pettype. , ce qui donne pettype.cat . Nous utilisons ensuite cette chaîne comme clé pour récupérer le texte d’interface localisé :


pettype.bird=bird
pettype.cat=cat
pettype.dog=dog
pettype.bird=pájaro
pettype.cat=gato
pettype.dog=perro
Info icon

Vous avez peut-être remarqué que nous venons de modifier le template d’un composant réutilisable. Puisque les composants réutilisables sont destinés à servir plusieurs clients, il n’est pas correct d’y apporter une logique client.

Dans ce cas particulier, le composant de liste déroulante devient lié aux types d’animaux, ce qui est problématique pour quiconque veut l’utiliser pour autre chose.

Ce défaut était là depuis le début - voir dog comme texte par défaut des options. Nous avons juste propagé ce défaut plus loin. Cela ne devrait pas être fait dans de vrais projets et nécessite une refactorisation.


Bien sûr, il y a plus de code de projet à internationaliser ; cependant, le reste s’aligne principalement avec les exemples ci-dessus. Pour un examen complet de tous mes changements, vous êtes invité à examiner les commits dans mon fork.

Ajouter les clés manquantes

Après avoir remplacé tous les textes d’interface par des références aux clés de bundle de propriétés, nous devons nous assurer d’introduire toutes ces nouvelles clés. Nous n’avons pas besoin de traduire quoi que ce soit à ce stade, juste ajouter les clés et les textes originaux au fichier messages.properties .

IntelliJ IDEA a un bon support Thymeleaf. Il détecte si un template référence une propriété manquante, donc vous pouvez repérer les manquantes sans beaucoup de vérification manuelle :

IntelliJ IDEA affiche un avertissement dans le modèle qui fait référence à une clé de propriété manquante. IntelliJ IDEA affiche un avertissement dans le modèle qui fait référence à une clé de propriété manquante.

Avec toutes les préparations terminées, nous arrivons à la partie la plus intéressante du travail. Nous avons toutes les clés, et nous avons toutes les valeurs pour l’anglais. Où obtenons-nous les valeurs pour les autres langues ?

Traduire les textes

Pour traduire les textes, nous allons créer un script qui utilise un service de traduction externe. Il existe de nombreux services de traduction disponibles, et de nombreuses façons d’écrire un tel script. J’ai fait les choix suivants pour l’implémentation :

Je n’ai pas fait de recherche extensive, donc ces choix sont quelque peu arbitraires. N’hésitez pas à expérimenter et découvrir ce qui vous convient le mieux.

Info icon

Si vous décidez d’utiliser le script ci-dessous, vous devez créer un compte chez DeepL et passer votre clé API personnelle au script via la variable d’environnement DEEPL_KEY

Voici le script :

import os
import requests
import json

deepl_key = os.getenv('DEEPL_KEY')
properties_directory = "../src/main/resources/messages/"


def extract_properties(text):
    properties = {}

    for line in text:
        line = line.strip()

        if line and not line.startswith('#') and '=' in line:
            key_value = line.split('=')
            key = key_value[0].strip()
            value = key_value[1].strip()
            if key and value:
                properties[key] = value

    return properties


def missing_properties(properties_file, properties_checklist):
    with open(properties_file, 'r') as f:
        text = f.readlines()

    present_properties = extract_properties(text)
    missing = {k: v for k, v in properties_checklist.items() if k not in present_properties.keys()}
    return missing


def translate_property(value, target_lang):
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'DeepL-Auth-Key {deepl_key}',
        'User-Agent': 'LocalizationScript/1.0'

    }
    url = 'https://api-free.deepl.com/v2/translate'
    data = {
        'text': [value],
        'source_lang': 'EN',
        'target_lang': target_lang,
        'preserve_formatting': True
    }

    response = requests.post(url, headers=headers, data=json.dumps(data))

    return response.json()["translations"][0]["text"]


def populate_properties(file_path, properties_checklist, target_lang):
    with open(file_path, 'a+') as file:
        properties_to_translate = missing_properties(file_path, properties_checklist)
        for key, value in properties_to_translate.items():
            new_value = translate_property(value, target_lang)
            property_line = f"{key}={new_value}\n"
            print(property_line)
            file.write(property_line)


with open(properties_directory + 'messages.properties') as base_properties_file:
    base_properties = extract_properties(base_properties_file)

languages = [
    # configure languages here
    "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "fi"
]

for language in languages:
    populate_properties(properties_directory + f"messages_{language}.properties", base_properties, language)

Le script extrait les clés du bundle de propriétés par défaut ( messages.properties ) et cherche leurs traductions dans les bundles spécifiques à la locale. S’il trouve qu’une certaine clé manque de traduction, le script demandera la traduction à l’API DeepL et l’ajoutera au bundle de propriétés.

J’ai spécifié 10 langues cibles, mais vous pouvez modifier la liste ou ajouter vos langues préférées, tant que DeepL les supporte.

Le script peut être encore optimisé pour envoyer les textes à traduire par lots de 50. Je ne l’ai pas fait ici pour garder les choses simples.

Exécuter le script

Exécuter le script sur 10 langues m’a pris environ 5 minutes. Le tableau de bord d’utilisation montre 8348 caractères, ce qui aurait coûté 0,16€ si nous étions sur un plan payant.

En résultat, les fichiers suivants apparaissent :

De plus, les propriétés manquantes sont ajoutées à :

Mais qu’en est-il des traductions réelles ? Pouvons-nous les voir déjà ?

Vérifier les résultats

Relançons l’application et testons-la en utilisant différentes valeurs de paramètre lang . Par exemple :

Personnellement, je trouve très satisfaisant de voir chaque page correctement localisée. Nous avons fourni des efforts, et maintenant ça porte ses fruits :


Page d'accueil Spring Petclinic entièrement localisée
Page d'accueil Spring Petclinic entièrement localisée
Page d'accueil Spring Petclinic entièrement localisée
Page d'accueil Spring Petclinic entièrement localisée
Page d'accueil Spring Petclinic entièrement localisée

Résoudre les problèmes

Les résultats sont impressionnants. Cependant, si vous regardez de plus près, vous pouvez découvrir des erreurs qui proviennent d’un manque de contexte. Par exemple :

visit.update = Visit

Visit peut être à la fois un nom et un verbe. Sans contexte supplémentaire, le service de traduction produit une traduction incorrecte dans certaines langues.

Cela peut être résolu soit par une édition manuelle, soit en ajustant le flux de travail de traduction. Une solution possible est de fournir du contexte dans les fichiers .properties en utilisant des commentaires :

# Noun. Heading. Displayed on the page that allows the user to edit details of a veterinary visit
visit.update = Visit

Nous pouvons ensuite modifier le script de traduction pour analyser ces commentaires et les passer avec le paramètre context :

url = 'https://api-free.deepl.com/v2/translate'
data = {
    'text': [value],
    'source_lang': 'EN',
    'target_lang': target_lang,
    'preserve_formatting': True,
    'context': context
}

Au fur et à mesure que nous approfondissons et considérons plus de langues, nous pourrions rencontrer plus de choses qui doivent être améliorées. C’est un processus itératif.

S’il y a une chose indispensable dans ce processus, c’est la révision et les tests. Indépendamment du fait que nous améliorions l’automatisation ou que nous éditions sa sortie, nous trouverons nécessaire de conduire un contrôle qualité et une évaluation.

Au-delà du scope

Spring Petclinic est un projet simple, mais réaliste, tout comme les problèmes que nous venons de résoudre. Bien sûr, la localisation présente beaucoup de défis qui sont hors du scope de cet article, incluant :

Chacun de ces sujets mérite un article à part entière. Si vous souhaitez en lire plus, je serai heureux de couvrir ces sujets dans des articles séparés.

Résumé

Bien, maintenant que nous avons fini de localiser notre application, il est temps de réfléchir à ce que nous avons appris :

J’espère que vous avez apprécié cet article, et j’aimerais avoir vos retours ! Si vous avez des questions de suivi ou souhaitez discuter de l’approche, n’hésitez pas à me contacter.

Au plaisir de vous voir dans les prochains articles !

all posts ->