Publié par
Il y a 11 mois · 16 minutes · iOS, Mobile

Internationaliser vos applications iOS

Par le biais diTunes Connect et de l’App Store, Apple offre un déploiement quasi instantané des applications sur plus de 150 pays. En tirant parti du marché international et en adaptant votre application localement, vous augmentez vos chances d’atteindre une audience plus large et d’être mis en avant sur l’App Store. Souvent considéré à tort en fin de développement, il vous faudra penser à restructurer vos applications mobiles pour tirer parti des API d’Apple et supporter du contenu localisé.

Cet article synthétise les concepts fondamentaux du processus d’internationalisation et de localisation en iOS. Nous y présenterons une liste de bonnes pratiques et nos conseils se feront sur la base de l’écriture d’une application moderne en Swift. Aussi nous couvrirons un cas de figure particulier qui consiste à supporter plusieurs langues dans une même application sans passer par la configuration système.

1. Back to basics:

a. Internationalisation Localisation 

Rappelons tout d’abord qu’il existe une différence entre le processus d’internationalisation et de localisation bien que ces deux concepts soient parfois confondus.

Le processus d’internationalisation consiste à effectuer les modifications côté code et interface afin que le support multilingue soit assuré dans votre application. Il s’agit d’utiliser pour cela les APIs d’Apple pour faire en sorte que l’application ait un rendu adapté à la région ou bien le pays où l’application sera utilisée. 

 La localisation consiste uniquement à effectuer le travail de traduction de l’interface et des ressources pour chacune des langues supportées : ce sera le point d’entrée du processus d’internationalisation. Ce travail peut être effectué par un service tiers si vous ne vous sentez pas avoir une âme de traducteur ou bien par vous même. 

b. Internationalisation

L’internationalisation des textes visibles par l’utilisateur se fait essentiellement en deux parties :

  • l’internationalisation de l’interface qui se base sur les textes inclus dans les storyboards ou les xibs ;
  • l’internationalisation du code pour les textes présents dans le code source.

Le but de ces deux procédures est d’extraire l’ensemble de ces textes et de les placer dans des fichiers *.string. Ces fichiers peuvent ensuite être exportés dans un format standard : le XML Localization Interchange File Format (xliff) et être soumis à une équipe de localisation pour la traduction en plusieurs langues. Voyez les fichiers xliff donc comme une couche d’abstraction supplémentaire aux fichiers .strings afin d’en faciliter le management auprès de vos traducteurs.

L’internationalisation de l’interface peut s’effectuer depuis la section Localizations dans le panneau info de votre projet. Pensez à activer le système d’internationalisation par default appelé « base internationalisation » (cette option est activée par défaut depuis XCode 5 et toute versions ultérieures). Vous n’aurez alors plus besoin de maintenir plusieurs storyboards et xibs pour chaque langue supportée.

BaseInternationalization

En cliquant sur le button + (bouton juste au dessus de l’option “Use Base Internationalization”) ou bien en passant par le menu command Editor -> add localization vous pourrez ajouter une nouvelle langue pour les fichiers ressources de votre choix : vous pourrez en l’occurence sélectionner les .xibs et storyboards que vous voudrez localiser.

Avec l’option « Use Base Internationalization » Xcode va créer un dossier Base.lproj où se situe l’ensemble des fichiers sources. Ensuite pour chaque nouvelle langue supportée Xcode va générer d’autres répertoires avec l’extension .lproj. Par exemple ce sera fr.lproj pour le français et on ne trouvera que les fichiers .strings associés aux fichiers sources.

L’internationalisation du code consiste à envelopper l’ensemble des strings visibles par l’utilisateur avec la fonction NSLocalizedString.  Dans votre code vous allez alors devoir changer l’ensemble des strings concernées.

var greetingText = NSLocalizedString("Home.Greeting-Label", value: "Hello!", comment: "Greeting Text On The Home View") 

Une fois cette tâche réalisée il existe deux pratiques pour extraire les strings marquées par la fonction NSLocalizedString. La plus simple consiste à utiliser l’outil d’import et d’export fournit par Xcode pour la localisation : au moment de l’import Xcode va créer un fichier localize.strings pour chaque langue supportée et pour les strings à localiser dans votre code.

Une approche plus manuelle consiste à utiliser l’outil genstrings. En ouvrant un terminal et en se positionnant dans le répertoire home et en executant la commande suivante :

find ./ "*.m" -print0 | xargs -0 genstrings -o fr.lproj

vous allez extraire l’ensemble des appels à la macro NSLocalizedString et les insérer en un seul fichier strings.localize qui sera ensuite placé dans le répertoire fr.lproj.

c. La localisation

