@symbiote-native/localization 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/build/angular/index.d.ts +3 -0
  4. package/build/angular/index.js +7 -0
  5. package/build/angular/services/calendars.service/index.d.ts +6 -0
  6. package/build/angular/services/calendars.service/index.js +68 -0
  7. package/build/angular/services/locales.service/index.d.ts +6 -0
  8. package/build/angular/services/locales.service/index.js +69 -0
  9. package/build/core/index.d.ts +4 -0
  10. package/build/core/index.js +3 -0
  11. package/build/core/localization.d.ts +39 -0
  12. package/build/core/localization.js +42 -0
  13. package/build/core/native-module.d.ts +12 -0
  14. package/build/core/native-module.js +21 -0
  15. package/build/core/types.d.ts +174 -0
  16. package/build/core/types.js +59 -0
  17. package/build/react/hooks/use-calendars/index.d.ts +2 -0
  18. package/build/react/hooks/use-calendars/index.js +13 -0
  19. package/build/react/hooks/use-locales/index.d.ts +2 -0
  20. package/build/react/hooks/use-locales/index.js +15 -0
  21. package/build/react/index.d.ts +3 -0
  22. package/build/react/index.js +7 -0
  23. package/build/vue/composables/use-calendars/index.d.ts +3 -0
  24. package/build/vue/composables/use-calendars/index.js +16 -0
  25. package/build/vue/composables/use-locales/index.d.ts +3 -0
  26. package/build/vue/composables/use-locales/index.js +20 -0
  27. package/build/vue/index.d.ts +3 -0
  28. package/build/vue/index.js +7 -0
  29. package/build-ngc/angular/index.d.ts +3 -0
  30. package/build-ngc/angular/index.js +8 -0
  31. package/build-ngc/angular/index.js.map +1 -0
  32. package/build-ngc/angular/services/calendars.service/index.d.ts +9 -0
  33. package/build-ngc/angular/services/calendars.service/index.js +27 -0
  34. package/build-ngc/angular/services/calendars.service/index.js.map +1 -0
  35. package/build-ngc/angular/services/locales.service/index.d.ts +9 -0
  36. package/build-ngc/angular/services/locales.service/index.js +28 -0
  37. package/build-ngc/angular/services/locales.service/index.js.map +1 -0
  38. package/build-ngc/core/index.d.ts +4 -0
  39. package/build-ngc/core/index.js +4 -0
  40. package/build-ngc/core/index.js.map +1 -0
  41. package/build-ngc/core/localization.d.ts +39 -0
  42. package/build-ngc/core/localization.js +43 -0
  43. package/build-ngc/core/localization.js.map +1 -0
  44. package/build-ngc/core/native-module.d.ts +12 -0
  45. package/build-ngc/core/native-module.js +22 -0
  46. package/build-ngc/core/native-module.js.map +1 -0
  47. package/build-ngc/core/types.d.ts +174 -0
  48. package/build-ngc/core/types.js +60 -0
  49. package/build-ngc/core/types.js.map +1 -0
  50. package/native-link.json +12 -0
  51. package/package.json +107 -0
  52. package/src/angular/index.ts +17 -0
  53. package/src/angular/services/calendars.service/calendars.service.test.ts +102 -0
  54. package/src/angular/services/calendars.service/index.ts +26 -0
  55. package/src/angular/services/locales.service/index.ts +27 -0
  56. package/src/angular/services/locales.service/locales.service.test.ts +122 -0
  57. package/src/core/index.ts +4 -0
  58. package/src/core/localization.test.ts +64 -0
  59. package/src/core/localization.ts +45 -0
  60. package/src/core/native-module.ts +43 -0
  61. package/src/core/types.ts +181 -0
  62. package/src/react/hooks/use-calendars/index.ts +16 -0
  63. package/src/react/hooks/use-calendars/use-calendars.test.tsx +87 -0
  64. package/src/react/hooks/use-locales/index.ts +18 -0
  65. package/src/react/hooks/use-locales/use-locales.test.tsx +107 -0
  66. package/src/react/index.ts +18 -0
  67. package/src/vue/composables/use-calendars/index.ts +25 -0
  68. package/src/vue/composables/use-calendars/use-calendars.test.ts +96 -0
  69. package/src/vue/composables/use-locales/index.ts +24 -0
  70. package/src/vue/composables/use-locales/use-locales.test.ts +116 -0
  71. package/src/vue/index.ts +18 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # @symbiote-native/localization
