Localisation d'applications dans iOS
Partie 1. Qu'avons-nous?
Guide des ressources de chaînes localisées
Présentation
Il y a quelques années, je me suis plongé dans le monde magique du développement iOS qui, avec toute son essence, me promettait un avenir heureux dans le domaine de l'informatique. Cependant, en plongeant profondément dans les fonctionnalités de la plate-forme et de l'environnement de développement, j'ai dû faire face à de nombreuses difficultés et inconvénients pour résoudre des tâches apparemment très triviales: le «conservatisme innovant» d'Apple rend parfois les développeurs très sophistiqués afin de satisfaire le client débridé «WANT».
L'un de ces problèmes est la question de la localisation des ressources de chaîne de l'application. Je voudrais consacrer plusieurs de mes premières publications sur les étendues de Habr à ce problème.
Au départ, j'espérais intégrer mes réflexions dans un seul article, mais la quantité d'informations que j'aimerais présenter était assez importante. Dans cet article, je vais essayer de découvrir l'essence des mécanismes standard pour travailler avec des ressources localisées en mettant l'accent sur certains aspects qui sont négligés par la plupart des guides et tutoriels. Le matériel est principalement destiné aux développeurs débutants (ou à ceux qui n'ont pas rencontré de telles tâches). Pour les développeurs expérimentés, ces informations peuvent ne pas être particulièrement utiles. Mais à propos des inconvénients et des inconvénients que l'on peut rencontrer dans la pratique, je raconterai à l'avenir ...
Hors de la boîte. Comment le stockage des ressources de chaîne est organisé dans les applications iOS
Pour commencer, on note que la présence de mécanismes de localisation dans la plateforme est déjà un énorme plus, car sauve le programmeur d'un développement supplémentaire et définit un format unique pour travailler avec les données. Et souvent, les mécanismes de base sont suffisants pour la mise en œuvre de projets relativement petits.
Et donc, quelles opportunités Xcode nous offre-t-il «prêt à l'emploi»? Tout d'abord, examinons la norme de stockage des ressources de chaîne dans un projet.
Dans les projets à contenu statique, les données de chaîne peuvent être stockées directement dans l'interface (fichiers de balisage .storyboard
et .xib
, qui sont à leur tour des fichiers XML rendus à l'aide des outils Interface Builder ) ou dans du code. La première approche nous permet de simplifier et d'accélérer le processus de balisage des écrans et des affichages individuels, comme le développeur peut observer la plupart des changements sans construire l'application. Cependant, dans ce cas, il n'est pas difficile d'exécuter la redondance des données (si le même texte est utilisé par plusieurs éléments, s'affiche). La deuxième approche élimine simplement le problème de la redondance des données, mais conduit à la nécessité de remplir les écrans manuellement (en définissant des IBOutlet
supplémentaires et en leur affectant des valeurs de texte correspondantes), ce qui conduit à son tour à la redondance du code (bien sûr, sauf dans les cas où le texte doit être installé directement par le code de l'application).
De plus, Apple fournit une extension de fichier standard .strings
. Cette norme réglemente le format de stockage des données de chaîne sous la forme d'un tableau associatif ( "-"
):
"key" = "value";
La clé est sensible à la casse, permet l'utilisation d'espaces, de soulignements, de ponctuation et de caractères spéciaux.
Il est important de noter que malgré la syntaxe simple, les fichiers Strings sont des sources régulières d'erreurs lors de la compilation, de l'assemblage ou du fonctionnement d'une application. Il y a plusieurs raisons à cela.
Tout d'abord, les erreurs de syntaxe. Les points-virgules manquants, les signes égaux, les guillemets supplémentaires ou non échappés entraîneront inévitablement une erreur de compilation. De plus, Xcode pointera vers le fichier avec l'erreur, mais ne mettra pas en évidence la ligne dans laquelle quelque chose ne va pas. La recherche d'une telle faute de frappe peut prendre un temps considérable, surtout si le fichier contient une quantité importante de données.
Deuxièmement, la duplication des clés. L'application, bien sûr, ne se bloquera pas à cause de cela, mais des données incorrectes peuvent être affichées pour l'utilisateur. Le fait est que lors de l'accès à une ligne par clé, la valeur correspondant à la dernière occurrence de la clé dans le fichier est relevée.
En conséquence, une conception simple nécessite que le programmeur soit très minutieux et attentif lors du remplissage des fichiers avec des données.
Bien informé les développeurs peuvent immédiatement s'exclamer: "Mais qu'en est-il de JSON et de PLIST? Qu'est-ce qui ne leur a pas plu?" Eh bien, premièrement, JSON
et PLIST
(en fait, XML
ordinaire) sont des normes universelles qui permettent de stocker à la fois des chaînes et des données numériques, logiques ( BOOL
), binaires, l'heure et la date, ainsi que des collections - indexées ( Array
) et associatives ( Dictionary
) tableaux. En conséquence, la syntaxe de ces normes est plus saturée et donc plus facile à grignoter. Deuxièmement, la vitesse de traitement de ces fichiers est légèrement inférieure à celle des fichiers Strings, encore une fois en raison de la syntaxe plus complexe. Cela ne veut pas dire que pour travailler avec eux, vous devez effectuer un certain nombre de manipulations dans le code.
Localisé, localisé, mais pas localisé. Localisation de l'interface utilisateur
Et donc, avec les normes établies, essayons maintenant de savoir comment tout utiliser.
Allons dans l'ordre. Tout d'abord, créez une application simple à vue unique et ajoutez des composants de texte au Main.storyboard du ViewController .