La localisation doit s’effectuer de préférence à travers l’outil d’import et d’export d’Xcode. Depuis Xcode 6, il n’est plus nécessaire d’éditer manuellement les fichiers localize.strings pour effectuer les traductions. Préférer plutôt l’outil d’export d’Xcode permettant de rassembler l’ensemble des fichiers .strings en un seul fichier XLIFF (XML Localisation Interchange File Format) pour une langue donnée. Ce format facilitera ainsi le traitement des fichiers à localiser pour vos équipes de traducteurs.

L’export est possible en sélectionnant votre projet depuis le navigateur de fichier et en choisissant le menu Editor >  Export For Localization. Une fois la traduction des fichiers XLIFF effectués, il faudra réimporter les fichiers XLIFFs en utilisant cette fois le menu Editor > Import For Localization.

2. Les bonnes pratiques d’internationalisation et de localisation

Le processus d’internationalisation et la localisation va bien au delà du simple usage de la macro NSLocalizedString et de la traduction des fichiers Localize.strings. Ces étapes présentent de nombreux pièges et il est simple de faire des suppositions erronées. Au cours de cette section, nous tâcherons de mettre en avant la meilleure approche à adopter selon différents cas de figures.

a. Faites bon usage de NSLocalizedString

Dans sa plus simple expression NSLocalizedString prend en paramètre une clef et un commentaire :

NSLocalizedString(<key>, comment: <comment>)

On pourrait donc utiliser NSLocalized(“OK”, comment:“”) pour définir le plus simplement possible le label d’un bouton OK. Cette approche présente pourtant plusieurs problèmes. Tout d’abord, le commentaire est primordial et il ne faudrait l’omettre. Rappelez vous qu’il sera exporté dans le fichier .string généré par Xcode et il permettra aux traducteurs de comprendre le contexte du mot à traduire. Ensuite, la clef est utilisée à mauvais escient : elle identifie de façon unique une string littérale. Utiliser donc plutôt un espace de nommage unique comme Settings.Menu.OK-Button par exemple. Sachez aussi que vous pouvez définir une valeur par défaut et ce grâce au paramètre value. Vous n’aurez alors pas besoin de définir un nouveau fichier .strings pour votre langue par default. Au final votre invocation à NSLocalizedString devrait plutôt ressembler à NSLocalized(“Settings.Menu.OK-Button”, value:”OK”, comment: “Button OK pour le menu settings »).

Méfiez vous des extensions Swift avec NSLocalizedString, L’usage des extensions en Swift est très tentante car elle permet d’ajouter du sucre syntaxique et rend votre code plus lisible. Par exemple l’extension suivante:

public extension String {
    public func localized(comment:String = "") -> String {
        return NSLocalizedString(self, comment: comment)
    }
}
permettrait d’écrire “OK ».localized(comment:“Button OK pour le menu settings”). Cependant, rappelez-vous que NSLocalizedString contient un flag statique utilisé pour la génération automatique des fichiers .strings. En opérant de la sorte vous allez casser toutes opportunités d’exporter proprement vos strings à localiser avec l’outils genstrings. Soyez donc très vigilants par rapport à ce genre de pratique.

d. Genstrings: une récupération en fait partielle des strings

Bien qu’il soit possible de récupérer l’ensemble des strings définies dans votre code source avec l’outil genstrings, il se peut que vous n’ayez pas couvert l’ensemble des strings à localiser. Certaines images peuvent par exemple inclure du texte ou bien vous pouvez utiliser des icônes qui auront en fait un sens différent selon le pays et donc devront être elles aussi localisées. Un moyen simple de tester que tous vos contenus sont correctement localisés peut être fait en activant l’accessibilité. Enfin, sachez que le nom de votre bundle id peut être localisé.

b. Eviter l’internationalisation des storyboards/xib

Même si nous avons vu que le système de « base internationalization” offre une simplicité supplémentaire dans la gestion des storyboards et xibs, préférez l’approche où l’ensemble des strings affichées sont définies en code plutôt que de créer un fichier localize.string par langue supportée dans vos storyboards/xibs.

Il vous faudra créer des outlets pour correctement relier toutes les strings définies en storyboard avec celle en code. Cela peut sembler fastidieux mais vous bénéficierez directement des avantages suivant :

  • vous n’aurez plus besoin de supporter des fichiers .strings supplémentaires spécialement dédiées aux storyboards/xib ce qui à fortiori pourra être moins source d’erreurs ;
  • vous pourrez changer de langue dynamiquement au sein de l’app et nous verrons comment procéder par la suite.

b. Utiliser NSNumberFormatter pour les devises

