Récupérer la locale de l'appareil en React Native
AppleLocale renvoie undefined depuis iOS 13, et le fallback en cascade copié partout masque le bug. react-native-get-device-locale demande la locale à l'OS.

AppleLocale renvoie undefined depuis iOS 13, et le fallback en cascade copié partout masque le bug. react-native-get-device-locale demande la locale à l'OS.

Animalert existe en plusieurs langues. La première fois que quelqu'un ouvre l'app, il n'y a aucune préférence enregistrée à lire, pas de compte, pas de réglage. L'app a une question à trancher avant d'afficher quoi que ce soit : quelle langue cette personne utilise-t-elle vraiment ?
Pendant des années, la réponse tenait en une ligne de JavaScript. Puis la ligne a cessé de fonctionner, et le contournement adopté par tout l'écosystème s'est révélé pire que le bug d'origine.
Si vous avez appris la localisation React Native dans un tuto ou une réponse Stack Overflow, vous avez appris ça : lire NativeModules.SettingsManager.settings.AppleLocale sur iOS, et NativeModules.I18nManager.localeIdentifier sur Android.
Ça n'a jamais été une API publique documentée. C'était un blob de settings exposé par un module natif, et lire une valeur dedans marchait, presque par accident.
En septembre 2019, le champ a simplement cessé d'exister. Deux issues se sont ouvertes sur le dépôt de React Native à trois jours d'intervalle : AppleLocale ne fonctionne plus sur iOS 13 et le champ AppleLocale est absent des settings sur iOS 13.
Les deux sont fermées depuis des années. Les deux sont toujours ce qu'on trouve en cherchant le problème aujourd'hui, ce qui en dit long sur le nombre d'apps qui en ont discrètement hérité.
Le contournement qui a circulé est une cascade : essayer AppleLocale, et s'il est undefined se rabattre sur la première entrée de AppleLanguages, et si ça échoue aussi retomber sur un anglais hardcodé.
Ça a l'air défensif. En pratique, ça fait trois sources de vérité qui renvoient trois formats différents : AppleLocale donne en_US, la liste des langues donne en-US, et la valeur par défaut donne en. Ajoutez la branche Android et une seule app parse des locales de quatre façons.
Le vrai coût n'est pas le parsing. Une cascade comme celle-là n'échoue jamais bruyamment. Elle renvoie une réponse fausse mais plausible : une app qui devait s'ouvrir en français s'ouvre en anglais, personne n'ouvre de ticket, les gens s'en vont.
Une résolution de langue construite sur un fallback en cascade continue de tourner longtemps après être devenue fausse. Testez-la en réglant vraiment un appareil dans une langue dans laquelle vous ne développez pas, parce qu'aucune erreur ne viendra vous prévenir.
React Native a activé la New Architecture par défaut en 0.76, où les TurboModules remplacent entièrement l'ancien système NativeModules, et l'ancien bridge a été désactivé purement et simplement en 0.82.
Rien de tout ça n'a cassé la lecture de la locale volontairement. Ça a seulement retiré les dernières raisons de croire qu'un objet de settings legacy était un point d'appui fiable.
Le correctif n'a rien de spectaculaire. Plutôt que de lire un blob de settings qui contient une locale par hasard, on demande la locale directement à la plateforme, par l'API dont c'est le job.
Sur iOS, c'est NSLocale.currentLocale.localeIdentifier : une ligne, toujours présente, sans condition de version. Android a son équivalent exact. Wrapper les deux dans un TurboModule donne une seule fonction async qui renvoie une string au format en_US, avec un fallback facultatif si la résolution échoue.
C'est tout ce que fait react-native-get-device-locale : npm install react-native-get-device-locale, une méthode, zéro dépendance, et ça tourne sur la New Architecture comme sur l'ancienne parce que le côté natif compile les deux chemins.
Dans Animalert, react-native-get-device-locale décide de la langue de l'interface au premier lancement, avant qu'aucune préférence n'existe. Il y a un second usage moins évident : la locale résolue est écrite dans une clé de préférences partagées que lisent les widgets d'écran d'accueil iOS et Android, pour que le widget posé sur l'écran d'accueil parle la même langue que l'app qui l'a mis là.
Ce détail plaide pour garder la résolution de langue dans un seul endroit, petit et prévisible. Quand quatre chemins de code devinent chacun la langue, le widget est l'endroit où le désaccord devient visible.
Un seul format, en_US, et une seule valeur. Il ne renvoie pas toutes les langues préférées dans l'ordre, et il ne sait rien du fuseau horaire, du calendrier, de la devise ni du formatage des nombres.
Si la locale de l'appareil est tout ce que vous cherchez, react-native-get-device-locale est le bon choix dans la grande majorité des cas : une méthode, zéro dépendance, rien à configurer, et vous n'embarquez pas une lib de localisation entière pour lire une string.
Si vous avez besoin du reste, react-native-localize est la lib mature et très utilisée pour ce job, et c'est la recommandation honnête. Le choix porte sur le périmètre, pas sur la qualité.
La leçon ne tient pas à un nom de champ en particulier : la plateforme expose presque toujours ce que vous cherchez à travers une API faite pour ça, et le raccourci qui va le lire dans un objet voisin bien pratique est celui qui casse deux versions majeures plus tard, sans rien dire.
Le code est sur GitHub, les issues et les PR sont ouvertes, et le README détaille les deux chemins natifs et les APIs que ce module remplace.
react-native-get-device-locale appelle directement l'API de locale de la plateforme et renvoie une string comme en_US ou fr_FR, avec zéro dépendance et le support de la New Architecture comme de l'ancienne. Pour le fuseau horaire, la devise, le formatage des nombres ou la négociation des langues, react-native-localize va bien plus loin.
Le champ a cessé d'être présent à partir d'iOS 13, et ce n'était de toute façon pas une API publique documentée. Tout code qui le lit devrait passer par une API de locale de la plateforme au lieu d'ajouter un fallback.
Par un module natif qui appelle l'API de locale de la plateforme, NSLocale.currentLocale.localeIdentifier sur iOS et son équivalent sur Android, plutôt qu'en lisant une valeur dans l'ancien objet de settings.
Oui, à condition que le module soit un TurboModule. L'ancienne approche reposait sur le système NativeModules legacy que la New Architecture remplace, ce qui fait une deuxième raison de s'en éloigner.
react-native-get-device-locale renvoie la forme avec tiret bas, par exemple en_US ou fr_FR. Ça compte plus qu'il n'y paraît, parce que les différentes sources de fallback renvoient aussi des formes avec trait d'union et des formes courtes, et qu'un code qui suppose une seule forme casse sur les autres.