Dans ce cas, le contenu est stocké directement dans l'interface. Pour le localiser, vous devez procéder comme suit:
1) Accédez aux paramètres du projet

2) Ensuite - de la cible au projet

3) Ouvrez l'onglet Info

Dans la section Localisations , nous voyons immédiatement que nous avons déjà l'entrée "Anglais - Langue de développement" . Cela signifie que l'anglais est défini comme langue de développement (ou par défaut).
Ajoutons maintenant une autre langue. Pour ce faire, cliquez sur " + " et sélectionnez la langue souhaitée (par exemple, j'ai choisi le russe). Caring Xcode nous propose immédiatement de choisir les fichiers à localiser pour la langue ajoutée.

Cliquez sur Terminer , voyez ce qui s'est passé. Dans le navigateur de projet, des boutons pour afficher l'imbrication sont apparus près des fichiers sélectionnés. En cliquant dessus, nous voyons que les fichiers précédemment sélectionnés contiennent les fichiers de localisation créés.

Par exemple, Main.storyboard (Base)
est le fichier de balisage d'interface par défaut dans le langage de développement de base, et lors de la formation de la localisation, le Main.strings (Russian)
associé Main.strings (Russian)
été créé par paires - un fichier de chaîne pour la localisation russe. En l'ouvrant, vous pouvez voir ce qui suit:
/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = "Label"; /* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */ "cpp-y2-Z0N.placeholder" = "TextField"; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */ "EKl-Rz-Dc2.normalTitle" = "Button";
Ici, en général, tout est simple, mais pour plus de clarté, nous allons considérer plus en détail, en faisant attention aux commentaires générés par le Xcode attentionné:
/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = "Label";
Voici une instance de la classe UILabel
avec la valeur "Label"
pour le paramètre text
. ObjectID
- l'identifiant de l'objet dans le fichier de balisage - c'est une ligne unique assignée à n'importe quel composant au moment où il est placé sur le Storyboard/Xib
. Il est à cause de ObjectID
et le nom du paramètre de l'objet (dans ce cas, le text
) est formé par une clé, et le dossier lui - même peut être interprété formellement comme suit:
Définissez le paramètre de texte de l'objet tQe-tG-eeo sur Étiquette.
Dans cet enregistrement, seule la « valeur » est susceptible de changer. Remplacez " Label " par " Label ". Nous ferons de même avec d'autres objets.
/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = ""; /* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */ "cpp-y2-Z0N.placeholder" = " "; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */ "EKl-Rz-Dc2.normalTitle" = "";
Nous lançons notre application.

Mais que voyons-nous? L'application utilise une localisation de base. Comment vérifier si nous avons effectué le transfert correctement?
Ici, il vaut la peine de faire une petite digression et de creuser un peu dans le sens des fonctionnalités de la plate-forme iOS et de la structure de l'application.
Pour commencer, envisagez de modifier la structure du projet dans le processus d'ajout de localisation. Voici à quoi ressemble le répertoire du projet avant d'ajouter la localisation russe:

Et donc après:

Comme nous pouvons le voir, Xcode a créé un nouveau répertoire ru.lproj
, dans lequel il a placé les chaînes localisées créées.

