tva
← Analyses

Drapeaux dans les widgets : pourquoi préférer les codes pays ISO aux noms localisés

Le bug était subtil. Un widget iOS affichait le nom de pays correct en anglais mais montrait le mauvais emoji de drapeau en allemand. Avec les paramètres régionaux français, un autre pays affichait le mauvais drapeau. Les drapeaux n’étaient pas aléatoires — ils étaient systématiquement incorrects selon un schéma 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 profonde é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.

Avec les paramètres régionaux anglais, cela fonctionnait correctement. Le nom stocké était « Germany », la clé du dictionnaire était « Germany », et la recherche retournait le bon drapeau. Mais lorsque les paramètres régionaux de l’appareil étaient réglés sur l’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, utilisait une valeur de repli, et affichait le mauvais drapeau.

La variante la plus insidieuse concernait 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 d’assurance qualité exécuté uniquement en locale anglaise ne l’aurait jamais détectée.

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 quels que soient la personne qui les lit, le moment ou le 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 des paramètres régionaux, 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 par programmation entre différents paramètres régionaux.

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 correspondance, et aucune maintenance. La correspondance est définie 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. Les codes tels que UN et EU, qui sont réservés dans ISO 3166-1 plutôt qu’attribués à des pays, produisent des emojis de drapeaux que toutes les plateformes ne rendent pas de manière cohérente. La fonction ci-dessus ne valide pas non plus les entrées : elle convertit tous les caractères qu’elle reçoit. Il faut donc vérifier les entrées par rapport à la liste des codes ISO 3166-1 alpha-2 attribués (exactement deux lettres de A à Z) avant la conversion. Restreindre les entrées aux codes attribués aux pays et territoires et tester sur la plateforme cible permet de couvrir ce cas.

La table de correspondance 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 la partie 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 une table de correspondance appropriée.

La table de correspondance que nous avons construite 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 cette table de correspondance 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 de données parle en codes ISO. La couche de 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 schéma. Le même problème est apparu à trois autres endroits : un module d’affichage de devises utilisant des noms de devises localisés comme clés de cache, un sélecteur de langue stockant des noms de langues localisés 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 schéma. 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, balises 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 relèvent alors exclusivement de la couche de présentation.

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 les paramètres régionaux de l’appareil ne correspondent pas à la locale dans laquelle le dictionnaire ou la table de correspondance ont été initialement construits — 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é. Le débogage est également moins direct que dans l’application principale : vous exécutez le schéma de l’extension widget dans Xcode (ou utilisez Debug > Attach to Process), et le code de la chronologie et de la vue s’exécute selon le calendrier de WidgetKit plutôt qu’en réponse à vos propres interactions. Identifier les bugs de rendu dépendants de la locale dans les widgets nécessite d’exécuter l’extension avec les locales pertinentes, d’utiliser les aperçus Xcode avec des entrées de chronologie d’exemple et de prêter une attention particulière au contexte de rendu réel de l’extension — une étape facile à omettre jusqu’à ce 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 avec différents paramètres régionaux

La leçon la plus directe de ce bug est que les tests de locale doivent faire partie de la matrice de tests standard, plutôt que d’être ajoutés après coup. iOS rend cela relativement simple : le simulateur prend en charge 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 par instantané 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. L’assurance qualité 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.

Analyses connexes

Pour aller plus loin