N’essayez pas de formatter manuellement une devise. Utiliser plutôt NSFormatter pour afficher correctement la devise selon le pays. En passant en paramètre la locale, l’identifiant de la devise et le style du formateur (en l’occurence .CurrencyStyle pour la devise) le bon formatage sera automatiquement appliqué et vous n’aurez pas à vous soucier du symbole à utiliser ni de son positionnement.

let numberFormatter = NSNumberFormatter()
numberFormatter.numberStyle = .CurrentStyle
numberFormatter.currencyCode = "USD"
numberFormatter.locale = NSLocale(localeIdentifier:"en_US")
let price = numberFormatter.stringFromNumber(42.0)

c. Tester le support pour les langues aux noms allongés

Pour tester le comportement de votre application pour des langues allongées tels que l’allemand activer l’option Double Length Pseudolanguage pour l’option de lancement de l’application. Cette option aura pour but de dupliquer la taille des strings localisées ce qui vous permettra de valider si votre interface est suffisamment flexible pour ces langues.

DoubleLengthPseudoLanguage

d. Tester le support RTL (Right-To-Left)

iOS supporte de base les systèmes d’écritures nécessitant l’écriture de droite à gauche telle que pour l’alphabet Arabe et Hébreu.

Il y a deux règles de base à respecter pour que UIKit gère automatiquement l’ajustement du layout pour ces écritures:

  • Utiliser les contraintes d’autolayout leading/trailing et non left/right.
  • Utiliser l’alignement .Natural et non .Left pour les textes.

Concrètement voyons comment ces propriétés sont définies en Storyboard. En inspectant les contraintes d’alignement de types leading et trailing via l’inspecteur d’XCode, vous verrez que l’option Respect Language est bien sélectionné ce qui signifie que le support LTR est bien activé pour ces contraintes. Par contre si cette option est désactivée les contraintes repasseront en alignement left/right.

LeadingAlignment

Pour ce qui des propriétés d’alignement pour les textes comme sur les UILabel choisissez l’alignement “- – -“ correspondant à l’alignement .Natural.

NaturalAlignment

Enfin afin de vous assurer que vous avez fait les modifications adéquates sur les contraintes et les labels assurez que le support pour l’écriture droite à gauche est correctement implémenté en activant l’option “Right To Left Pseudo Language” dans les options de lancement de l’application via les schemes.

RightToLeftPseudoLanguage

e. Attention à la gestion des pluriels et adjectifs

La composition des mots n’est pas interchangeable de la même façon pour une langue donnée. Si il existe plusieurs versions d’un adjectif selon le genre et le pluriel ce n’est pas le cas pour certaine langue comme l’anglais. Par exemple l’adjectif « chaud » pourra être décliné en quatre cas en français: chaud(e)(s) : il n’y a donc pas de correspondance 1 à 1 selon une langue donnée.

De la même manière; la gestion des pluriels n’est pas binaire. Pour certaines langues (comme le russe) la fin des noms est étroitement corrélé à la quantité utilisée. Utiliser plutôt les .stringdics : vous pourrez alors définir les règles à adopter selon la langue de façon plus aisée.

f. Utiliser le contexte culturel

Dernier point et non des moindres il est primordial de considérer le contexte et l’audience de votre application. Le langage utilisé pour une application de traitement texte sera certainement plus formel que celui d’un jeu vidéo. Idéalement faites tester votre application par des personnes qui connaissent non seulement la langue qui devra être traduire mais surtout la culture du pays pour cette langue.

3. Implémentation du changement de langue in-app:

a. Connaître les limitations de l’OS

Par défaut le choix d’une langue sur iOS se fait via l’option de « langue et région » du menu settings. Une des recommandations d’Apple sur le support multilingue est d’utiliser la langue système pour définir la langue de l’application.

Cette approche peut s’avérer trop restrictive pour certaines applications pour lesquelles il est impératif de pouvoir changer de langue à la volée. Imaginer par exemple le cas d’une application in-house présentée sur un même appareil qui circule entre les mains de personnes de différentes nationalités : il serait très contraignant de changer de langue en procédant uniquement via les settings de l’appareil surtout qu’un redémarrage de plusieurs secondes serait nécessaire.

Plusieurs approches pour résoudre cette problématique sont accessibles sur le net mais nous aimerions garantir que les contraintes suivantes soient respectées pour notre choix d’implémentation :

  • le choix d’une langue ne doit pas nécessiter de redémarrage de l’application.
  • nous souhaitons garder l’usage de NSLocalizedString afin de garder les possibilités d’exports via genstrings.
  • nous ne souhaitons pas passer par l’usage de NSLocalizedStringFromTable où chaque table référencerait une langue différente mais nous détournerait de l’usage des bundles localisés et par la même occasion des outils d’exports.