2
+
3
+ Port of [`expo-localization`](https://docs.expo.dev/versions/latest/sdk/localization/) for
4
+ [SymbioteNative](../../README.md) — the device's locale list and preferred calendar settings,
5
+ reachable from every adapter (React, Vue, Angular), not just React.
6
+
7
+ Built the same way as [`@symbiote-native/battery`](../battery) and
8
+ [`@symbiote-native/device`](../device), an `expo-modules-core`-based wrapper (see the
9
+ `symbiote-expo-native-module` project skill for the full mechanism: why `expo-modules-core` is
10
+ depended on directly and never the `expo` meta-package, why the upstream JS is hand-ported into
11
+ `core/` rather than imported, and how autolinking picks up the native module).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @symbiote-native/localization
17
+ ```
18
+
19
+ Depends on `expo-localization` and `expo-modules-core` directly (regular dependencies, pinned to
20
+ exact versions — never a caret range, since this package's `core/` is hand-ported against one
21
+ specific native API shape and a newer resolve could silently drift the two apart). Never install
22
+ `expo-localization` yourself, and never add the `expo` package to this project — it bundles its
23
+ own Metro/Babel pipeline that conflicts with this project's own.
24
+
25
+ ### Required one-time step: native autolinking wiring
26
+
27
+ Unlike a plain RN native module, `expo-localization`'s native code is discovered by
28
+ `expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism — this needs wiring
29
+ into the native host app **once**, covering this package and every other `expo-modules-core`
30
+ package (`@symbiote-native/battery`, `@symbiote-native/device`, ...) with zero further changes:
31
+
32
+ | Platform | Touches |
33
+ |---|---|
34
+ | iOS | `ios/Podfile` — add `use_expo_modules!` |
35
+ | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
36
+ | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
37
+ | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
38
+
39
+ Full mechanics — the Podfile pieces that normally ship inside the `expo` package, the `expo`
40
+ peer-dependency exclusion list — live in the `symbiote-expo-native-module` skill. Reference
41
+ implementation: `.examples/expo-react/ios/Podfile` and
42
+ `.examples/expo-react/android/app/src/main/java/com/canary/MainApplication.kt`.
43
+
44
+ No platform permission string is needed — locale and calendar settings are read-only system
45
+ state with no runtime permission prompt on either platform.
46
+
47
+ ## Shape
48
+
49
+ ```
50
+ src/core/ types.ts — Locale, Weekday, CalendarIdentifier, Calendar, hand-ported
51
+ verbatim from Localization.types.ts. native-module.ts resolves the
52
+ native module through expo-modules-core's requireNativeModule and
53
+ exposes addLocaleListener/addCalendarListener. localization.ts —
54
+ getLocales/getCalendars, synchronous getters delegating straight to the
55
+ native module.
56
+ src/react/hooks/ @symbiote-native/localization/react — useLocales, useCalendars
57
+ src/vue/composables/ @symbiote-native/localization/vue — same two names, Vue lifecycle
58
+ src/angular/services/ @symbiote-native/localization/angular — LocalesService, CalendarsService
59
+ (`.connect()` returns a Signal)
60
+ ```
61
+
62
+ Two independent getters, each with its own native change listener and its own reactive hook per
63
+ adapter — mirroring `@symbiote-native/battery`'s shape of shipping several distinct hooks in one
64
+ package, not one combined hook. Each hook/composable/service seeds its return value from the
65
+ matching synchronous `get*()` call (no initial "loading" state needed — the native call is
66
+ sync, not async) and recomputes it whenever the matching listener fires.
67
+
68
+ ## Use it
69
+
70
+ ```tsx
71
+ // React — examples/expo-react/screens/LocalizationScreen.tsx
72
+ import { useLocales, useCalendars } from '@symbiote-native/localization/react';
73
+
74
+ function LocalizationScreen() {
75
+ const locales = useLocales(); // Locale[], guaranteed at least 1 element
76
+ const calendars = useCalendars(); // Calendar[], guaranteed at least 1 element
77
+
78
+ return (
79
+ <>
80
+ <Text>{locales[0].languageTag}</Text>
81
+ <Text>{calendars[0].timeZone}</Text>
82
+ </>
83
+ );
84
+ }
85
+ ```
86
+
87
+ ```vue
88
+ <!-- Vue — examples/expo-vue-sfc/screens/LocalizationScreen.vue -->
89
+ <script setup lang="ts">
90
+ import { useLocales, useCalendars } from '@symbiote-native/localization/vue';
91
+
92
+ const locales = useLocales(); // Ref<Locale[]>
93
+ const calendars = useCalendars(); // Ref<Calendar[]>
94
+ </script>
95
+ <template>
96
+ <Text>{{ locales[0].languageTag }}</Text>
97
+ <Text>{{ calendars[0].timeZone }}</Text>
98
+ </template>
99
+ ```
100
+
101
+ ```ts
102
+ // Angular — examples/expo-angular/src/screens/LocalizationScreen.ts
103
+ import { Component, inject } from '@angular/core';
104
+ import { LocalesService, CalendarsService } from '@symbiote-native/localization/angular';
105
+
106
+ @Component({ /* ... */ })
107
+ export class LocalizationScreen {
108
+ readonly locales = inject(LocalesService).connect(); // Signal<Locale[]>
109
+ readonly calendars = inject(CalendarsService).connect(); // Signal<Calendar[]>
110
+ }
111
+ ```
112
+
113
+ ## API
114
+
115
+ Two independent synchronous getters (`getLocales`, `getCalendars`), each with its own
116
+ listener-based subscription (`addLocaleListener`/`addCalendarListener`) and its own adapter-level
117
+ lifecycle hook — one hook/composable/service per getter, matching upstream's own `useLocales`/
118
+ `useCalendars` being two separate hooks, not one combined hook.
119
+
120
+ ```ts
121
+ getLocales(): Locale[] // user's locales, in device-settings order
122
+ getCalendars(): Calendar[] // user's preferred calendars
123
+ addLocaleListener(listener): EventSubscription // fires when locale settings change
124
+ addCalendarListener(listener): EventSubscription // fires when calendar settings change
125
+ ```
126
+
127
+ Plus `Locale`, `Weekday` (enum), `CalendarIdentifier` (enum), `Calendar` — ported from upstream's
128
+ `Localization.types.ts`.
129
+
130
+ ```ts
131
+ import { getLocales, getCalendars } from '@symbiote-native/localization';
132
+
133
+ // framework-scoped entry points re-export the same free functions, plus a lifecycle
134
+ // hook/composable/service per getter:
135
+ import { useLocales, useCalendars } from '@symbiote-native/localization/react';
136
+ import { useLocales, useCalendars } from '@symbiote-native/localization/vue';
137
+ import { LocalesService, CalendarsService } from '@symbiote-native/localization/angular';
138
+ ```
139
+
140
+ Each hook/composable/service seeds its initial value from the matching synchronous `get*()`
141
+ call, then subscribes to the matching listener for updates, and unsubscribes on unmount —
142
+ mirroring upstream's own `useLocales`/`useCalendars`.
143
+
144
+ ## Test it
145
+
146
+ No Fabric/Descriptor angle at all — localization is a pure synchronous-function + `EventEmitter`
147
+ listener surface, never a view. Tests inject a fake native-module object in place of the real
148
+ `requireNativeModule` resolution (`src/core/localization.test.ts`,
149
+ `src/{react,vue,angular}/**/*.test.{ts,tsx}`, `vitest`), the same pattern
150
+ `@symbiote-native/battery` and `@symbiote-native/device` use — no `installFabric()`, no
151
+ ViewConfig for the core test; the adapter hook/composable/service tests do use `installFabric()`
152
+ purely to mount a host component, same as every other sibling package. Native rendering itself is
153
+ verified on-device — see the parent [README](../../README.md).
@@ -0,0 +1,3 @@
1
+ export { LocalesService } from './services/locales.service';
2
+ export { CalendarsService } from './services/calendars.service';
3
+ export { getLocales, getCalendars, addLocaleListener, addCalendarListener, Weekday, CalendarIdentifier, type Locale, type Calendar, type EventSubscription, } from '../core';
@@ -0,0 +1,7 @@
1
+ // LocalesService/CalendarsService are the Angular-only lifecycle half; the free functions and
2
+ // event subscription plumbing all live in core, shared with React/Vue. Two separate services
3
+ // (not one combined service), matching upstream's own useLocales/useCalendars being two separate
4
+ // hooks.
5
+ export { LocalesService } from './services/locales.service';
6
+ export { CalendarsService } from './services/calendars.service';
7
+ export { getLocales, getCalendars, addLocaleListener, addCalendarListener, Weekday, CalendarIdentifier, } from '../core';
@@ -0,0 +1,6 @@
1
+ import { type Signal } from '@angular/core';
2
+ import { type Calendar } from '../../../core';
3
+ export declare class CalendarsService {
4
+ private readonly injector;
5
+ connect(): Signal<Calendar[]>;
6
+ }
@@ -0,0 +1,68 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { effect, inject, Injectable, Injector, signal } from '@angular/core';
36
+ import { addCalendarListener, getCalendars } from '../../../core';
37
+ // Angular twin of React's `useCalendars` hook and Vue's `useCalendars` composable — mirrors
38
+ // LocalesService's shape exactly.
39
+ //
40
+ // readonly calendars = inject(CalendarsService).connect();
41
+ // // template: {{ calendars() }}
42
+ let CalendarsService = (() => {
43
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
44
+ let _classDescriptor;
45
+ let _classExtraInitializers = [];
46
+ let _classThis;
47
+ var CalendarsService = class {
48
+ static { _classThis = this; }
49
+ static {
50
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
51
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
52
+ CalendarsService = _classThis = _classDescriptor.value;
53
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
54
+ __runInitializers(_classThis, _classExtraInitializers);
55
+ }
56
+ injector = inject(Injector);
57
+ connect() {
58
+ const calendars = signal(getCalendars());
59
+ effect(onCleanup => {
60
+ const subscription = addCalendarListener(() => calendars.set(getCalendars()));
61
+ onCleanup(() => subscription.remove());
62
+ }, { injector: this.injector });
63
+ return calendars.asReadonly();
64
+ }
65
+ };
66
+ return CalendarsService = _classThis;
67
+ })();
68
+ export { CalendarsService };
@@ -0,0 +1,6 @@
1
+ import { type Signal } from '@angular/core';
2
+ import { type Locale } from '../../../core';
3
+ export declare class LocalesService {
4
+ private readonly injector;
5
+ connect(): Signal<Locale[]>;
6
+ }
@@ -0,0 +1,69 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { effect, inject, Injectable, Injector, signal } from '@angular/core';
36
+ import { addLocaleListener, getLocales } from '../../../core';
37
+ // Angular twin of React's `useLocales` hook and Vue's `useLocales` composable. See
38
+ // packages/battery's BatteryStateService for the `connect()` pattern rationale. getLocales() is
39
+ // synchronous, so the signal seeds from it directly rather than an async `.then()` fetch.
40
+ //
41
+ // readonly locales = inject(LocalesService).connect();
42
+ // // template: {{ locales() }}
43
+ let LocalesService = (() => {
44
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
45
+ let _classDescriptor;
46
+ let _classExtraInitializers = [];
47
+ let _classThis;
48
+ var LocalesService = class {
49
+ static { _classThis = this; }
50
+ static {
51
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
52
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
53
+ LocalesService = _classThis = _classDescriptor.value;
54
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
55
+ __runInitializers(_classThis, _classExtraInitializers);
56
+ }
57
+ injector = inject(Injector);
58
+ connect() {
59
+ const locales = signal(getLocales());
60
+ effect(onCleanup => {
61
+ const subscription = addLocaleListener(() => locales.set(getLocales()));
62
+ onCleanup(() => subscription.remove());
63
+ }, { injector: this.injector });
64
+ return locales.asReadonly();
65
+ }
66
+ };
67
+ return LocalesService = _classThis;
68
+ })();
69
+ export { LocalesService };
@@ -0,0 +1,4 @@
1
+ export { getLocales, getCalendars } from './localization';
2
+ export { addLocaleListener, addCalendarListener } from './native-module';
3
+ export { Weekday, CalendarIdentifier, type Locale, type Calendar } from './types';
4
+ export type { EventSubscription } from 'expo-modules-core';
@@ -0,0 +1,3 @@
1
+ export { getLocales, getCalendars } from './localization';
2
+ export { addLocaleListener, addCalendarListener } from './native-module';
3
+ export { Weekday, CalendarIdentifier } from './types';
@@ -0,0 +1,39 @@
1
+ import type { Calendar, Locale } from './types';
2
+ /**
3
+ * List of user's locales, returned as an array of objects of type `Locale`.
4
+ * Guaranteed to contain at least 1 element.
5
+ * These are returned in the order the user defines in their device settings.
6
+ * On the web currency and measurements systems are not provided, instead returned as null.
7
+ * If needed, you can infer them from the current region using a lookup table.
8
+ * @example
9
+ * ```js
10
+ * [{
11
+ * "languageTag": "pl-PL",
12
+ * "languageCode": "pl",
13
+ * "textDirection": "ltr",
14
+ * "digitGroupingSeparator": " ",
15
+ * "decimalSeparator": ",",
16
+ * "measurementSystem": "metric",
17
+ * "currencyCode": "PLN",
18
+ * "currencySymbol": "zł",
19
+ * "regionCode": "PL",
20
+ * "temperatureUnit": "celsius"
21
+ * }]
22
+ * ```
23
+ */
24
+ export declare const getLocales: () => Locale[];
25
+ /**
26
+ * List of user's preferred calendars, returned as an array of objects of type `Calendar`.
27
+ * Guaranteed to contain at least 1 element.
28
+ * For now always returns a single element, but it's likely to return a user preference list on some platforms in the future.
29
+ * @example
30
+ * ```js
31
+ * [{
32
+ * "calendar": "gregory",
33
+ * "timeZone": "Europe/Warsaw",
34
+ * "uses24hourClock": true,
35
+ * "firstWeekday": 1
36
+ * }]
37
+ * ```
38
+ */
39
+ export declare const getCalendars: () => Calendar[];
@@ -0,0 +1,42 @@
1
+ // Hand-ported from .vendors/expo/packages/expo-localization/src/Localization.ts (sdk-57) —
2
+ // both functions delegate straight to the native module, no extra wrapping needed beyond typing
3
+ // them as synchronous getters.
4
+ import ExpoLocalizationModule from './native-module';
5
+ /**
6
+ * List of user's locales, returned as an array of objects of type `Locale`.
7
+ * Guaranteed to contain at least 1 element.
8
+ * These are returned in the order the user defines in their device settings.
9
+ * On the web currency and measurements systems are not provided, instead returned as null.
10
+ * If needed, you can infer them from the current region using a lookup table.
11
+ * @example
12
+ * ```js
13
+ * [{
14
+ * "languageTag": "pl-PL",
15
+ * "languageCode": "pl",
16
+ * "textDirection": "ltr",
17
+ * "digitGroupingSeparator": " ",
18
+ * "decimalSeparator": ",",
19
+ * "measurementSystem": "metric",
20
+ * "currencyCode": "PLN",
21
+ * "currencySymbol": "zł",
22
+ * "regionCode": "PL",
23
+ * "temperatureUnit": "celsius"
24
+ * }]
25
+ * ```
26
+ */
27
+ export const getLocales = ExpoLocalizationModule.getLocales;
28
+ /**
29
+ * List of user's preferred calendars, returned as an array of objects of type `Calendar`.
30
+ * Guaranteed to contain at least 1 element.
31
+ * For now always returns a single element, but it's likely to return a user preference list on some platforms in the future.
32
+ * @example
33
+ * ```js
34
+ * [{
35
+ * "calendar": "gregory",
36
+ * "timeZone": "Europe/Warsaw",
37
+ * "uses24hourClock": true,
38
+ * "firstWeekday": 1
39
+ * }]
40
+ * ```
41
+ */
42
+ export const getCalendars = ExpoLocalizationModule.getCalendars;
@@ -0,0 +1,12 @@
1
+ import { type EventSubscription } from 'expo-modules-core';
2
+ import type { Calendar, Locale } from './types';
3
+ export type INativeLocalizationModule = {
4
+ getLocales(): Locale[];
5
+ getCalendars(): Calendar[];
6
+ addListener(eventName: 'onLocaleSettingsChanged', listener: (event?: unknown) => void): EventSubscription;
7
+ addListener(eventName: 'onCalendarSettingsChanged', listener: (event?: unknown) => void): EventSubscription;
8
+ };
9
+ declare const ExpoLocalizationModule: INativeLocalizationModule;
10
+ export declare function addLocaleListener(listener: (event?: unknown) => void): EventSubscription;
11
+ export declare function addCalendarListener(listener: (event?: unknown) => void): EventSubscription;
12
+ export default ExpoLocalizationModule;
@@ -0,0 +1,21 @@
1
+ // Hand-ported from .vendors/expo/packages/expo-localization/src/ExpoLocalization.native.ts
2
+ // (sdk-57). getLocales()/getCalendars() are synchronous, JSI-bridged native calls (no await) —
3
+ // the native module itself implements them as sync-bridged functions, the same constants-style
4
+ // read pattern this repo already uses for expo-device's eagerly-resolved fields
5
+ // (packages/device/src/core/native-module.ts). One native module fans out two independent event
6
+ // streams (locale settings vs calendar settings) through the same addListener, keyed by event
7
+ // name — mirrors the three-event-names-on-one-addListener overload trick in
8
+ // packages/battery/src/core/native-module.ts.
9
+ import { requireNativeModule } from 'expo-modules-core';
10
+ const EXPO_LOCALIZATION_MODULE_NAME = 'ExpoLocalization';
11
+ const ExpoLocalizationModule = requireNativeModule(EXPO_LOCALIZATION_MODULE_NAME);
12
+ export function addLocaleListener(
13
+ // NOTE: upstream never uses the event's data — the listener is invoked purely as an
14
+ // invalidate signal for useLocales/the Vue composable/the Angular service below.
15
+ listener) {
16
+ return ExpoLocalizationModule.addListener('onLocaleSettingsChanged', listener);
17
+ }
18
+ export function addCalendarListener(listener) {
19
+ return ExpoLocalizationModule.addListener('onCalendarSettingsChanged', listener);
20
+ }
21
+ export default ExpoLocalizationModule;
@@ -0,0 +1,174 @@
1
+ export type Locale = {
2
+ /**
3
+ * An [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) with a region code.
4
+ * @example
5
+ * `'en-US'`, `'es-419'`, `'pl-PL'`.
6
+ */
7
+ languageTag: string;
8
+ /**
9
+ * An [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) without the region code.
10
+ * @example
11
+ * `'en'`, `'es'`, `'pl'`.
12
+ */
13
+ languageCode: string | null;
14
+ /**
15
+ * An [ISO 15924](https://en.wikipedia.org/wiki/ISO_15924) 4-letter script code. On Android and Web, it may be `null` if none is defined.
16
+ * @example
17
+ * `'Latn'`, `'Hans'`, `'Hebr'`.
18
+ */
19
+ languageScriptCode: string | null;
20
+ /**
21
+ * The region code for your device that comes from the Region setting under Language & Region on iOS, Region settings on Android and is parsed from locale on Web (can be `null` on Web).
22
+ * @example
23
+ * `'US'`.
24
+ */
25
+ regionCode: string | null;
26
+ /**
27
+ * The region code for the preferred language. When the language is not region-specific, it returns the same value as `regionCode`. When the language is region-specific, it returns the region code for the language (`en-CA` -> `CA`).
28
+ * Prefer using `regionCode` for any internalization purposes.
29
+ * @example
30
+ * `'US'`.
31
+ */
32
+ languageRegionCode: string | null;
33
+ /**
34
+ * Currency code for the locale.
35
+ * On iOS, it's the currency code from the `Region` setting under Language & Region, not for the current locale.
36
+ * On Android, it's the currency specifc to the locale in the list, as there are no separate settings for selecting a region.
37
+ * Is `null` on Web, use a table lookup based on region instead.
38
+ * @example
39
+ * `'USD'`, `'EUR'`, `'PLN'`.
40
+ */
41
+ currencyCode: string | null;
42
+ /**
43
+ * Currency symbol for the currency specified by `currencyCode`.
44
+ * @example
45
+ * `'$'`, `'€'`, `'zł'`.
46
+ */
47
+ currencySymbol: string | null;
48
+ /**
49
+ * Currency code for the locale.
50
+ * On iOS, it's the currency code for the current locale in the list, not the device region.
51
+ * On Android, it's equal to `currencyCode`.
52
+ * Is `null` on Web.
53
+ * Prefer using `currencyCode` for any internalization purposes.
54
+ * @example
55
+ * `'USD'`, `'EUR'`, `'PLN'`.
56
+ */
57
+ languageCurrencyCode: string | null;
58
+ /**
59
+ * Currency symbol for the currency specified by `languageCurrencyCode`.
60
+ * Prefer using `currencySymbol` for any internalization purposes.
61
+ * @example
62
+ * `'$'`, `'€'`, `'zł'`.
63
+ */
64
+ languageCurrencySymbol: string | null;
65
+ /**
66
+ * Decimal separator used for formatting numbers with fractional parts.
67
+ * @example
68
+ * `'.'`, `','`.
69
+ */
70
+ decimalSeparator: string | null;
71
+ /**
72
+ * Digit grouping separator used for formatting large numbers.
73
+ * @example
74
+ * `'.'`, `','`.
75
+ */
76
+ digitGroupingSeparator: string | null;
77
+ /**
78
+ * Text direction for the locale. One of: `'ltr'`, `'rtl'`.
79
+ */
80
+ textDirection: 'ltr' | 'rtl';
81
+ /**
82
+ * The measurement system used in the locale.
83
+ * Is `null` on Web, as user chosen measurement system is not exposed on the web and using locale to determine measurement systems is unreliable.
84
+ * Ask for user preferences if possible.
85
+ */
86
+ measurementSystem: `metric` | `us` | `uk` | null;
87
+ /**
88
+ * The temperature unit used in the locale.
89
+ * Returns `null` if the region code is unknown.
90
+ */
91
+ temperatureUnit: 'celsius' | 'fahrenheit' | null;
92
+ };
93
+ /**
94
+ * An enum mapping days of the week in Gregorian calendar to their index as returned by the `firstWeekday` property.
95
+ */
96
+ export declare enum Weekday {
97
+ SUNDAY = 1,
98
+ MONDAY = 2,
99
+ TUESDAY = 3,
100
+ WEDNESDAY = 4,
101
+ THURSDAY = 5,
102
+ FRIDAY = 6,
103
+ SATURDAY = 7
104
+ }
105
+ /**
106
+ * The calendar identifier, one of [Unicode calendar types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar).
107
+ */
108
+ export declare enum CalendarIdentifier {
109
+ /** Thai Buddhist calendar */
110
+ BUDDHIST = "buddhist",
111
+ /** Traditional Chinese calendar */
112
+ CHINESE = "chinese",
113
+ /** Coptic calendar */
114
+ COPTIC = "coptic",
115
+ /** Traditional Korean calendar */
116
+ DANGI = "dangi",
117
+ /** Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E) */
118
+ ETHIOAA = "ethioaa",
119
+ /** Ethiopic calendar, Amete Mihret (epoch approx, 8 C.E.) */
120
+ ETHIOPIC = "ethiopic",
121
+ /** Gregorian calendar (also referred to upstream as "gregorian" — dropped as a separate
122
+ * member here since this repo's lint bans duplicate enum values). */
123
+ GREGORY = "gregory",
124
+ /** Traditional Hebrew calendar */
125
+ HEBREW = "hebrew",
126
+ /** Indian calendar */
127
+ INDIAN = "indian",
128
+ /** Islamic calendar */
129
+ ISLAMIC = "islamic",
130
+ /** Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - civil epoch) */
131
+ ISLAMIC_CIVIL = "islamic-civil",
132
+ /** Islamic calendar, Saudi Arabia sighting */
133
+ ISLAMIC_RGSA = "islamic-rgsa",
134
+ /**Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - astronomical epoch) */
135
+ ISLAMIC_TBLA = "islamic-tbla",
136
+ /** Islamic calendar, Umm al-Qura */
137
+ ISLAMIC_UMALQURA = "islamic-umalqura",
138
+ /** ISO calendar (Gregorian calendar using the ISO 8601 calendar week rules) */
139
+ ISO8601 = "iso8601",
140
+ /** Japanese imperial calendar */
141
+ JAPANESE = "japanese",
142
+ /** Persian calendar */
143
+ PERSIAN = "persian",
144
+ /** Civil (algorithmic) Arabic calendar */
145
+ ROC = "roc"
146
+ }
147
+ export type Calendar = {
148
+ /**
149
+ * The calendar identifier, one of [Unicode calendar types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar).
150
+ *
151
+ * On Android is limited to one of device's [available calendar types](https://developer.android.com/reference/java/util/Calendar#getAvailableCalendarTypes()).
152
+ *
153
+ * On iOS uses [calendar identifiers](https://developer.apple.com/documentation/foundation/calendar/identifier), but maps them to the corresponding Unicode types, will also never contain `'dangi'` or `'islamic-rgsa'` due to it not being implemented on iOS.
154
+ */
155
+ calendar: CalendarIdentifier | null;
156
+ /**
157
+ * True when current device settings use 24-hour time format.
158
+ * Can be null on some browsers that don't support the [hourCycle](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle) property in [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) API.
159
+ */
160
+ uses24hourClock: boolean | null;
161
+ /**
162
+ * The first day of the week. For most calendars Sunday is numbered `1`, with Saturday being number `7`.
163
+ * Can be null on some browsers that don't support the [weekInfo](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/weekInfo) property in [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) API.
164
+ * @example
165
+ * `1`, `7`.
166
+ */
167
+ firstWeekday: Weekday | null;
168
+ /**
169
+ * Time zone for the calendar. Can be `null` on Web.
170
+ * @example
171
+ * `'America/Los_Angeles'`, `'Europe/Warsaw'`, `'GMT+1'`.
172
+ */
173
+ timeZone: string | null;
174
+ };