Internationalisation des apps
Faites voyager votre application !
Tels Phileas Fogg, nous voulons que notre app fasse le tour du monde. J’ai eu l’occasion de travailler sur plusieurs apps qui devaient sortir dans beaucoup de pays et vais vous partager ici les différentes problématiques et solutions que j’ai pu utiliser. Bien évidemment ce ne sont pas des réponses universelles et chaque projet a sa spécificité, mais ça vous donne déjà un aperçu de l'étendue de la tâche de rendre son app compatible à l’international. Nous débuterons notre voyage en séparant notre code des traductions. Nous verrons ensuite comment localiser la user interface, puis les différentes méthodes et formatters pour localiser notre code. Un troisième aspect sera le Right-to-Left puis deux trois astuces pour traduire et tester. Commençons notre voyage !
Séparons le code et les traductions
Lorsqu’on veut faire voyager notre application, il y a deux concepts à bien comprendre localiser et internationaliser.
- Localiser signifie simplement traduire notre application. Ça comprend les textes et storyboards.
- Internationaliser, c’est rendre l’application accessible à une langue, un pays ou une culture. Ça inclut donc l’utilisation les bon formats que ce soit de string, de date ou de mesure, les couleurs et les images. Nous allons donc bien séparer les parties user interface, qui seront traduites, du code.
Localisons
Storyboard
Que ce soit un XIB ou un storyboard nous pouvons les localiser, en cliquant sur le bouton localiser. Il nous proposera alors de faire des fichiers .strings par langue. Ces fichiers seront ensuite utilisé dans l’exportation que nous verrons plus tard. Afin de ne pas modifier le storyboard par mégarde nous avons un systeme de lock qui existe. Nous le trouvons dans Editor -> Localization Locking. Il existe trois options:
- localizable properties,
- non-localizable properties,
- all properties. Le localizable properties empêche de modifier les textes, fonts, alignements, les contraintes dans la vue mais de pouvoir tout de même changer certaines propriétés tel que isHidden, contentMode, etc. Le non-localizable properties est l’inverse du précédent et le all properties bloque complètement toute la vue.
Localizable.string
Ca peut paraître évident mais nous allons devoir traduire TOUTES nos strings dans chacune des langues supportées. Ces traductions seront contenues dans un (ou plusieurs) fichier Localizable.strings. C’est un simple fichier clé valeur dont voici un exemple.
"WATCHES_LIST"="Liste de Montres";
Par défaut ce fichier existe lorsqu’on crée un projet. S’il n’y est pas, nous pouvons l’ajouter à la main depuis la création de fichier en tant que Strings file. Il y a différentes règles assez importantes pour ne pas se retrouver avec des choses bizarres. Nous n’allons jamais concaténer des strings ensemble. D’une langue à l’autre les tournures de phrases sont différentes. Un exemple concret serait en Français nous disont : “La montre de John” et en anglais “John’s watch”. Pour ajouter des variables (ici le nom) nous pouvons utiliser String.localizedStringWithFormat qui prend un NSLocalizedString et un varargs.
title = String.localizedStringWithFormat(NSLocalizedString("WATCHES", comment: ""), "Thibaut")
Deuxième chose à laquelle il faut faire attention, nous ne pouvons pas forcément utiliser les mêmes traductions selon le contexte. Nous pouvons prendre l’exemple d’un texte utilisé sur un bouton et d’un texte utilisé ailleurs. Si le contexte est différent, malgré la ressemblance dans le langage de développement, nous aurons bien deux traductions différentes. L’exemple type : “Clear” qui peut servir d’action et d’adjectif.
Localizable.stringdict
Lorsqu’une traduction diffère selon un numéro, nous pouvons utiliser les stringdicts. IOS prendra la forme la plus appropriée selon le paramètre. Ce n’est pas forcément évident, mais certaines langues tels que le Russe font la différence entre peu et beaucoup, entre zéro et un. Chaque traduction du stringdicts se présente comme suit :
<key>DEBTS_NUMBER</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@debts@</string>
<key>debts</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>You have no debt</string>
<key>one</key>
<string>You have one debt</string>
<key>two</key>
<string>You have two debts</string>
<key>few</key>
<string>You have %d debts</string>
<key>many</key>
<string>You have %d debts</string>
<key>other</key>
<string>You have %d debts</string>
</dict>
</dict>
Info.plist
Le fichier Info.plist est celui qui contient pas mal d’informations tel que le nom de l’app, sa version, les différents textes de demande d’autorisations, etc… Ce fichier peut être localisé de deux façons. On peut directement cliquer sur le bouton localiser que l’on trouve dans le File Inspector. La deuxième méthode est de créer un Infoplist.strings qui va servir à contenir les différentes traductions.
Internationalisons
Maintenant que nous avons traduit nos storyboards, nos textes et l’info.plist, attaquons nous à l’internationalisation. Nous avons à gérer les couleurs, les images et les formats.
Couleurs
Les couleurs sont quelque chose de très important, ça permet de signifier par exemple qu’une action est positive (vert) ou négative (rouge). Ces couleurs ont une signification différente selon la culture. En effet au Japon, le rouge a une connotation positive et le bleu négative. Actuellement je n’ai pas trouvé de meilleur solution que de mettre les couleurs dans le .xcasset et ensuite de localiser le nom de la couleur.
// Style object used in the whole app
enum Style {
static let acceptColor = UIColor(named: NSLocalizedString("acceptColor", comment: ""))
}
// Japanese version of Localizable.string
"acceptColor"="red";
---------
// French version of Localizable.string
"acceptColor"="green";
Images
Certaines images diffèrent selon les pays, par exemple si elles contiennent du texte. Nous avons différentes manières de le faire. On peut directement ajouter l’image dans le projet et ainsi le localiser, ce qui est recommandé par Apple. Ou bien passer par le fichier xcasset et donc mettre une image par langue et localiser le nom. Méthode qui ressemble beaucoup à celle utilisé pour les couleurs. Bien evidemment si l’image ne change pas, pas besoin de la localiser.
Les formatters
Dans nos apps, il arrive que nous utilisions des poids, distances qui dépendent du système (metric/imperial) utilisé. Pour que notre app soit la plus plaisante pour l’utilisateur nous nous devons d’afficher tout ceci dans le système de son choix (disponible dans les paramètres du téléphones dans Langue et région). Depuis iOS 10 nous avons un helper nommé Measurement qui permet de le faire pour nous.
var measurementFormatter = MeasurementFormatter()
measurementFormatter.unitStyle = .medium
let measure = Measurement(value: 25.0, unit: UnitLength.kilometers)
distanceLabel.text = measurementFormatter.string(from: measure)
Un deuxième helper est NumberFormatter, il permet de gérer tout ce qui est nombre, donc pourcentage, devises, point ou virgule. En dessous on peut voir comment il est utilisé.
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
let number = NSNumber(value: contact.price)
priceLabel.text = numberFormatter.string(from: number)
Pour les adresses nous avons CNPostalAddressFormatter (du framework Contact), en effet au Japon contrairement à la France, l’adresse est écrite du plus global au plus spécifique.
let addressFormatter = CNPostalAddressFormatter()
addressFormatter.style = .mailingAddress
let address = CNMutablePostalAddress()
address.street = "Champs-Élysées"
address.city = "Paris"
address.country = "France"
address.postalCode = "75008"
addressLabel.textaddressFormatter.string(from: address)
Même les noms ont une API pour le formatage. Au Japon nous utilisons par exemple le nom puis le prénom sans espace.
var personName = PersonNameComponents()
let formatter = PersonNameComponentsFormatter()
formatter.style = .default
personName.familyName = contact.lastName
personName.givenName = contact.firstName
nameLabel.text = formatter.string(from: personName)
Une autre qu’on utilise très souvent est le DateFormatter qui formate une date. On y pense pas forcément en premier lieu mais l'écriture de la date dépend beaucoup du pays, certains l'écrivent 21 décembre 2018, d’autres december 21st 2018.
var formatter = DateFormatter()
var date = Date()
formatter.dateStyle = .medium
formatter.timeStyle = .short
dateLabel.text = formatter.string(from: date)
Utiliser les informations du téléphone et non les deviner
Ca peut paraître évident, mais un téléphone en Français n’est peut être pas utilisé en France. L’inverse est vrai, ce n’est pas parce qu’on est en France que notre téléphone est en Français. Dans mes précédentes expériences à plusieurs reprises nous avons fais ces suppositions. Nous récupérions le pays de l’utilisateur pour l’envoyer au backend qui l’utilisait pour nous envoyer les push-notifications. Hors dans une push notification nous pouvons envoyer des clés de traductions qui laisse donc iOS utiliser la bonne traduction. Le soucis ici est d’avoir une cohérence dans l’application. Si l’utilisateur change sa langue dans les settings sans relancer l’application, il lui faut les bonnes traductions dans la notification. Si l’utilisateur a une locale qui n’est pas supportée il faut avoir un fallback similaire entre back et mobile. Donc si cette gestion est uniquement coté mobile, pas de risque de logique différente. Dans le payload de la push notification nous utiliserons donc les clés title-loc-key, action-loc-key, que j’expliquerais en détail dans un prochain article dédié. Lorsqu’un utilisateur a son téléphone en anglais il s’attends à avoir ses applications en anglais. Certains pays ont plusieurs langues comme la Suisse qui a une partie Française, une autre Allemande et une autre Italienne.
Right-to-left
Maintenant voyageons dans les pays utilisant une écriture right-to-left, tel que l’arabe, l’hébreux ou le Perse. Pour ce faire il faut tout d’abord cerner ce qui doit changer ou non. Les players, timelines, horloges, partitions musicales, les boussoles, les axes de graphiques et les images sont des vues qui ne doivent pas flipper dans ces pays là.
Si vous utilisez l’AutoLayout, la plupars de vos écrans fonctionneront comme par magie. Nous devons pour ce faire utiliser les contraintes leading et trailing (et non right et left). Pour connaitre l’orientation actuelle d’une vue :
if UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .leftToRight {
}
On peut setter directement le semanticContentAttribute en leftToRight par exemple si c’est pour gérer un player.
Les textes vont par défaut s’adapter. Il arrive parfois que l’on veuille afficher des textes en left-to-right dans un texte en right-to-left comme les numéros de téléphones ou les noms propres. Dans ce cas nous pouvons utiliser le LRE (Left-to-Right Embedding) character U+202A avant le texte et le PDF (Pop Directional Formatting) character U+202C après.
Passons maintenant aux images. Nous avons trois méthodes possible pour les flipper :
- utiliser imageFlippedForRightToLeftLayoutDirection sur l’UIImage
- ajouter les images dans xcode et les localiser.
- ajouter des images différentes dans les assets et changer d’image dans le code.
Testons tout ça
Nous voilà maintenant dans la phase la plus importante et certainement la plus critique : le test. Plus nous vérifions tôt dans le développement, plus nous pouvons corriger rapidement. Une des premières techniques que je vais vous proposer est donc la preview. Certains d’entre vous connaissent certainement cette option dans le Assistant Editor. Nous pouvons sélectionner Automatic, Manual, Localization mais également Preview. Chaque changement dans l’UI est répercuté directement et nous pouvons voir les impacts sur les différentes langues et pseudo-langues. En pseudo-langue nous avons la double length, utile pour les pays tels que l’allemand, l’accented qui vérifie tout les accents possible et imaginables et bounded string.
Deuxième outils très puissant : les schemes. Nous y avons beaucoup d’options tel que la langue, la région, et qui peut également prendre des pseudos-langues pour vérifier par exemple que dans dans langues longues tels que l’allemand, nous n’avons pas de crop. Ici comme précédemment, nous avons double-length, accented, bounded et un nouveau choix : right-to-left pseudo language. Autre option : Localization debugging, qui va mettre en majuscule les textes non traduits.
Traduisons
Le contexte lors de la traduction d’une application est important, c’est pour ça que xCode autorise l’internationalization incrémentale. Nous allons voir cela étape par étape. Nous allons tout d’abord exporter les localisations. Pour se faire, il faut aller sur le pbxproj, puis cliquer sur Editor, export localization. Ensuite sélectionnez les traductions dont vous avez besoin et save. Dans le dossier exporté nous avons accès à un dossier par localisation. Dans chaque dossier de localisation, nous avons un fichier contents.json, un dossier localized contents, notes et sources contents. Le dossier localized contents contient tout les .xliff qui seront utilisé par les traducteurs. Le dossier Notes permet d’ajouter des informations supplémentaires qui seront donnés aux traducteurs. Dans le dossier Sources Contents, nous avons pour chaque localisation, les storyboards et les localisable.strings. xliff.
Le schéma ci-dessous vous montre comment fonctionne l’internationalisation incrémentale. Le principe est d’internationaliser tout au long du développement. On va donc de façon régulière exporter les fichiers de traductions pour les traducteurs et importer les nouvelles traductions dans l’app. Cela peut par exemple être fait sur le serveur de CI (Continuous Integration), et ainsi les traducteurs ont toujours la dernière version.
Nous n’allons pas utiliser ces fichiers comme ça. Il existe des sites très bien fait tels que PhraseApp ou CrowdIn qui nous mâche le travail et nous permet d’interfacer entre xcode et les traducteurs.
Conclusion
Vous voilà fin prêt à découvrir le monde ! Apple nous facilite d’année en année le travail en nous offrant tout un tas de frameworks. Ce n’est pas forcément évident au départ d'écrire une application fonctionnelle pour chaque pays et il faudra parfois faire des correctifs pour y arriver. Ajouter une nouvelle langue ou un nouveau pays est un travail minutieux qui demande patience et test pour être certain d’offrir une experience la plus parfaite possible pour les utilisateurs de votre application. Nous avons vu ici beaucoup trucs et astuces, il en existe encore beaucoup, et je serais heureux d’entendre vos retours d’expérience sur la sortie d’applications à l’international.