Et où est la structure du projet Xcode pour l'application iOS terminée? Et malgré le fait que cela aide à mieux comprendre les fonctionnalités de la plateforme, ainsi que les principes de distribution et de stockage des ressources directement dans l'application finie. L'essentiel est que lors de l'assemblage d'un projet Xcode, en plus de générer un fichier exécutable, l'environnement transfère des ressources (fichiers de disposition d'interface Storyboard / Xib , images, fichiers de ligne, etc.) vers l'application finie, en préservant la hiérarchie spécifiée au stade du développement.
Pour travailler avec cette hiérarchie, Apple fournit la classe Bundle(NSBundle)
( traduction gratuite ):
Apple utilise le Bundle
pour fournir un accès aux applications, aux frameworks, aux plugins et à de nombreux autres types de contenu. Bundle
est d' organiser les ressources dans les sous-répertoires clairement définis, et les structures de faisceau varient en fonction de la plate - forme et le type. À l'aide de bundle
, vous pouvez accéder aux ressources d'un package sans connaître sa structure. Bundle
est une interface unique pour rechercher des éléments, en tenant compte de la structure du package, des besoins des utilisateurs, des localisations disponibles et d'autres facteurs pertinents.
Recherche et découverte d'une ressource
Avant de commencer à travailler avec une ressource, vous devez spécifier son bundle
. La classe Bundle
a de nombreux constructeurs, mais main est le plus souvent utilisée. Bundle.main
fournit un chemin d'accès aux répertoires contenant le code exécutable actuel. De cette façon, Bundle.main
permet d'accéder aux ressources utilisées par l'application actuelle.
Considérez la structure Bundle.main
à l'aide de la classe FileManager
:

De ce qui précède , nous pouvons conclure que lorsque les charges de l' application il a formé Bundle.main
, analyse l'unité de localisation actuelle (paramètres régionaux du système), la localisation des applications et des ressources localisées. Ensuite, l'application sélectionne parmi toutes les localisations disponibles celle qui correspond à la langue actuelle du système et récupère les ressources localisées correspondantes. S'il n'y a pas de correspondance, les ressources du répertoire par défaut sont utilisées (dans notre cas, la localisation en anglais, car l'anglais a été défini comme langage de développement, et la nécessité d'une localisation supplémentaire des ressources peut être négligée). Si vous changez la langue de l'appareil en russe et redémarrez l'application, l'interface correspondra déjà à la localisation russe.

Mais avant de fermer le sujet de la localisation de l'interface utilisateur via Interface Builder , il convient de noter une autre manière notable. Lors de la création de fichiers de localisation (en ajoutant une nouvelle langue au projet ou dans l'inspecteur de fichiers localisés), il est facile de remarquer que Xcode offre la possibilité de choisir le type de fichier à créer:

