react-native-get-device-locale
v1.1.0
Published
react-native-get-device-locale is a lightweight and efficient React Native module that provides easy access to the device's locale.
Maintainers
Readme
react-native-get-device-locale
AppleLocale is undefined. I18nManager.localeIdentifier returns nothing. This module asks the OS directly instead.
If you landed here from that problem: NativeModules.SettingsManager.settings.AppleLocale has been returning undefined on iOS since iOS 13, and the SettingsManager / I18nManager.localeIdentifier approaches stopped working in React Native 0.76 and above. The two React Native core issues where that pain still lands are facebook/react-native#26540 and facebook/react-native#26606. The community answer became a fallback chain across three undocumented internals, with a hardcoded guess for when all three miss.
react-native-get-device-locale asks the OS instead. One native call, one string back (en_US, fr_FR), a TurboModule on the new architecture and a bridge module on the old one, and no runtime dependencies.
🔁 APIs this replaces
These are the calls that stopped being reliable:
NativeModules.SettingsManager.settings.AppleLocaleNativeModules.SettingsManager.settings.AppleLanguages[0]NativeModules.I18nManager.localeIdentifier
The widely copied workaround, and what it becomes here:
- import { NativeModules, Platform } from 'react-native';
-
- const locale =
- Platform.OS === 'ios'
- ? NativeModules.SettingsManager.settings.AppleLocale ||
- NativeModules.SettingsManager.settings.AppleLanguages[0]
- : NativeModules.I18nManager.localeIdentifier;
+ import { getDeviceLocale } from 'react-native-get-device-locale';
+
+ const locale = await getDeviceLocale();No platform branch, no reaching into NativeModules, and nothing that depends on an internal React Native module keeping its shape.
✨ Features
- Retrieve the device's locale in
en_US,fr_FRformat. - Or read it structured:
getDeviceLocaleInfo()returns the user's ranked locale list as BCP-47 tags, plus the language and region codes split out. - Compatible with React Native >= 0.68, the new architecture (TurboModules) and backward compatibility with old arch.
- Minimal setup with low resource usage.
- The call never rejects: a failed lookup resolves to the fallback locale you choose.
- TypeScript types shipped with the package. Zero runtime dependencies.
| Support | | | ----------- | -----------: | | react-native version | >=0.68 | | Android | ✅ | | iOS | ✅ | | New Architecture | ✅ | | Old Architecture | ✅ | | TypeScript types | ✅ | | Runtime dependencies | 0 |
Android minSdkVersion defaults to 21 and follows your app's rootProject.ext when it sets one. On iOS the podspec uses React Native's own min_ios_version_supported, so the minimum tracks your React Native version rather than being pinned here.
📦 Installation
npm install react-native-get-device-localeAdditional Steps (iOS only)
After installation, run the following command in your project's ios directory:
cd ios && pod installThen rebuild the app. Autolinking handles the rest on both platforms: the package ships its podspec and its react-native.config.js, so there is nothing to register by hand.
🧪 Expo
- There is no Expo config plugin in this package, and it does not need one. A plugin exists to patch native config at prebuild time, and this module adds nothing to patch: its
AndroidManifest.xmldeclares no permissions and it requires noInfo.plistentry. Autolinking is the whole integration. - It cannot run in Expo Go, because Expo Go ships a fixed set of native modules. The library's own linking error says so out loud.
- In an Expo project, use a development build:
npx expo prebuildthennpx expo run:ios/npx expo run:android, or EAS Build. From there it behaves like any other autolinked native module.
📖 Usage Example
Here's how to use react-native-get-device-locale to retrieve the device locale:
import { getDeviceLocale } from 'react-native-get-device-locale';
const deviceLocale = await getDeviceLocale("fr_FR"); // Optional default locale if fetching fails (en_US by default in case of error)And if you need more than the one string, for instance the ranked list of languages the user actually set, or the region on its own:
import { getDeviceLocaleInfo } from 'react-native-get-device-locale';
const { locales, languageCode, regionCode } = await getDeviceLocaleInfo();
// locales: ["en-FR", "fr-FR"] ranked as the OS ranks them, most-preferred first
// languageCode: "en"
// regionCode: "FR"API
getDeviceLocale
function getDeviceLocale(defaultLocale?: string): Promise<string>| | |
| ----------- | ----------- |
| defaultLocale | Optional. Returned if the lookup fails. Defaults to "en_US". |
| returns | A promise resolving to the locale as the platform reports it, for example en_US or fr_FR. |
It is async, and it never rejects. Any failure inside the call is caught, logged with console.error, and defaultLocale is resolved instead, so a locale lookup cannot take down the screen that awaited it.
If the module is not linked at all, the package raises a linking error naming the three usual causes: pods not installed, app not rebuilt, or running in Expo Go. On the old architecture that error travels the path above, so you see it in the console and get the fallback value. On the new architecture TurboModuleRegistry.getEnforcing raises as the package is imported, before any call of yours runs.
getDeviceLocaleInfo
Added in 1.1.0. It is additive: getDeviceLocale is unchanged, and nothing here replaces it.
type DeviceLocaleInfo = {
locales: Array<string>;
languageCode: string;
regionCode: string | null;
identifier: string;
};
function getDeviceLocaleInfo(defaultLocaleInfo?: DeviceLocaleInfo): Promise<DeviceLocaleInfo>| | |
| ----------- | ----------- |
| defaultLocaleInfo | Optional. Resolved if the lookup fails. Defaults to { locales: ["en-US"], languageCode: "en", regionCode: null, identifier: "en_US" }. That default region is null and not "US" on purpose: once the read has failed the region is genuinely unknown, and a confident wrong country costs a consumer a wrong dial code or currency, where null is something they can branch on. |
| returns | A promise resolving to a DeviceLocaleInfo, field by field below. |
| Field | |
| ----------- | ----------- |
| locales | Every locale the user has ranked in their OS language settings, most-preferred first, as BCP-47 tags: ["en-FR", "fr-FR"]. A single-language device gives you a one-entry array. Read from [NSLocale preferredLanguages] on iOS and LocaleList.getDefault() on Android. |
| languageCode | ISO 639-1, lowercase, from the active locale: "en". |
| regionCode | ISO 3166-1 alpha-2, uppercase: "FR", or null when the platform reports no region at all. This field does not mean quite the same thing on both platforms: on iOS it is the Region setting, which the user picks independently of their language, while Android has no such setting and the region comes from the chosen locale (with the SIM's country as a last resort). See the note below the table before you rely on it. |
| identifier | The raw platform identifier, unnormalized, exactly as the OS spells it: en_FR, or something like en_001@rg=frzzzz on an iPhone whose language and region disagree. Useful in logs and bug reports, not for comparisons. |
Like getDeviceLocale, it is async and never rejects: a failure is logged with console.error and defaultLocaleInfo is resolved instead.
Why regionCode is asymmetric. iOS has a Region setting genuinely separate from language, and this field is that setting: a phone set to Language English, Region France reports "FR", even though its identifier reads en_001@rg=frzzzz (Foundation resolves the @rg= subtag, so no string splitting of your own would have got there). Android has no equivalent setting, so the region is the country carried by the top locale, and only when that locale names no country at all (a device set to bare de) does it fall back to the SIM's country through TelephonyManager.getSimCountryIso(), which needs no permission. Because the SIM is a fallback and not a preference, an Android phone set to German with a French SIM reports "DE", not "FR". There is no SIM equivalent on iOS: CTCarrier was deprecated in iOS 16 and returns nil. If a wrong region would cost your users something, treat regionCode as a good default rather than a verified fact, and let them override it.
import { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
import { getDeviceLocale } from 'react-native-get-device-locale';
export default function App() {
const [locale, setLocale] = useState<string | null>(null);
useEffect(() => {
(async () => {
const deviceLocale = await getDeviceLocale();
setLocale(deviceLocale);
})();
}, []);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Device Locale: {locale}</Text>
</View>
);
}🔍 How it works
Each call is a native read per platform, resolved straight to the promise. Nothing is cached, computed or stored.
getDeviceLocale:
| Platform | What it reads | Source |
| ----------- | ----------- | ----------- |
| iOS | [[NSLocale currentLocale] localeIdentifier] | ios/GetDeviceLocale.mm |
| Android | java.util.Locale.getDefault().toString() | android/src/main/java/com/getdevicelocale/GetDeviceLocaleModuleImpl.kt |
getDeviceLocaleInfo:
| Platform | What it reads | Source |
| ----------- | ----------- | ----------- |
| iOS | [NSLocale preferredLanguages] for locales, then NSLocale.currentLocale for languageCode, countryCode and localeIdentifier | ios/GetDeviceLocale.mm |
| Android | LocaleList.getDefault(), each entry through toLanguageTag(), with the top entry's language and country; TelephonyManager.getSimCountryIso() only when that country is empty | android/src/main/java/com/getdevicelocale/GetDeviceLocaleModuleImpl.kt |
No permission is involved on either platform, which is why the package still declares none and still needs no Expo config plugin.
Both architectures are wired to the same reads, which is why the support table claims both:
- JS:
src/index.tsxuses the TurboModule spec (src/NativeGetDeviceLocale.ts) when the TurboModule proxy is present, andNativeModules.GetDeviceLocaleotherwise. - iOS:
GetDeviceLocale.hswitches onRCT_NEW_ARCH_ENABLED, conforming to the generatedNativeGetDeviceLocaleSpecon the new architecture and toRCTBridgeModuleon the old one. TheRCT_EXPORT_METHODimplementations serve both. - Android: the Gradle build compiles
src/newarchorsrc/oldarchdepending on your app'snewArchEnabled. Both are thin wrappers over the sameGetDeviceLocaleModuleImpl, so the two paths cannot drift.
⚖️ Compared to react-native-localize
react-native-localize is the mature, comprehensive option and the right answer for most localization work. It is a full toolbox: preferred locale list, country, currencies, calendar, timezone, number-format settings, temperature unit, 24-hour clock and metric-system flags, plus findBestLanguageTag for negotiating against the languages your app actually ships. It also covers more platforms (Android, iOS, macOS and web) and ships an Expo config plugin.
This library is not a smaller-and-worse version of that. It is a different scope choice: one function, one string.
| | react-native-get-device-locale | react-native-localize |
| ----------- | ----------- | ----------- |
| Scope | the device locale, nothing else | the full localization surface |
| Public API | getDeviceLocale() and getDeviceLocaleInfo() | a dozen-plus functions, plus a hook |
| Output | one identifier (en_US style), or the ranked locale list with its language and region codes | preferred-locale list with language tags, plus formats and settings |
| Platforms | iOS, Android | iOS, Android, macOS, web |
| Expo config plugin | not needed, none shipped | yes |
Pick react-native-localize if you need any of the rest of that surface, now or soon: timezone, currency, number formats, or proper language negotiation against your bundled translations. Depending on one well-maintained package beats bolting three small ones together.
Pick this one if the device locale string is genuinely all you need, and you would rather add a single zero-dependency TurboModule than a toolbox whose other functions you will never call.
🚧 Limitations
Deliberate for now, and worth knowing before you install:
- No format conversion. Two shapes are exposed and both come from the OS:
getDeviceLocalehands back the platform identifier as-is (en_US/fr_FR),getDeviceLocaleInfohands back BCP-47 tags plus the language and region codes split out. There is no formatter in between, so any other shape you need, you assemble from those. - The ranked list lives in one call.
getDeviceLocaleInfo().localesexposes every preferred language in the user's OS order;getDeviceLocalestill describes the single active locale and nothing else. Nothing filters or sorts that list against the languages your app ships, which is the No negotiation helper point below. regionCodeis not the same signal on both platforms. iOS reads a real Region setting, Android infers the region from the chosen locale and only consults the SIM when that locale names no country. The field documentation spells out what that costs you.- Locale only. No timezone, calendar, currency, number-format, temperature-unit or metric-system data. See the comparison above.
- No negotiation helper. Nothing matches the device locale against the languages your app ships. You compare the string yourself.
- iOS and Android only. Those are the two native folders in the package.
getDeviceLocalepasses the string through, not normalized. A locale carrying a script or a variant can therefore come back longer than two segments. Split on the first_if all you need is the language, or callgetDeviceLocaleInfoand readlanguageCode, which is already split for you.
🔄 Roadmap and Future Features
Both items that used to sit here shipped in 1.1.0, and are what getDeviceLocaleInfo returns:
- ✅ Alternate locale formats (e.g.,
enoren-US). - ✅ Getting all the device locales if multiple.
Nothing else is queued. The Limitations above are the scope, not a backlog: language negotiation, timezone, currency and number formats belong to a full localization toolbox, and react-native-localize (see the comparison above) is a better answer for them than a future version of this package would be. Ideas are still welcome as issues.
🏭 Used in production
This module ships in Animalert, a live lost-pet reporting platform available in several languages, on the App Store and Google Play. It resolves which language the interface opens in on first launch, before there is any stored user preference to read, which is exactly the moment the old AppleLocale chain used to fail.
🛠️ Contributing
Contributions are welcome! We follow the conventional commits guidelines. To contribute:
- Fork the repo.
- Clone it and create a new branch.
- Follow the commit message conventions.
- Open a Pull Request!
💡 Tip: Make sure your commit messages are in English for consistency!
See CONTRIBUTING.md for local setup, the two-architecture check, and what a useful bug report contains. Released versions are listed in CHANGELOG.md.
📞 Support
If you have questions or issues, feel free to open an issue on GitHub. I'll stay active to respond to queries and provide support.
Security reports have their own path: see SECURITY.md.
📄 License
MIT. See LICENSE.