Au final notre solution consistera à forcer NSLocalizedString a utiliser un bundle localisé dans la langue sélectionnée.

b. Mettre en place un language center:

Tout d’abord, nous allons créer un langage center dont le premier rôle sera de gérer l’ensemble des langues supportées par l’application. Un enum se prête très bien à cette fonctionnalité :

public enum Language: String, Equatable {
    case en_US = "en-US"
    case es_ES = "es-ES"
    case fr_FR = "fr-FR"
    // Add some more supported languages if needed...

    public static func build(value: String?) -> Language {
        if let value = value, language = Language(rawValue: value) {
            return language
        }
        return Language.defaultLanguage
    }

    public static var defaultLanguage: Language {
        return Language.en_US
    }
    var fullname: String {
        switch (self) {
            case .en_US: return "English (Default)"
            case .es_ES: return "Español (España)"
            case .fr_FR: return "Français (France)"
        }
    }
    static let allLanguages = [es_ES, fr_FR, en_US]
}

public func ==(lhs: Language, rhs: Language) -> Bool {
    return lhs.rawValue == rhs.rawValue
}

Notez que l’enum hérite d’une String : nous pourrons ainsi sauvegarder la langue choisie par l’utilisateur dans les préférences de l’application via NSUserDefault.

Nous allons ensuite rajouter à l’enum le calcul du bundleID : celui-ci nous permettra de récupérer le bon bundle localisé pour une langue donnée.

var bundleID: String {
    return self.rawValue
}

c. Créer une extension sur NSBundle:

Pour finaliser notre implémentation, nous allons créer une extension sur NSBundle permettant de charger dynamiquement un bundle localisé lors de l’invocation à NSLocalizedString :

public extension NSBundle {
    private struct AssociatedKeys {
        static var bundleKey = "bundleKey"
        static var token: dispatch_once_t = 0
    }
    public static func setupLanguage(language: Language) {
        dispatch_once(&AssociatedKeys.token) {
            object_setClass(NSBundle.mainBundle(), NSBundleLanguage.self)
        }

        if let bundlePath = NSBundle.mainBundle().pathForResource(language.bundleID, ofType:"lproj") {
            self.languageBundle = NSBundle(path:bundlePath)
        } else {
            self.languageBundle = nil
        }
    }

    static var languageBundle : NSBundle? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.bundleKey) as? NSBundle
        }
        set {
            objc_setAssociatedObject(
                    self,
                    &AssociatedKeys.bundleKey,
                    newValue as NSBundle?,
                    objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

Il nous faudra par la même occasion surcharger la méthode localizedStringForKey(key: String, value: String?, table tableName: String?) sur une classe héritant de NSBunble. Cette méthode sera en effet invoquée pour tout appel à la fonction NSLocalizedString et se chargera d’invoquer au runtime la même méthode sur le main bundle ou le bundle localisé si une langue à été sélectionnée.

class NSBundleLanguage : NSBundle {
    override func localizedStringForKey(key: String, value: String?, table tableName: String?) -> String {
        if let bundle = NSBundle.languageBundle {
            return bundle.localizedStringForKey(key, value: value, table: tableName)
        }
        return super.localizedStringForKey(key, value: value, table: tableName)
    }
}

et voila ; vous venez d’implémenter le changement de langue in-app sans dénaturer le processus d’internationalisation recommandé par Apple !

Ce mécanisme ne s’appliquant qu’à l’usage de NSLocalizedString, n’oubliez pas par contre de référencer l’ensemble des textes visibles à l’utilisateur en storyboard via des IBOutlets pour vous permettre de rafraichir ces textes au moment du changement de langue et de les configurer via l’usage de NSLocalizedString.

Conclusion:

Cet article nous a permis de couvrir un ensemble de bonnes pratiques tout autour de l’internationalisation d’une application iOS. Il est primordial d’engager ce processus le plus en amont possible dans votre cycle de développement : Si vous ne prévoyez pas de localiser votre application, vous aurez quand même un énorme bénéfice de pouvoir vérifier l’ensemble des Strings visibles à l’utilisateur. Et si à l’inverse vous êtes amené à localiser votre application un peu plus tard, vous aurez quand même une plus value à englober les strings avec NSLocalizedString au fil du temps plutôt qu’en une seule traite. Dans tous les cas et comme nous l’avons souligné à travers cet article, soyez bien conscient des nombreux pièges associés à cette pratique car il est très simple de faire les mauvaises suppositions.

Vous souhaitez approfondir le sujet ? n’hésitez pas à vous référer aux dernières conférences sur cette thématique:

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *