Localization
@symbiote-native/localization wraps
expo-localization — the device’s
locale list and preferred calendar settings — so every SymbioteNative adapter can reach it, not
just React. Unlike network’s single useNetworkState, this package
ships two independent getters, getLocales/getCalendars, each synchronous (a direct
JSI-bridged native call, no await) and each with its own native change listener and its own
reactive hook per adapter (useLocales, useCalendars) — mirroring
battery’s shape of several distinct hooks in one package rather than
one combined hook.
| OS platform | Support |
|---|---|
| iOS | ✅ live |
| Android | ✅ live |
| Framework adapter | Support |
|---|---|
| React | ✅ live |
| Vue | ✅ live |
| Angular | ✅ live |
| Svelte | ✅ live |
| Solid | ✅ live |
Installation
Section titled “Installation”npm install @symbiote-native/localizationScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --localization (or
add --localization in an existing app) installs and wires this for you — see
@symbiote-native/cli.
expo-localization and expo-modules-core come along as regular dependencies, pinned to exact
versions — never install either yourself, and never add the expo meta-package to your project
(it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).
No platform permission string is needed — locale and calendar settings are read-only system state with no runtime permission prompt on either platform.
import { useLocales, useCalendars } from '@symbiote-native/localization/react';
export default function LocalizationScreen() { const locales = useLocales(); // Locale[], guaranteed at least 1 element const calendars = useCalendars(); // Calendar[], guaranteed at least 1 element
return ( <> <text>{locales[0].languageTag}</text> <text>{calendars[0].timeZone}</text> </> );}<script setup lang="ts">import { useLocales, useCalendars } from '@symbiote-native/localization/vue';
const locales = useLocales(); // Ref<Locale[]>const calendars = useCalendars(); // Ref<Calendar[]></script>
<template> <text>{{ locales[0].languageTag }}</text> <text>{{ calendars[0].timeZone }}</text></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { LocalesService, CalendarsService } from '@symbiote-native/localization/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` <text>{{ locales()[0].languageTag }}</text> <text>{{ calendars()[0].timeZone }}</text> `,})export class LocalizationScreen { readonly locales = inject(LocalesService).connect(); // Signal<Locale[]> readonly calendars = inject(CalendarsService).connect(); // Signal<Calendar[]>}<script lang="ts"> import { useLocales, useCalendars } from '@symbiote-native/localization/svelte';
const locales = useLocales(); // { readonly current: Locale[] } const calendars = useCalendars(); // { readonly current: Calendar[] }</script>
<text>{locales.current[0].languageTag}</text><text>{calendars.current[0].timeZone}</text>Both runes return a boxed getter object rather than a bare value — Svelte 5 reactivity does
not survive being returned as a raw value from a plain function, so read .current exactly
like unwrapping Vue’s Ref via .value.
import { createLocales, createCalendars } from '@symbiote-native/localization/solid';
export default function LocalizationScreen() { const locales = createLocales(); // Accessor<Locale[]> const calendars = createCalendars(); // Accessor<Calendar[]>
return ( <> <text>{locales()[0].languageTag}</text> <text>{calendars()[0].timeZone}</text> </> );}Both primitives return an Accessor, not a plain value: call them, locales(), like any
other Solid signal, since a component body runs once.
Functions
Section titled “Functions”| Signature | Description |
|---|---|
getLocales(): Locale[] |
List of the user’s locales, in the order the user defines in their device settings. Guaranteed to contain at least 1 element |
getCalendars(): Calendar[] |
List of the user’s preferred calendars. For now always returns a single element, but may return a user preference list on some platforms in the future. Guaranteed to contain at least 1 element |
addLocaleListener(listener: (event?: unknown) => void): EventSubscription |
Subscribes to locale-settings-change notifications — the listener carries no payload, it’s an invalidate signal for useLocales to re-read getLocales() |
addCalendarListener(listener: (event?: unknown) => void): EventSubscription |
Subscribes to calendar-settings-change notifications — same invalidate-signal shape as addLocaleListener, for useCalendars |
useLocales() / useCalendars() config
Section titled “useLocales() / useCalendars() config”Both hooks take no arguments — each seeds and subscribes automatically.
useLocales() return value
Section titled “useLocales() return value”| Field | Type | Description |
|---|---|---|
| (return value) | Locale[] |
React returns this as a plain array, recomputed via useMemo whenever the locale-change listener fires. Vue returns Ref<Locale[]>. Angular’s LocalesService.connect() returns Signal<Locale[]>. Svelte returns { readonly current: Locale[] }, a boxed getter read as .current |
useCalendars() return value
Section titled “useCalendars() return value”| Field | Type | Description |
|---|---|---|
| (return value) | Calendar[] |
React returns this as a plain array, recomputed via useMemo whenever the calendar-change listener fires. Vue returns Ref<Calendar[]>. Angular’s CalendarsService.connect() returns Signal<Calendar[]>. Svelte returns { readonly current: Calendar[] }, a boxed getter read as .current |
Locale
Section titled “Locale”| Field | Type | Description |
|---|---|---|
languageTag |
string |
An IETF BCP 47 language tag with a region code, e.g. 'en-US', 'es-419', 'pl-PL' |
languageCode |
string | null |
An IETF BCP 47 language tag without the region code, e.g. 'en', 'es', 'pl' |
languageScriptCode |
string | null |
An ISO 15924 4-letter script code, e.g. 'Latn', 'Hans', 'Hebr'. May be null on Android and web |
regionCode |
string | null |
The device’s region, from the Region setting under Language & Region on iOS, Region settings on Android, and parsed from locale on web (can be null there), e.g. 'US' |
languageRegionCode |
string | null |
The region code for the preferred language — the same value as regionCode for a non-region-specific language, or the language’s own region for a region-specific one ('en-CA' → 'CA'). Prefer regionCode for internationalization purposes |
currencyCode |
string | null |
Currency code for the locale, e.g. 'USD', 'EUR', 'PLN'. On iOS this is the Region setting’s currency, not necessarily the current locale’s; on Android it’s specific to the locale in the list; null on web (look up by region instead) |
currencySymbol |
string | null |
Currency symbol for the currency in currencyCode, e.g. '$', '€', 'zł' |
languageCurrencyCode |
string | null |
Currency code for the current locale in the list rather than the device region (iOS), or equal to currencyCode (Android); null on web. Prefer currencyCode for internationalization purposes |
languageCurrencySymbol |
string | null |
Currency symbol for the currency in languageCurrencyCode. Prefer currencySymbol for internationalization purposes |
decimalSeparator |
string | null |
Decimal separator used for formatting numbers with fractional parts, e.g. '.', ',' |
digitGroupingSeparator |
string | null |
Digit grouping separator used for formatting large numbers, e.g. '.', ',' |
textDirection |
'ltr' | 'rtl' |
Text direction for the locale |
measurementSystem |
'metric' | 'us' | 'uk' | null |
The measurement system used in the locale. null on web, since the user’s chosen measurement system isn’t exposed there and inferring it from locale is unreliable — ask for user preference if possible |
temperatureUnit |
'celsius' | 'fahrenheit' | null |
The temperature unit used in the locale. null if the region code is unknown |
Calendar
Section titled “Calendar”| Field | Type | Description |
|---|---|---|
calendar |
CalendarIdentifier | null |
The calendar identifier, one of the Unicode calendar types. Limited to Android’s own available calendar types there; iOS maps its own identifiers to the closest Unicode type and never reports 'dangi' or 'islamic-rgsa' (not implemented on iOS) |
uses24hourClock |
boolean | null |
true when the current device settings use 24-hour time format. Can be null on browsers that don’t support the hourCycle property in Intl |
firstWeekday |
Weekday | null |
The first day of the week — for most calendars Sunday is 1 and Saturday is 7. Can be null on browsers that don’t support the weekInfo property in Intl |
timeZone |
string | null |
Time zone for the calendar, e.g. 'America/Los_Angeles', 'Europe/Warsaw', 'GMT+1'. Can be null on web |
Weekday
Section titled “Weekday”| Member | Value | Description |
|---|---|---|
SUNDAY |
1 |
Sunday |
MONDAY |
2 |
Monday |
TUESDAY |
3 |
Tuesday |
WEDNESDAY |
4 |
Wednesday |
THURSDAY |
5 |
Thursday |
FRIDAY |
6 |
Friday |
SATURDAY |
7 |
Saturday |
Maps days of the week in the Gregorian calendar to their index as returned by Calendar.firstWeekday.
CalendarIdentifier
Section titled “CalendarIdentifier”| Member | Value | Description |
|---|---|---|
BUDDHIST |
'buddhist' |
Thai Buddhist calendar |
CHINESE |
'chinese' |
Traditional Chinese calendar |
COPTIC |
'coptic' |
Coptic calendar |
DANGI |
'dangi' |
Traditional Korean calendar |
ETHIOAA |
'ethioaa' |
Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E) |
ETHIOPIC |
'ethiopic' |
Ethiopic calendar, Amete Mihret (epoch approx. 8 C.E.) |
GREGORY |
'gregory' |
Gregorian calendar |
HEBREW |
'hebrew' |
Traditional Hebrew calendar |
INDIAN |
'indian' |
Indian calendar |
ISLAMIC |
'islamic' |
Islamic calendar |
ISLAMIC_CIVIL |
'islamic-civil' |
Islamic calendar, tabular (intercalary years 2,5,7,10,13,16,18,21,24,26,29 — civil epoch) |
ISLAMIC_RGSA |
'islamic-rgsa' |
Islamic calendar, Saudi Arabia sighting |
ISLAMIC_TBLA |
'islamic-tbla' |
Islamic calendar, tabular (intercalary years 2,5,7,10,13,16,18,21,24,26,29 — astronomical epoch) |
ISLAMIC_UMALQURA |
'islamic-umalqura' |
Islamic calendar, Umm al-Qura |
ISO8601 |
'iso8601' |
ISO calendar (Gregorian calendar using the ISO 8601 calendar week rules) |
JAPANESE |
'japanese' |
Japanese imperial calendar |
PERSIAN |
'persian' |
Persian calendar |
ROC |
'roc' |
Civil (algorithmic) Arabic calendar |
Common questions
Section titled “Common questions”- The locale changed in Settings but my app shows the old one. On Android the user can change
it without restarting the app; read the locales again when the app returns to the foreground
(
AppState). On iOS the result stays the same while the app runs. - Does
getLocales()ever come back empty? No, it holds at least one entry, in the order the user set in system settings. - Do I need an in-app language picker? Often not: recent iOS and Android let users set a language per app. Upstream wires that through an Expo config plugin option, which is outside this project.
Sources: Expo docs: Localization, Expo guide: localization, React Native localization with Expo.
How the wrapper works
Section titled “How the wrapper works”@symbiote-native/localization ships zero React/Vue/Angular/Svelte/Solid logic in expo-localization itself
— its types and functions are hand-ported, verbatim, into this package’s own core/, resolving
the native module through expo-modules-core’s requireNativeModule rather than the expo
meta-package this project never installs. One native module fans out two independent event
streams (locale settings vs. calendar settings) through the same addListener, keyed by event
name — the same trick battery’s native module uses for its own
multiple event names on one addListener:
packages/localization/src/├── core/ types.ts — Locale, Weekday, CalendarIdentifier, Calendar, hand-ported│ verbatim from Localization.types.ts. native-module.ts resolves the│ native module through expo-modules-core's requireNativeModule and│ exposes addLocaleListener/addCalendarListener. localization.ts —│ getLocales/getCalendars, synchronous getters delegating straight to│ the native module.├── react/hooks/ @symbiote-native/localization/react — useLocales, useCalendars├── vue/composables/ @symbiote-native/localization/vue — same two names, Vue lifecycle├── svelte/runes/ @symbiote-native/localization/svelte — same two names, Svelte lifecycle└── angular/services/ @symbiote-native/localization/angular — LocalesService, CalendarsService (`.connect()` returns a Signal)Two independent getters, each with its own native change listener and its own reactive hook per
adapter — mirroring @symbiote-native/battery’s shape of shipping several distinct hooks in one
package, not one combined hook. Each hook/composable/rune/service seeds its return value from the
matching synchronous get*() call (no initial “loading” state needed — the native call is sync,
not async) and recomputes it whenever the matching listener fires. The native code itself is
never vendored or copied — expo-modules-autolinking resolves it straight out of node_modules
(see the native setup guide).