@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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/build/angular/index.d.ts +3 -0
- package/build/angular/index.js +7 -0
- package/build/angular/services/calendars.service/index.d.ts +6 -0
- package/build/angular/services/calendars.service/index.js +68 -0
- package/build/angular/services/locales.service/index.d.ts +6 -0
- package/build/angular/services/locales.service/index.js +69 -0
- package/build/core/index.d.ts +4 -0
- package/build/core/index.js +3 -0
- package/build/core/localization.d.ts +39 -0
- package/build/core/localization.js +42 -0
- package/build/core/native-module.d.ts +12 -0
- package/build/core/native-module.js +21 -0
- package/build/core/types.d.ts +174 -0
- package/build/core/types.js +59 -0
- package/build/react/hooks/use-calendars/index.d.ts +2 -0
- package/build/react/hooks/use-calendars/index.js +13 -0
- package/build/react/hooks/use-locales/index.d.ts +2 -0
- package/build/react/hooks/use-locales/index.js +15 -0
- package/build/react/index.d.ts +3 -0
- package/build/react/index.js +7 -0
- package/build/vue/composables/use-calendars/index.d.ts +3 -0
- package/build/vue/composables/use-calendars/index.js +16 -0
- package/build/vue/composables/use-locales/index.d.ts +3 -0
- package/build/vue/composables/use-locales/index.js +20 -0
- package/build/vue/index.d.ts +3 -0
- package/build/vue/index.js +7 -0
- package/build-ngc/angular/index.d.ts +3 -0
- package/build-ngc/angular/index.js +8 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/services/calendars.service/index.d.ts +9 -0
- package/build-ngc/angular/services/calendars.service/index.js +27 -0
- package/build-ngc/angular/services/calendars.service/index.js.map +1 -0
- package/build-ngc/angular/services/locales.service/index.d.ts +9 -0
- package/build-ngc/angular/services/locales.service/index.js +28 -0
- package/build-ngc/angular/services/locales.service/index.js.map +1 -0
- package/build-ngc/core/index.d.ts +4 -0
- package/build-ngc/core/index.js +4 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/localization.d.ts +39 -0
- package/build-ngc/core/localization.js +43 -0
- package/build-ngc/core/localization.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +12 -0
- package/build-ngc/core/native-module.js +22 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/types.d.ts +174 -0
- package/build-ngc/core/types.js +60 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +107 -0
- package/src/angular/index.ts +17 -0
- package/src/angular/services/calendars.service/calendars.service.test.ts +102 -0
- package/src/angular/services/calendars.service/index.ts +26 -0
- package/src/angular/services/locales.service/index.ts +27 -0
- package/src/angular/services/locales.service/locales.service.test.ts +122 -0
- package/src/core/index.ts +4 -0
- package/src/core/localization.test.ts +64 -0
- package/src/core/localization.ts +45 -0
- package/src/core/native-module.ts +43 -0
- package/src/core/types.ts +181 -0
- package/src/react/hooks/use-calendars/index.ts +16 -0
- package/src/react/hooks/use-calendars/use-calendars.test.tsx +87 -0
- package/src/react/hooks/use-locales/index.ts +18 -0
- package/src/react/hooks/use-locales/use-locales.test.tsx +107 -0
- package/src/react/index.ts +18 -0
- package/src/vue/composables/use-calendars/index.ts +25 -0
- package/src/vue/composables/use-calendars/use-calendars.test.ts +96 -0
- package/src/vue/composables/use-locales/index.ts +24 -0
- package/src/vue/composables/use-locales/use-locales.test.ts +116 -0
- package/src/vue/index.ts +18 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Hand-ported verbatim from .vendors/expo/packages/expo-localization/src/Localization.types.ts
|
|
2
|
+
// (sdk-57) — plain data shapes, no expo-modules-core dependency at all. Every JSDoc comment is
|
|
3
|
+
// kept as-is: they document real, per-platform behavior differences, not restated obviousness.
|
|
4
|
+
/**
|
|
5
|
+
* An enum mapping days of the week in Gregorian calendar to their index as returned by the `firstWeekday` property.
|
|
6
|
+
*/
|
|
7
|
+
export var Weekday;
|
|
8
|
+
(function (Weekday) {
|
|
9
|
+
Weekday[Weekday["SUNDAY"] = 1] = "SUNDAY";
|
|
10
|
+
Weekday[Weekday["MONDAY"] = 2] = "MONDAY";
|
|
11
|
+
Weekday[Weekday["TUESDAY"] = 3] = "TUESDAY";
|
|
12
|
+
Weekday[Weekday["WEDNESDAY"] = 4] = "WEDNESDAY";
|
|
13
|
+
Weekday[Weekday["THURSDAY"] = 5] = "THURSDAY";
|
|
14
|
+
Weekday[Weekday["FRIDAY"] = 6] = "FRIDAY";
|
|
15
|
+
Weekday[Weekday["SATURDAY"] = 7] = "SATURDAY";
|
|
16
|
+
})(Weekday || (Weekday = {}));
|
|
17
|
+
/**
|
|
18
|
+
* The calendar identifier, one of [Unicode calendar types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar).
|
|
19
|
+
*/
|
|
20
|
+
export var CalendarIdentifier;
|
|
21
|
+
(function (CalendarIdentifier) {
|
|
22
|
+
/** Thai Buddhist calendar */
|
|
23
|
+
CalendarIdentifier["BUDDHIST"] = "buddhist";
|
|
24
|
+
/** Traditional Chinese calendar */
|
|
25
|
+
CalendarIdentifier["CHINESE"] = "chinese";
|
|
26
|
+
/** Coptic calendar */
|
|
27
|
+
CalendarIdentifier["COPTIC"] = "coptic";
|
|
28
|
+
/** Traditional Korean calendar */
|
|
29
|
+
CalendarIdentifier["DANGI"] = "dangi";
|
|
30
|
+
/** Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E) */
|
|
31
|
+
CalendarIdentifier["ETHIOAA"] = "ethioaa";
|
|
32
|
+
/** Ethiopic calendar, Amete Mihret (epoch approx, 8 C.E.) */
|
|
33
|
+
CalendarIdentifier["ETHIOPIC"] = "ethiopic";
|
|
34
|
+
/** Gregorian calendar (also referred to upstream as "gregorian" — dropped as a separate
|
|
35
|
+
* member here since this repo's lint bans duplicate enum values). */
|
|
36
|
+
CalendarIdentifier["GREGORY"] = "gregory";
|
|
37
|
+
/** Traditional Hebrew calendar */
|
|
38
|
+
CalendarIdentifier["HEBREW"] = "hebrew";
|
|
39
|
+
/** Indian calendar */
|
|
40
|
+
CalendarIdentifier["INDIAN"] = "indian";
|
|
41
|
+
/** Islamic calendar */
|
|
42
|
+
CalendarIdentifier["ISLAMIC"] = "islamic";
|
|
43
|
+
/** Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - civil epoch) */
|
|
44
|
+
CalendarIdentifier["ISLAMIC_CIVIL"] = "islamic-civil";
|
|
45
|
+
/** Islamic calendar, Saudi Arabia sighting */
|
|
46
|
+
CalendarIdentifier["ISLAMIC_RGSA"] = "islamic-rgsa";
|
|
47
|
+
/**Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - astronomical epoch) */
|
|
48
|
+
CalendarIdentifier["ISLAMIC_TBLA"] = "islamic-tbla";
|
|
49
|
+
/** Islamic calendar, Umm al-Qura */
|
|
50
|
+
CalendarIdentifier["ISLAMIC_UMALQURA"] = "islamic-umalqura";
|
|
51
|
+
/** ISO calendar (Gregorian calendar using the ISO 8601 calendar week rules) */
|
|
52
|
+
CalendarIdentifier["ISO8601"] = "iso8601";
|
|
53
|
+
/** Japanese imperial calendar */
|
|
54
|
+
CalendarIdentifier["JAPANESE"] = "japanese";
|
|
55
|
+
/** Persian calendar */
|
|
56
|
+
CalendarIdentifier["PERSIAN"] = "persian";
|
|
57
|
+
/** Civil (algorithmic) Arabic calendar */
|
|
58
|
+
CalendarIdentifier["ROC"] = "roc";
|
|
59
|
+
})(CalendarIdentifier || (CalendarIdentifier = {}));
|
|
60
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AA+F/F;;GAEG;AACH,MAAM,CAAN,IAAY,OAQX;AARD,WAAY,OAAO;IACjB,yCAAU,CAAA;IACV,yCAAU,CAAA;IACV,2CAAW,CAAA;IACX,+CAAa,CAAA;IACb,6CAAY,CAAA;IACZ,yCAAU,CAAA;IACV,6CAAY,CAAA;AACd,CAAC,EARW,OAAO,KAAP,OAAO,QAQlB;AAED;;GAEG;AACH,MAAM,CAAN,IAAY,kBAsCX;AAtCD,WAAY,kBAAkB;IAC5B,6BAA6B;IAC7B,2CAAqB,CAAA;IACrB,mCAAmC;IACnC,yCAAmB,CAAA;IACnB,sBAAsB;IACtB,uCAAiB,CAAA;IACjB,kCAAkC;IAClC,qCAAe,CAAA;IACf,+DAA+D;IAC/D,yCAAmB,CAAA;IACnB,6DAA6D;IAC7D,2CAAqB,CAAA;IACrB;yEACqE;IACrE,yCAAmB,CAAA;IACnB,kCAAkC;IAClC,uCAAiB,CAAA;IACjB,sBAAsB;IACtB,uCAAiB,CAAA;IACjB,uBAAuB;IACvB,yCAAmB,CAAA;IACnB,kGAAkG;IAClG,qDAA+B,CAAA;IAC/B,8CAA8C;IAC9C,mDAA6B,CAAA;IAC7B,wGAAwG;IACxG,mDAA6B,CAAA;IAC7B,oCAAoC;IACpC,2DAAqC,CAAA;IACrC,+EAA+E;IAC/E,yCAAmB,CAAA;IACnB,iCAAiC;IACjC,2CAAqB,CAAA;IACrB,uBAAuB;IACvB,yCAAmB,CAAA;IACnB,0CAA0C;IAC1C,iCAAW,CAAA;AACb,CAAC,EAtCW,kBAAkB,KAAlB,kBAAkB,QAsC7B"}
|
package/native-link.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@symbiote-native/localization",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "expo-localization wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Two independent synchronous getters (getLocales, getCalendars), each with its own native change listener and its own reactive hook per adapter.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/OneEyed1366/symbiote-native.git",
|
|
9
|
+
"directory": "packages/localization"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/localization#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/OneEyed1366/symbiote-native/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "Andrey Prokopenko <psevdoproger@gmail.com>",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./build/core/index.js",
|
|
18
|
+
"module": "./build/core/index.js",
|
|
19
|
+
"types": "./build/core/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./build/core/index.d.ts",
|
|
23
|
+
"default": "./build/core/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./vue": {
|
|
26
|
+
"types": "./build/vue/index.d.ts",
|
|
27
|
+
"default": "./build/vue/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./build/react/index.d.ts",
|
|
31
|
+
"default": "./build/react/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./angular": {
|
|
34
|
+
"types": "./build-ngc/angular/index.d.ts",
|
|
35
|
+
"react-native": "./build-ngc/angular/index.js",
|
|
36
|
+
"default": "./src/angular/index.ts"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"build",
|
|
42
|
+
"build-ngc",
|
|
43
|
+
"native-link.json"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"expo-localization": "57.0.1",
|
|
50
|
+
"expo-modules-core": "57.0.5"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@symbiote-native/engine": ">=0.1.7",
|
|
54
|
+
"@angular/core": ">=20",
|
|
55
|
+
"@vue/runtime-core": "^3.5.13",
|
|
56
|
+
"react": ">=19.0.0",
|
|
57
|
+
"react-native": ">=0.86",
|
|
58
|
+
"vue": ">=3.5.0",
|
|
59
|
+
"@symbiote-native/angular": "0.6.1",
|
|
60
|
+
"@symbiote-native/react": "0.2.8",
|
|
61
|
+
"@symbiote-native/vue": "0.3.8"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@symbiote-native/angular": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@symbiote-native/react": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@symbiote-native/vue": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@angular/core": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@vue/runtime-core": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"react": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"vue": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@angular/compiler": "^22",
|
|
88
|
+
"@angular/compiler-cli": "^22",
|
|
89
|
+
"@angular/core": "^22",
|
|
90
|
+
"@types/node": "^26.0.0",
|
|
91
|
+
"@types/react": "^19.2.0",
|
|
92
|
+
"@vue/runtime-core": "^3.5.13",
|
|
93
|
+
"react": "19.2.3",
|
|
94
|
+
"typescript": "~6.0.0",
|
|
95
|
+
"@symbiote-native/angular": "0.6.1",
|
|
96
|
+
"@symbiote-native/react": "0.2.8",
|
|
97
|
+
"@symbiote-native/vue": "0.3.8",
|
|
98
|
+
"@symbiote-native/test-utils": "0.1.6",
|
|
99
|
+
"@symbiote-native/engine": "0.1.7"
|
|
100
|
+
},
|
|
101
|
+
"scripts": {
|
|
102
|
+
"typecheck": "tsc --build",
|
|
103
|
+
"clean": "rm -rf build-ngc",
|
|
104
|
+
"ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
|
|
105
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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 {
|
|
8
|
+
getLocales,
|
|
9
|
+
getCalendars,
|
|
10
|
+
addLocaleListener,
|
|
11
|
+
addCalendarListener,
|
|
12
|
+
Weekday,
|
|
13
|
+
CalendarIdentifier,
|
|
14
|
+
type Locale,
|
|
15
|
+
type Calendar,
|
|
16
|
+
type EventSubscription,
|
|
17
|
+
} from '../core';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Co-located Angular-driven test (ADR 0025) for CalendarsService. See locales.service.test.ts for
|
|
2
|
+
// the shared rationale.
|
|
3
|
+
|
|
4
|
+
import '@angular/compiler';
|
|
5
|
+
import { Component, inject, type Signal } from '@angular/core';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { mount, unmount } from '@symbiote-native/angular';
|
|
8
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
+
import { CalendarIdentifier, Weekday, type Calendar } from '../../../core/types';
|
|
10
|
+
import { CalendarsService } from './index';
|
|
11
|
+
|
|
12
|
+
const FAKE_CALENDARS_INITIAL: Calendar[] = [
|
|
13
|
+
{
|
|
14
|
+
calendar: CalendarIdentifier.GREGORY,
|
|
15
|
+
uses24hourClock: true,
|
|
16
|
+
firstWeekday: Weekday.SUNDAY,
|
|
17
|
+
timeZone: 'Europe/Warsaw',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const FAKE_CALENDARS_UPDATED: Calendar[] = [
|
|
22
|
+
{
|
|
23
|
+
calendar: CalendarIdentifier.ISO8601,
|
|
24
|
+
uses24hourClock: false,
|
|
25
|
+
firstWeekday: Weekday.MONDAY,
|
|
26
|
+
timeZone: 'America/New_York',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const addListenerMock = vi.fn();
|
|
31
|
+
const removeMock = vi.fn();
|
|
32
|
+
const getCalendarsMock = vi.fn(() => FAKE_CALENDARS_INITIAL);
|
|
33
|
+
|
|
34
|
+
vi.mock('../../../core', () => ({
|
|
35
|
+
addCalendarListener: (listener: () => void) => addListenerMock(listener),
|
|
36
|
+
getCalendars: () => getCalendarsMock(),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const ROOT_TAG = 975;
|
|
40
|
+
const fabric = installFabric();
|
|
41
|
+
const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
|
|
42
|
+
|
|
43
|
+
let capturedResult: Signal<Calendar[]> | undefined;
|
|
44
|
+
let capturedListener: (() => void) | undefined;
|
|
45
|
+
|
|
46
|
+
@Component({
|
|
47
|
+
selector: 'symbiote-calendars-host',
|
|
48
|
+
standalone: true,
|
|
49
|
+
template: '',
|
|
50
|
+
})
|
|
51
|
+
class CalendarsHost {
|
|
52
|
+
readonly calendars = inject(CalendarsService).connect();
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
capturedResult = this.calendars;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
capturedResult = undefined;
|
|
61
|
+
capturedListener = undefined;
|
|
62
|
+
getCalendarsMock.mockReturnValue(FAKE_CALENDARS_INITIAL);
|
|
63
|
+
addListenerMock.mockImplementation(listener => {
|
|
64
|
+
capturedListener = listener;
|
|
65
|
+
return { remove: removeMock };
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
unmount(ROOT_TAG);
|
|
71
|
+
fabric.reset();
|
|
72
|
+
vi.clearAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('CalendarsService.connect', () => {
|
|
76
|
+
it('reads the current calendars synchronously', () => {
|
|
77
|
+
mount(ROOT_TAG, CalendarsHost);
|
|
78
|
+
|
|
79
|
+
expect(capturedResult?.()).toEqual(FAKE_CALENDARS_INITIAL);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('recomputes the signal when the registered listener fires', async () => {
|
|
83
|
+
mount(ROOT_TAG, CalendarsHost);
|
|
84
|
+
await tick();
|
|
85
|
+
|
|
86
|
+
if (capturedListener === undefined) throw new Error('addListener callback was not captured');
|
|
87
|
+
getCalendarsMock.mockReturnValue(FAKE_CALENDARS_UPDATED);
|
|
88
|
+
capturedListener();
|
|
89
|
+
|
|
90
|
+
expect(capturedResult?.()).toEqual(FAKE_CALENDARS_UPDATED);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('removes the subscription when the host component is unmounted', async () => {
|
|
94
|
+
mount(ROOT_TAG, CalendarsHost);
|
|
95
|
+
await tick();
|
|
96
|
+
|
|
97
|
+
unmount(ROOT_TAG);
|
|
98
|
+
await tick();
|
|
99
|
+
|
|
100
|
+
expect(removeMock).toHaveBeenCalledOnce();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
|
|
2
|
+
import { addCalendarListener, getCalendars, type Calendar } from '../../../core';
|
|
3
|
+
|
|
4
|
+
// Angular twin of React's `useCalendars` hook and Vue's `useCalendars` composable — mirrors
|
|
5
|
+
// LocalesService's shape exactly.
|
|
6
|
+
//
|
|
7
|
+
// readonly calendars = inject(CalendarsService).connect();
|
|
8
|
+
// // template: {{ calendars() }}
|
|
9
|
+
@Injectable({ providedIn: 'root' })
|
|
10
|
+
export class CalendarsService {
|
|
11
|
+
private readonly injector = inject(Injector);
|
|
12
|
+
|
|
13
|
+
connect(): Signal<Calendar[]> {
|
|
14
|
+
const calendars = signal<Calendar[]>(getCalendars());
|
|
15
|
+
|
|
16
|
+
effect(
|
|
17
|
+
onCleanup => {
|
|
18
|
+
const subscription = addCalendarListener(() => calendars.set(getCalendars()));
|
|
19
|
+
onCleanup(() => subscription.remove());
|
|
20
|
+
},
|
|
21
|
+
{ injector: this.injector },
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return calendars.asReadonly();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
|
|
2
|
+
import { addLocaleListener, getLocales, type Locale } from '../../../core';
|
|
3
|
+
|
|
4
|
+
// Angular twin of React's `useLocales` hook and Vue's `useLocales` composable. See
|
|
5
|
+
// packages/battery's BatteryStateService for the `connect()` pattern rationale. getLocales() is
|
|
6
|
+
// synchronous, so the signal seeds from it directly rather than an async `.then()` fetch.
|
|
7
|
+
//
|
|
8
|
+
// readonly locales = inject(LocalesService).connect();
|
|
9
|
+
// // template: {{ locales() }}
|
|
10
|
+
@Injectable({ providedIn: 'root' })
|
|
11
|
+
export class LocalesService {
|
|
12
|
+
private readonly injector = inject(Injector);
|
|
13
|
+
|
|
14
|
+
connect(): Signal<Locale[]> {
|
|
15
|
+
const locales = signal<Locale[]>(getLocales());
|
|
16
|
+
|
|
17
|
+
effect(
|
|
18
|
+
onCleanup => {
|
|
19
|
+
const subscription = addLocaleListener(() => locales.set(getLocales()));
|
|
20
|
+
onCleanup(() => subscription.remove());
|
|
21
|
+
},
|
|
22
|
+
{ injector: this.injector },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return locales.asReadonly();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Co-located Angular-driven test (ADR 0025) for LocalesService. See packages/battery's
|
|
2
|
+
// battery-state.service.test.ts for the shared rationale.
|
|
3
|
+
|
|
4
|
+
import '@angular/compiler';
|
|
5
|
+
import { Component, inject, type Signal } from '@angular/core';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { mount, unmount } from '@symbiote-native/angular';
|
|
8
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
+
import type { Locale } from '../../../core';
|
|
10
|
+
import { LocalesService } from './index';
|
|
11
|
+
|
|
12
|
+
const FAKE_LOCALES_INITIAL: Locale[] = [
|
|
13
|
+
{
|
|
14
|
+
languageTag: 'pl-PL',
|
|
15
|
+
languageCode: 'pl',
|
|
16
|
+
languageScriptCode: 'Latn',
|
|
17
|
+
regionCode: 'PL',
|
|
18
|
+
languageRegionCode: 'PL',
|
|
19
|
+
currencyCode: 'PLN',
|
|
20
|
+
currencySymbol: 'zł',
|
|
21
|
+
languageCurrencyCode: 'PLN',
|
|
22
|
+
languageCurrencySymbol: 'zł',
|
|
23
|
+
decimalSeparator: ',',
|
|
24
|
+
digitGroupingSeparator: ' ',
|
|
25
|
+
textDirection: 'ltr',
|
|
26
|
+
measurementSystem: 'metric',
|
|
27
|
+
temperatureUnit: 'celsius',
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const FAKE_LOCALES_UPDATED: Locale[] = [
|
|
32
|
+
{
|
|
33
|
+
languageTag: 'en-US',
|
|
34
|
+
languageCode: 'en',
|
|
35
|
+
languageScriptCode: 'Latn',
|
|
36
|
+
regionCode: 'US',
|
|
37
|
+
languageRegionCode: 'US',
|
|
38
|
+
currencyCode: 'USD',
|
|
39
|
+
currencySymbol: '$',
|
|
40
|
+
languageCurrencyCode: 'USD',
|
|
41
|
+
languageCurrencySymbol: '$',
|
|
42
|
+
decimalSeparator: '.',
|
|
43
|
+
digitGroupingSeparator: ',',
|
|
44
|
+
textDirection: 'ltr',
|
|
45
|
+
measurementSystem: 'us',
|
|
46
|
+
temperatureUnit: 'fahrenheit',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const addListenerMock = vi.fn();
|
|
51
|
+
const removeMock = vi.fn();
|
|
52
|
+
const getLocalesMock = vi.fn(() => FAKE_LOCALES_INITIAL);
|
|
53
|
+
|
|
54
|
+
vi.mock('../../../core', () => ({
|
|
55
|
+
addLocaleListener: (listener: () => void) => addListenerMock(listener),
|
|
56
|
+
getLocales: () => getLocalesMock(),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const ROOT_TAG = 974;
|
|
60
|
+
const fabric = installFabric();
|
|
61
|
+
const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
|
|
62
|
+
|
|
63
|
+
let capturedResult: Signal<Locale[]> | undefined;
|
|
64
|
+
let capturedListener: (() => void) | undefined;
|
|
65
|
+
|
|
66
|
+
@Component({
|
|
67
|
+
selector: 'symbiote-locales-host',
|
|
68
|
+
standalone: true,
|
|
69
|
+
template: '',
|
|
70
|
+
})
|
|
71
|
+
class LocalesHost {
|
|
72
|
+
readonly locales = inject(LocalesService).connect();
|
|
73
|
+
|
|
74
|
+
constructor() {
|
|
75
|
+
capturedResult = this.locales;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
capturedResult = undefined;
|
|
81
|
+
capturedListener = undefined;
|
|
82
|
+
getLocalesMock.mockReturnValue(FAKE_LOCALES_INITIAL);
|
|
83
|
+
addListenerMock.mockImplementation(listener => {
|
|
84
|
+
capturedListener = listener;
|
|
85
|
+
return { remove: removeMock };
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
unmount(ROOT_TAG);
|
|
91
|
+
fabric.reset();
|
|
92
|
+
vi.clearAllMocks();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('LocalesService.connect', () => {
|
|
96
|
+
it('reads the current locales synchronously', () => {
|
|
97
|
+
mount(ROOT_TAG, LocalesHost);
|
|
98
|
+
|
|
99
|
+
expect(capturedResult?.()).toEqual(FAKE_LOCALES_INITIAL);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('recomputes the signal when the registered listener fires', async () => {
|
|
103
|
+
mount(ROOT_TAG, LocalesHost);
|
|
104
|
+
await tick();
|
|
105
|
+
|
|
106
|
+
if (capturedListener === undefined) throw new Error('addListener callback was not captured');
|
|
107
|
+
getLocalesMock.mockReturnValue(FAKE_LOCALES_UPDATED);
|
|
108
|
+
capturedListener();
|
|
109
|
+
|
|
110
|
+
expect(capturedResult?.()).toEqual(FAKE_LOCALES_UPDATED);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('removes the subscription when the host component is unmounted', async () => {
|
|
114
|
+
mount(ROOT_TAG, LocalesHost);
|
|
115
|
+
await tick();
|
|
116
|
+
|
|
117
|
+
unmount(ROOT_TAG);
|
|
118
|
+
await tick();
|
|
119
|
+
|
|
120
|
+
expect(removeMock).toHaveBeenCalledOnce();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -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,64 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { CalendarIdentifier, Weekday, type Calendar, type Locale } from './types';
|
|
3
|
+
|
|
4
|
+
const FAKE_LOCALES: Locale[] = [
|
|
5
|
+
{
|
|
6
|
+
languageTag: 'pl-PL',
|
|
7
|
+
languageCode: 'pl',
|
|
8
|
+
languageScriptCode: 'Latn',
|
|
9
|
+
regionCode: 'PL',
|
|
10
|
+
languageRegionCode: 'PL',
|
|
11
|
+
currencyCode: 'PLN',
|
|
12
|
+
currencySymbol: 'zł',
|
|
13
|
+
languageCurrencyCode: 'PLN',
|
|
14
|
+
languageCurrencySymbol: 'zł',
|
|
15
|
+
decimalSeparator: ',',
|
|
16
|
+
digitGroupingSeparator: ' ',
|
|
17
|
+
textDirection: 'ltr',
|
|
18
|
+
measurementSystem: 'metric',
|
|
19
|
+
temperatureUnit: 'celsius',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const FAKE_CALENDARS: Calendar[] = [
|
|
24
|
+
{
|
|
25
|
+
calendar: CalendarIdentifier.GREGORY,
|
|
26
|
+
uses24hourClock: true,
|
|
27
|
+
firstWeekday: Weekday.MONDAY,
|
|
28
|
+
timeZone: 'Europe/Warsaw',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const getLocalesMock = vi.fn(() => FAKE_LOCALES);
|
|
33
|
+
const getCalendarsMock = vi.fn(() => FAKE_CALENDARS);
|
|
34
|
+
|
|
35
|
+
// The real ExpoLocalization native module only exists on device — resolving it via
|
|
36
|
+
// requireNativeModule() at import time would throw in this headless test run, so the
|
|
37
|
+
// module-lookup file is faked in place of expo-modules-core's runtime resolution, the same
|
|
38
|
+
// pattern packages/battery/src/core/battery.test.ts uses.
|
|
39
|
+
vi.mock('./native-module', () => ({
|
|
40
|
+
default: {
|
|
41
|
+
getLocales: getLocalesMock,
|
|
42
|
+
getCalendars: getCalendarsMock,
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const { getLocales, getCalendars } = await import('./localization');
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('getLocales', () => {
|
|
53
|
+
it('delegates to the native module', () => {
|
|
54
|
+
expect(getLocales()).toEqual(FAKE_LOCALES);
|
|
55
|
+
expect(getLocalesMock).toHaveBeenCalledOnce();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('getCalendars', () => {
|
|
60
|
+
it('delegates to the native module', () => {
|
|
61
|
+
expect(getCalendars()).toEqual(FAKE_CALENDARS);
|
|
62
|
+
expect(getCalendarsMock).toHaveBeenCalledOnce();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
import type { Calendar, Locale } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* List of user's locales, returned as an array of objects of type `Locale`.
|
|
9
|
+
* Guaranteed to contain at least 1 element.
|
|
10
|
+
* These are returned in the order the user defines in their device settings.
|
|
11
|
+
* On the web currency and measurements systems are not provided, instead returned as null.
|
|
12
|
+
* If needed, you can infer them from the current region using a lookup table.
|
|
13
|
+
* @example
|
|
14
|
+
* ```js
|
|
15
|
+
* [{
|
|
16
|
+
* "languageTag": "pl-PL",
|
|
17
|
+
* "languageCode": "pl",
|
|
18
|
+
* "textDirection": "ltr",
|
|
19
|
+
* "digitGroupingSeparator": " ",
|
|
20
|
+
* "decimalSeparator": ",",
|
|
21
|
+
* "measurementSystem": "metric",
|
|
22
|
+
* "currencyCode": "PLN",
|
|
23
|
+
* "currencySymbol": "zł",
|
|
24
|
+
* "regionCode": "PL",
|
|
25
|
+
* "temperatureUnit": "celsius"
|
|
26
|
+
* }]
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const getLocales: () => Locale[] = ExpoLocalizationModule.getLocales;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List of user's preferred calendars, returned as an array of objects of type `Calendar`.
|
|
33
|
+
* Guaranteed to contain at least 1 element.
|
|
34
|
+
* For now always returns a single element, but it's likely to return a user preference list on some platforms in the future.
|
|
35
|
+
* @example
|
|
36
|
+
* ```js
|
|
37
|
+
* [{
|
|
38
|
+
* "calendar": "gregory",
|
|
39
|
+
* "timeZone": "Europe/Warsaw",
|
|
40
|
+
* "uses24hourClock": true,
|
|
41
|
+
* "firstWeekday": 1
|
|
42
|
+
* }]
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const getCalendars: () => Calendar[] = ExpoLocalizationModule.getCalendars;
|
|
@@ -0,0 +1,43 @@
|
|
|
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 { type EventSubscription, requireNativeModule } from 'expo-modules-core';
|
|
10
|
+
import type { Calendar, Locale } from './types';
|
|
11
|
+
|
|
12
|
+
const EXPO_LOCALIZATION_MODULE_NAME = 'ExpoLocalization';
|
|
13
|
+
|
|
14
|
+
export type INativeLocalizationModule = {
|
|
15
|
+
getLocales(): Locale[];
|
|
16
|
+
getCalendars(): Calendar[];
|
|
17
|
+
addListener(
|
|
18
|
+
eventName: 'onLocaleSettingsChanged',
|
|
19
|
+
listener: (event?: unknown) => void,
|
|
20
|
+
): EventSubscription;
|
|
21
|
+
addListener(
|
|
22
|
+
eventName: 'onCalendarSettingsChanged',
|
|
23
|
+
listener: (event?: unknown) => void,
|
|
24
|
+
): EventSubscription;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ExpoLocalizationModule = requireNativeModule<INativeLocalizationModule>(
|
|
28
|
+
EXPO_LOCALIZATION_MODULE_NAME,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export function addLocaleListener(
|
|
32
|
+
// NOTE: upstream never uses the event's data — the listener is invoked purely as an
|
|
33
|
+
// invalidate signal for useLocales/the Vue composable/the Angular service below.
|
|
34
|
+
listener: (event?: unknown) => void,
|
|
35
|
+
): EventSubscription {
|
|
36
|
+
return ExpoLocalizationModule.addListener('onLocaleSettingsChanged', listener);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function addCalendarListener(listener: (event?: unknown) => void): EventSubscription {
|
|
40
|
+
return ExpoLocalizationModule.addListener('onCalendarSettingsChanged', listener);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default ExpoLocalizationModule;
|