Drapeaux de widgets : pourquoi les codes ISO de pays surpassent les noms dépendants de la locale
Le bug était subtil. Un widget iOS affichait le nom de pays correct en anglais mais montrait le mauvais emoji de drapeau en allemand. En locale française, un autre pays affichait le mauvais drapeau. Les drapeaux n'étaient pas aléatoires — ils étaient systématiquement incorrects selon un pattern qui a pris un moment à reconnaître : la recherche retournait le drapeau du pays dont le nom, dans la locale actuelle, ressemblait au nom d'affichage stocké.
La cause racine était une recherche de pays utilisant des noms de pays localisés comme clés de dictionnaire. Des noms qui sont des chaînes stables dans une locale deviennent des chaînes différentes dans une autre. Le correctif a nécessité de reconstruire intégralement la recherche autour des codes ISO 3166-1 alpha-2, et la leçon va bien au-delà des emojis de drapeaux.
Comment le bug est apparu
Le widget affichait une liste de pays avec leurs emojis de drapeaux correspondants à côté d'autres données. L'implémentation originale stockait les noms d'affichage des pays comme identifiant — la chaîne “Germany” ou “France” — et utilisait un dictionnaire indexé sur ces noms pour rechercher l'emoji de drapeau correspondant.
En locale anglaise, cela fonctionnait correctement. Le nom stocké était “Germany”, la clé du dictionnaire était “Germany”, et la recherche retournait le bon drapeau. Mais quand la locale de l'appareil était réglée en allemand, NSLocale.current.localizedString(forRegionCode: "DE") retourne “Deutschland”. Le widget appelait cette méthode pour résoudre les noms d'affichage dynamiquement. Le nom affiché devenait donc “Deutschland” — correct, dans le contexte — mais la recherche dans le dictionnaire indexé en anglais échouait, retombait sur une valeur par défaut, et affichait le mauvais drapeau.
La variante la plus insidieuse était les correspondances partielles. Dans certaines locales, le nom localisé d'un pays partage un préfixe ou une quasi-correspondance avec le nom anglais d'un autre pays. Sans vérifications d'égalité stricte, les recherches approximatives retournaient des résultats plausibles mais incorrects. Un processus de QA exécuté uniquement en locale anglaise ne l'aurait jamais détecté.
Pourquoi les noms dépendants de la locale sont un piège
Le problème fondamental est d'utiliser une chaîne dépendante de la locale comme identifiant. Les identifiants doivent être stables — la même valeur quel que soit qui les lit, quand, ou dans quel contexte linguistique. Le nom d'un pays dans la locale actuelle est une valeur de présentation. Il est correct et approprié de l'afficher à un utilisateur. Il est incorrect et fragile de l'utiliser comme clé de recherche, identifiant de base de données, clé de cache, ou toute autre forme de référence stable.
Ce n'est pas limité aux noms de pays. Les noms de devises, les noms de langues, les noms d'affichage de locale, et les descriptions de fuseaux horaires partagent tous la même propriété : ce sont des chaînes dépendantes de la locale que le système génère pour l'affichage, pas pour l'identité. NSLocale.current.localizedString(forCurrencyCode: "EUR") retourne “Euro” en anglais et “Euro” en allemand, ce qui coïncide — mais localizedString(forCurrencyCode: "GBP") retourne “British Pound” en anglais et “Britisches Pfund” en allemand. Utiliser l'un ou l'autre comme clé produit la même catégorie de bug.
La règle est simple : utiliser le code ISO comme identifiant partout dans la logique, et appeler la méthode de localisation uniquement lors de la construction d'une chaîne pour l'affichage à l'utilisateur. Ne jamais stocker la chaîne localisée. Ne jamais l'utiliser comme clé. Ne jamais la comparer programmatiquement à travers les locales.
Générer des emojis de drapeaux depuis des codes ISO
Les emojis de drapeaux sont encodés en Unicode comme des paires de Symboles d'Indicateur Régional. La séquence de caractères pour un drapeau de pays donné correspond au code ISO 3166-1 alpha-2 du pays, avec chaque lettre remplacée par son équivalent en Symbole d'Indicateur Régional. Les Symboles d'Indicateur Régional vont de U+1F1E6 (pour A) à U+1F1FF (pour Z).
En Swift, générer un emoji de drapeau depuis un code ISO est une courte fonction utilitaire :
func flagEmoji(for isoCode: String) -> String {
let base: UInt32 = 0x1F1E6 - 65 // offset from A
return isoCode.uppercased().unicodeScalars.compactMap {
UnicodeScalar(base + $0.value)
}.map { String($0) }.joined()
}
Cette fonction prend “DE” et retourne 🇩🇪. Elle prend “SG” et retourne 🇸🇬. Elle ne nécessite aucun dictionnaire, aucun fichier de mapping, et aucune maintenance. Le mapping est défini par le standard Unicode et suit directement les codes ISO 3166-1 alpha-2.
Le seul cas limite qui mérite d'être géré est le fait que tous les codes à deux lettres n'ont pas d'emoji de drapeau correspondant. UN, EU et quelques autres codes régionaux produisent des emojis de drapeaux que toutes les plateformes ne rendent pas de manière cohérente. Pour un widget traitant des données d'États-nations, restreindre les entrées aux codes d'États membres de l'ONU valides et tester sur la plateforme cible est suffisant.
Le mapping de plus de 150 pays dont nous avions encore besoin
Générer les emojis de drapeaux depuis des codes ISO de manière algorithmique gère le côté affichage. Mais nous avions également besoin de stocker et d'interroger les pays de manière structurée — le widget filtrait les pays par région, les triait selon divers critères, et associait chacun à des métadonnées supplémentaires. Cela nécessitait un mapping approprié.
Le mapping que nous avons construit associe les codes ISO 3166-1 alpha-2 à un nom anglais canonique (pour usage interne et journalisation), un code numérique ISO 3166-1 (pour les systèmes nécessitant des identifiants numériques), et un regroupement régional. Le nom anglais dans ce mapping n'est jamais utilisé comme clé de recherche — il n'existe que pour le débogage et l'affichage administratif dans des contextes non localisés. Tous les noms destinés aux utilisateurs sont générés au moment du rendu via NSLocale.current.localizedString(forRegionCode:).
Cette séparation est la décision architecturale clé. La couche données parle en codes ISO. La couche présentation traduit dans la locale actuelle. Elles ne se rencontrent jamais au milieu. Une recherche de drapeau, un regroupement régional, un ordre de tri — tout cela fonctionne sur des codes ISO. Seul le texte final rendu pour l'utilisateur passe par la localisation.
Étendre le correctif aux autres données
Une fois le bug du drapeau corrigé, nous avons audité le reste de la base de code pour le même pattern. Le même problème est apparu à trois autres endroits : un module d'affichage de devises utilisant des noms de devises localisées comme clés de cache, un sélecteur de langue stockant des noms de langues localisées dans les préférences utilisateur, et un module de formatage de dates comparant des noms de mois dépendants de la locale.
Le correctif dans chaque cas suivait le même pattern. Remplacer la chaîne dépendante de la locale par son équivalent ISO ou IETF stable : codes de devises (ISO 4217) au lieu de noms de devises, tags de langue BCP 47 au lieu de noms de langues localisés, valeurs numériques de mois au lieu de chaînes de noms de mois. Les chaînes localisées deviennent des préoccupations exclusivement de la couche vue.
L'audit valait la peine d'être mené de manière systématique. Chacun de ces bugs aurait été invisible lors des tests en locale anglaise et n'aurait été détecté qu'en production, sur des appareils configurés pour d'autres langues. Étant donné que l'application ciblait un public multilingue, l'impact aurait été significatif.
La portée du problème en production
L'une des raisons pour lesquelles cette classe de bugs persiste est qu'elle est invisible dans les environnements de développement à locale unique. Un développeur travaillant en locale anglaise, construisant une application pour un public international, peut écrire et tester toute la fonctionnalité d'affichage des pays sans jamais déclencher le bug. Le bug ne se manifeste que quand la locale de l'appareil ne correspond pas à la locale dans laquelle le dictionnaire ou le mapping a été initialement construit — ce qui est précisément la condition qui décrit la majorité des utilisateurs cibles.
Dans un widget spécifiquement, l'écart est plus large que dans une application principale parce que les widgets s'exécutent dans un contexte d'extension contraint. L'extension widget s'exécute avec la locale système actuelle, qui peut différer de la locale sur laquelle le développeur a testé. Il n'y a pas de débogage en cours de processus pour les rendus de widget — vous ne pouvez pas attacher un débogueur à une extension widget et inspecter l'état à l'exécution de la même façon qu'avec un processus d'application principale. Identifier les bugs de rendu dépendants de la locale dans les widgets nécessite le débogueur de timeline de Xcode et une attention particulière au contexte de rendu réel de l'extension, que la plupart des développeurs ne rencontrent qu'après qu'un utilisateur signale le problème en production.
La réponse pratique est de traiter toute logique d'affichage dépendante de la locale comme une catégorie de risque, pas seulement un bug individuel à corriger. La revue de code devrait inclure une vérification des chaînes dépendantes de la locale utilisées comme identifiants. La question de revue est simple : cette chaîne passe-t-elle par une méthode de localisation avant d'être utilisée comme clé ou valeur de comparaison ? Si oui, la conception doit changer. Les codes ISO, les identifiants numériques et autres références indépendantes de la locale doivent remplacer les chaînes localisées à chaque couche sauf l'affichage final.
Tests à travers les locales
La leçon la plus directe de ce bug est que les tests de locale doivent faire partie de la matrice de tests standard, pas une réflexion après coup. iOS rend cela relativement simple : le simulateur supporte le changement de locale sans changer la langue système, et les tests UI peuvent être paramétrés par locale.
Pour l'affichage des drapeaux et des pays spécifiquement, nous avons ajouté des tests de snapshot qui s'exécutent sur un ensemble fixe de locales d'appareils — anglais, allemand, français, japonais, arabe — et vérifient que l'emoji de drapeau pour un ensemble fixe de codes ISO reste correct dans toutes. Ces tests détectent immédiatement les régressions si quelqu'un introduit une recherche dépendante de la locale dans le chemin d'affichage.
Le principe plus large est que toute fonctionnalité touchant à la localisation, l'internationalisation ou l'affichage dépendant de la région doit avoir des tests automatisés exerçant plusieurs locales. La QA manuelle dans une seule locale n'est pas un substitut. La catégorie de bugs causés par des hypothèses dépendantes de la locale est à la fois courante et systématiquement invisible pour les tests à locale unique.
Ressources connexes
- Dynamic Island for Timer Apps: Implementation and Lessons — un autre contexte d'extension iOS où le comportement de la plateforme diffère de ce que les tests en application révèlent
- Closure Tables for Relationship Calculation — utiliser des identifiants stables (IDs plutôt que noms) pour construire une logique de recherche et traversal robuste