Au lieu d'un fichier de ligne, vous pouvez facilement créer un Storyboard/Xib
localisé qui enregistrera tout le balisage du fichier de base. Un avantage considérable de cette approche est que le développeur peut immédiatement voir comment le contenu sera affiché dans une langue particulière et corriger immédiatement la disposition de l'écran, surtout si la quantité de texte diffère, ou si une autre direction du texte est utilisée (par exemple, en arabe, en hébreu), etc. . Mais en même temps, la création de fichiers Storyboard / Xib supplémentaires augmente considérablement la taille de l'application elle-même (tout de même, les fichiers de chaîne prennent beaucoup moins d'espace).
Par conséquent, lors du choix de l'une ou l'autre méthode de localisation d'interface, il convient de considérer quelle approche sera la plus appropriée et la plus pratique dans une situation particulière.
Faites-le vous-même. Utilisation de ressources de chaîne localisées dans le code
Si tout va bien avec le contenu statique, tout est plus ou moins clair. Mais qu'en est-il du texte défini directement dans le code?
Les développeurs du système d'exploitation iOS s'en sont occupés.
Pour travailler avec des ressources textuelles localisées, le framework Foundation fournit la famille de méthodes NSLocalizedStrings
dans Swift
NSLocalizedString(_ key: String, comment: String) NSLocalizedString(_ key: String, tableName: String?, bundle: Bundle, value: String, comment: String)
et macros dans Objective-C
NSLocalizedString(key, comment) NSLocalizedStringFromTable(key, tbl, comment) NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)
Commençons par l'évidence. Le paramètre key
est la clé de chaîne dans le fichier Strings; val
(valeur par défaut) - la valeur par défaut utilisée si la clé spécifiée n'est pas dans le fichier; comment
- (moins évident) une brève description de la chaîne localisée (en fait, elle ne comporte pas de fonctionnalité utile et est destinée à expliquer le but de l'utilisation d'une chaîne spécifique).
Quant aux paramètres tableName
( tbl
) et bunble
, alors ils doivent être examinés plus en détail.
tableName
( tbl
) est le nom du fichier String (pour être honnête, je ne sais pas pourquoi Apple l'appelle une table), qui contient la ligne dont nous avons besoin par la clé spécifiée; lorsqu'elle est transférée, l'extension .string
pas spécifiée. La possibilité de naviguer entre les tables vous permet de ne pas stocker les ressources de chaîne dans un seul fichier, mais de les distribuer à votre discrétion. Cela vous permet de vous débarrasser de la congestion des fichiers, de simplifier l'édition et de minimiser les risques d'erreurs.
Le paramètre bundle
étend encore la navigation dans les ressources. Comme mentionné précédemment, le bundle est un mécanisme d'accès aux ressources d'application, c'est-à-dire que nous pouvons déterminer indépendamment la source des ressources.
Un peu plus. Nous irons directement à la Fondation et envisagerons la déclaration de méthodes (macros) pour une image plus claire, car la grande majorité des didacticiels ignorent tout simplement ce point. Le cadre Swift n'est pas très informatif:
/// Returns a localized string, using the main bundle if one is not specified. public func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String
"Le bundle principal renvoie une chaîne localisée" - tout ce que nous avons. Objective-C est un peu différent.
#define NSLocalizedString(key, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil] #define NSLocalizedStringFromTable(key, tbl, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \ [bundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \ [bundle localizedStringForKey:(key) value:(val) table:(tbl)]
Ici, vous pouvez déjà voir clairement que nul autre que bundle
(dans les deux premiers cas mainBundle
) fonctionne avec les fichiers de ressources de chaîne - le même que dans le cas de la localisation d'interface. Bien sûr, je pourrais immédiatement dire cela, compte tenu de la classe Bundle
( NSBundle
) dans le paragraphe précédent, mais à cette époque, ces informations NSBundle
pas une valeur pratique particulière. Mais dans le contexte de l'utilisation de lignes dans le code, cela ne peut pas être dit. En fait, les fonctions globales fournies par la Fondation ne sont que des wrappers sur les méthodes de bundle standard, dont la tâche principale est de rendre le code plus concis et plus sûr. Personne n'interdit d'initialiser le bundle
manuellement et d'accéder directement aux ressources en son nom, mais de cette manière, il apparaît (quoique très, très faible) la probabilité de formation de liens circulaires et de fuites de mémoire.
Les exemples ci-dessous décrivent comment travailler avec des fonctions globales et des macros.
Voyons comment tout cela fonctionne.
Créez d'abord un fichier String qui contiendra nos ressources de chaîne. Appelez-le Localizable.strings * et ajoutez-le.
"testKey" = "testValue";
( Les fichiers de chaînes sont localisés exactement de la même manière que Storyboard / Xib , donc je ne décrirai pas ce processus. Nous remplacerons " testValue " dans le fichier de localisation russe par " test value *".)
Important! Sous iOS, un fichier portant ce nom est le fichier de ressources de chaîne par défaut, c'est-à-dire si vous ne spécifiez pas le nom de la table tableName
( tbl
), l'application se tbl
automatiquement à Localizable.strings
.
Ajoutez le code suivant à notre projet
//Swift print("String for 'testKey': " + NSLocalizedString("testKey", comment: ""))
et exécutez le projet. Après avoir exécuté le code, une ligne apparaîtra dans la console
String for 'testKey': testValue
Tout fonctionne bien!
De même avec l'exemple de localisation de l'interface, modifiez la localisation et exécutez l'application. Le résultat de l'exécution du code sera
String for 'testKey':
Essayons maintenant d'obtenir la valeur par la clé, qui ne se trouve pas dans le fichier Localizable.strings
:
//Swift print("String for 'unknownKey': " + NSLocalizedString("unknownKey", comment: ""))
Le résultat de l'exécution de ce code sera
String for 'unknownKey': unknownKey
Puisqu'il n'y a pas de clé dans le fichier, la méthode renvoie la clé elle-même en conséquence. Si un tel résultat est inacceptable, il est préférable d'utiliser la méthode
//Swift print("String for 'testKey': " + NSLocalizedString("unknownKey", tableName: nil, bundle: Bundle.main, value: "noValue", comment: ""))
où il y a un paramètre de value
( valeur par défaut ). Mais dans ce cas, vous devez spécifier la source des ressources - bundle
.
Les chaînes localisées prennent en charge le mécanisme d'interpolation, similaire aux chaînes iOS standard. Pour ce faire, ajoutez un enregistrement au fichier de chaîne à l'aide de littéraux de chaîne ( %@
, %li
, %f
, etc.), par exemple:
"stringWithArgs" = "String with %@: %li, %f";
Pour sortir une telle ligne, vous devez ajouter un code du formulaire
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", 123, 123.098 ))
Mais lorsque vous utilisez de tels modèles, vous devez être très prudent! Le fait est que iOS surveille strictement le nombre, l'ordre des arguments, la correspondance de leurs types avec les littéraux spécifiés. Ainsi, par exemple, si vous remplacez la chaîne comme deuxième argument au lieu de la valeur entière
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", "123", 123.098 ))
alors l'application remplacera le code entier de la chaîne "123" à la place de la non-concordance
"String with some: 4307341664, 123.089000"
Si vous le sautez, nous obtenons
"String with some: 0, 123.089000"
Mais si vous manquez dans la liste d'arguments objet correspondant %@
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "123", 123.098 ))
alors l'application plantera simplement au moment de l'exécution du code.
Pousse-moi, bébé! Localisation des notifications
Une autre tâche importante dans le travail avec des ressources de chaîne localisées, dont je voudrais parler brièvement, est la tâche de localiser les notifications. L'essentiel est que la plupart des didacticiels (à la fois sur les Push Notifications
et les Localizable Strings
) négligent souvent ce problème, et de telles tâches ne sont pas si rares. Par conséquent, face à cela pour la première fois, le développeur peut avoir une question raisonnable: est-ce possible en principe? Je ne considérerai pas ici le mécanisme de fonctionnement d' Apple Push Notification Service
, d'autant plus qu'à partir d'iOS 10.0, les notifications Push et locales sont implémentées via le même cadre - UserNotifications
.
Vous devez faire face à un problème similaire lors du développement d'applications client-serveur multilingues. Quand une telle tâche m'a confronté pour la première fois, la première chose qui m'est venue à l'esprit était de résoudre le problème de la localisation des messages côté serveur. L'idée était extrêmement simple: l'application envoie la localisation actuelle au backend au démarrage, et le serveur sélectionne le message approprié lors de l'envoi du push . Mais le problème a immédiatement mûri: si la localisation de l'appareil a changé et que l'application n'a pas été redémarrée (n'a pas mis à jour les données dans la base de données), alors le serveur a envoyé le texte correspondant à la dernière localisation "enregistrée". Et si l'application est installée sur plusieurs appareils avec différentes langues système à la fois, alors l'implémentation entière fonctionnerait comme l'enfer sait quoi. Puisqu'une telle solution me semblait immédiatement la béquille la plus folle, j'ai immédiatement commencé à chercher des solutions adéquates (drôle, mais dans de nombreux forums, les "développeurs" ont conseillé de localiser les pushies exactement sur le backend ).
La bonne décision était horriblement simple, mais pas entièrement évidente. Au lieu du JSON standard envoyé par le serveur sur APNS
"aps" : { "alert" : { "body" : "some message"; }; };
besoin d'envoyer JSON du formulaire
"aps" : { "alert" : { "loc-key" : "message localized key"; }; };
où la loc-key
est utilisée pour transmettre la clé de chaîne localisée à partir du fichier Localizable.strings
. En conséquence, le message push est affiché conformément à la localisation actuelle de l'appareil.
Le mécanisme d'interpolation des chaînes localisées dans les notifications push fonctionne de la même manière:
"aps" : { "alert" : { "loc-key" : "message localized key"; "loc-args" : [ "First argument", "Second argument" ]; }; };
La clé loc-args
transmet un tableau d'arguments qui doivent être incorporés dans le texte de notification localisé.
Pour résumer ...
Et donc, qu'avons-nous finalement:
- Stockage des données de chaîne standard dans les fichiers spéciaux
.string
avec un simple et la syntaxe abordable; - la possibilité de localiser l'interface sans manipulations supplémentaires dans le code;
- accès rapide aux ressources localisées à partir du code;
- génération automatique de fichiers de localisation et structuration des ressources du répertoire du projet (application) à l'aide des outils Xcode;
- la possibilité de localiser le texte de notification.
, Xcode , .
.